mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Merge branch 'main' into theme_plugin
# Conflicts: # crates/egui/src/widget_style.rs
This commit is contained in:
60
CHANGELOG.md
60
CHANGELOG.md
@@ -14,6 +14,66 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
### Highlights
|
||||
|
||||
384
Cargo.lock
384
Cargo.lock
File diff suppressed because it is too large
Load Diff
235
Cargo.toml
235
Cargo.toml
@@ -25,7 +25,7 @@ members = [
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
version = "0.35.0"
|
||||
version = "0.36.1"
|
||||
|
||||
|
||||
[profile.release]
|
||||
@@ -56,23 +56,23 @@ opt-level = 2
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
emath = { version = "0.35.0", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.35.0", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.35.0", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.35.0", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.35.0", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.35.0", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.35.0", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.35.0", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.35.0", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.35.0", path = "crates/egui_glow", default-features = false }
|
||||
egui_inspection = { version = "0.35.0", path = "crates/egui_inspection", default-features = false }
|
||||
egui_kittest = { version = "0.35.0", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.35.0", path = "crates/eframe", default-features = false }
|
||||
emath = { version = "0.36.1", path = "crates/emath", default-features = false }
|
||||
ecolor = { version = "0.36.1", path = "crates/ecolor", default-features = false }
|
||||
epaint = { version = "0.36.1", path = "crates/epaint", default-features = false }
|
||||
epaint_default_fonts = { version = "0.36.1", path = "crates/epaint_default_fonts" }
|
||||
egui = { version = "0.36.1", path = "crates/egui", default-features = false }
|
||||
egui-winit = { version = "0.36.1", path = "crates/egui-winit", default-features = false }
|
||||
egui_extras = { version = "0.36.1", path = "crates/egui_extras", default-features = false }
|
||||
egui-wgpu = { version = "0.36.1", path = "crates/egui-wgpu", default-features = false }
|
||||
egui_demo_lib = { version = "0.36.1", path = "crates/egui_demo_lib", default-features = false }
|
||||
egui_glow = { version = "0.36.1", path = "crates/egui_glow", default-features = false }
|
||||
egui_inspection = { version = "0.36.1", path = "crates/egui_inspection", default-features = false }
|
||||
egui_kittest = { version = "0.36.1", path = "crates/egui_kittest", default-features = false }
|
||||
eframe = { version = "0.36.1", path = "crates/eframe", default-features = false }
|
||||
|
||||
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_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 = [
|
||||
"no-rng", # we don't need DOS-protection, so we let users opt-in to it instead
|
||||
"std",
|
||||
@@ -89,23 +89,19 @@ dify = { version = "0.8.0", default-features = false }
|
||||
directories = "6.0"
|
||||
document-features = "0.2.12"
|
||||
ehttp = { version = "0.7.1", default-features = false }
|
||||
enum-map = "2.7"
|
||||
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
|
||||
font-types = { version = "0.11.3", default-features = false, features = [
|
||||
"std",
|
||||
] } # 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"
|
||||
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/2.x, duplicating the 0.1.x that android_logger needs
|
||||
font-types = { version = "0.12.2", default-features = false, features = ["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
|
||||
glutin = { version = "0.32.3", 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
|
||||
home = "0.5.12"
|
||||
harfrust = "0.12.0"
|
||||
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"
|
||||
jiff = { version = "0.2.29", default-features = false }
|
||||
jiff = { version = "0.2.35", default-features = false }
|
||||
js-sys = "0.3.103"
|
||||
kittest = { version = "0.4.0" }
|
||||
log = { version = "0.4.33", features = ["std"] }
|
||||
memoffset = "0.9.1"
|
||||
mimalloc = "0.1.52"
|
||||
mime_guess2 = { version = "2.3", default-features = false }
|
||||
mint = "0.5.9"
|
||||
@@ -114,42 +110,43 @@ objc2 = "0.6.4"
|
||||
objc2-app-kit = { 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 }
|
||||
open = "5.3"
|
||||
open = "5.4"
|
||||
parking_lot = "0.12.5"
|
||||
percent-encoding = "2.3"
|
||||
poll-promise = { version = "0.3.0", default-features = false }
|
||||
pollster = "0.4.0"
|
||||
pollster = "1.0"
|
||||
profiling = { version = "1.0", default-features = false }
|
||||
puffin = "0.20.0"
|
||||
puffin_http = "0.17.0"
|
||||
rand = "0.10.1"
|
||||
rand = "0.10.2"
|
||||
raw-window-handle = "0.6.2"
|
||||
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"
|
||||
rmp-serde = "1.3"
|
||||
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_bytes = "0.11.19"
|
||||
similar-asserts = "2.0"
|
||||
skrifa = { version = "0.42.1", default-features = false, features = [
|
||||
skrifa = { version = "0.44.0", default-features = false, features = [
|
||||
"std",
|
||||
"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"
|
||||
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"
|
||||
syntect = { version = "5.3", default-features = false }
|
||||
tempfile = "3.27"
|
||||
thiserror = "2.0"
|
||||
tokio = "1.52"
|
||||
toml = { version = "1.0", default-features = false }
|
||||
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.53"
|
||||
toml = { version = "1.1", default-features = false }
|
||||
type-map = "0.5.1"
|
||||
unicode_names2 = { version = "3.1", default-features = false }
|
||||
unicode-general-category = "1.1"
|
||||
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",
|
||||
"u8_pipeline",
|
||||
"f32_pipeline",
|
||||
@@ -159,7 +156,7 @@ wasm-bindgen-futures = "0.4.76"
|
||||
wayland-cursor = { version = "0.31.14", default-features = false }
|
||||
web-sys = "0.3.103"
|
||||
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"] }
|
||||
windows-sys = "0.61.2"
|
||||
winit = { version = "0.30.13", default-features = false }
|
||||
@@ -167,16 +164,10 @@ winit = { version = "0.30.13", default-features = false }
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
future_incompatible = { level = "warn", priority = -1 }
|
||||
nonstandard_style = { 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"
|
||||
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_lifetimes = "warn"
|
||||
|
||||
@@ -185,216 +176,128 @@ unused_qualifications = "allow"
|
||||
|
||||
[workspace.lints.rustdoc]
|
||||
all = "warn"
|
||||
missing_crate_level_docs = "warn"
|
||||
broken_intra_doc_links = "warn"
|
||||
|
||||
# See also clippy.toml
|
||||
[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 }
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
allow_attributes = "warn"
|
||||
as_ptr_cast_mut = "warn"
|
||||
await_holding_lock = "warn"
|
||||
bool_to_int_with_if = "warn"
|
||||
branches_sharing_code = "warn"
|
||||
char_lit_as_u8 = "warn"
|
||||
checked_conversions = "warn"
|
||||
clear_with_drain = "warn"
|
||||
clone_on_ref_ptr = "warn"
|
||||
cloned_instead_of_copied = "warn"
|
||||
coerce_container_to_any = "warn"
|
||||
dbg_macro = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
decimal_bitwise_operands = "warn"
|
||||
default_union_representation = "warn"
|
||||
derive_partial_eq_without_eq = "warn"
|
||||
disallowed_macros = "warn" # See clippy.toml
|
||||
disallowed_methods = "warn" # See clippy.toml
|
||||
disallowed_names = "warn" # See clippy.toml
|
||||
disallowed_script_idents = "warn" # See clippy.toml
|
||||
disallowed_types = "warn" # See clippy.toml
|
||||
doc_broken_link = "warn"
|
||||
doc_comment_double_space_linebreaks = "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_enums = "warn"
|
||||
empty_line_after_outer_attr = "warn"
|
||||
enum_glob_use = "warn"
|
||||
equatable_if_let = "warn"
|
||||
exit = "warn"
|
||||
expl_impl_clone_on_copy = "warn"
|
||||
explicit_deref_methods = "warn"
|
||||
explicit_into_iter_loop = "warn"
|
||||
explicit_iter_loop = "warn"
|
||||
fallible_impl_from = "warn"
|
||||
filter_map_next = "warn"
|
||||
flat_map_option = "warn"
|
||||
float_cmp_const = "warn"
|
||||
fn_params_excessive_bools = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
from_iter_instead_of_collect = "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"
|
||||
inconsistent_struct_constructor = "warn"
|
||||
index_refutable_slice = "warn"
|
||||
inefficient_to_string = "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_single_items = "warn"
|
||||
iter_over_hash_type = "warn"
|
||||
iter_without_into_iter = "warn"
|
||||
large_digit_groups = "warn"
|
||||
large_futures = "warn"
|
||||
large_include_file = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
large_stack_frames = "warn"
|
||||
large_types_passed_by_value = "warn"
|
||||
let_unit_value = "warn"
|
||||
linkedlist = "warn"
|
||||
literal_string_with_formatting_args = "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_flatten = "warn"
|
||||
match_bool = "warn"
|
||||
match_same_arms = "warn"
|
||||
match_wild_err_arm = "warn"
|
||||
match_wildcard_for_single_variants = "warn"
|
||||
mem_forget = "warn"
|
||||
mismatching_type_param_order = "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"
|
||||
needless_borrow = "warn"
|
||||
needless_continue = "warn"
|
||||
needless_for_each = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_pass_by_value = "warn"
|
||||
needless_raw_string_hashes = "warn"
|
||||
needless_type_cast = "warn"
|
||||
negative_feature_names = "warn"
|
||||
non_std_lazy_statics = "warn"
|
||||
non_zero_suggestions = "warn"
|
||||
nonstandard_macro_braces = "warn"
|
||||
only_used_in_recursion = "warn"
|
||||
option_as_ref_cloned = "warn"
|
||||
option_option = "warn"
|
||||
or_fun_call = "warn"
|
||||
path_buf_push_overwrite = "warn"
|
||||
pathbuf_init_then_push = "warn"
|
||||
precedence_bits = "warn"
|
||||
print_stderr = "warn"
|
||||
print_stdout = "warn"
|
||||
ptr_as_ptr = "warn"
|
||||
ptr_cast_constness = "warn"
|
||||
pub_underscore_fields = "warn"
|
||||
pub_without_shorthand = "warn"
|
||||
rc_mutex = "warn"
|
||||
readonly_write_lock = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
ref_as_ptr = "warn"
|
||||
ref_option = "warn"
|
||||
ref_option_ref = "warn"
|
||||
ref_patterns = "warn"
|
||||
rest_pat_in_fully_bound_structs = "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"
|
||||
single_char_pattern = "warn"
|
||||
single_match_else = "warn"
|
||||
single_option_map = "warn"
|
||||
str_split_at_newline = "warn"
|
||||
std_instead_of_core = "warn"
|
||||
str_to_string = "warn"
|
||||
string_add = "warn"
|
||||
string_add_assign = "warn"
|
||||
string_lit_as_bytes = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
suspicious_command_arg_space = "warn"
|
||||
suspicious_xor_used_as_pow = "warn"
|
||||
todo = "warn"
|
||||
too_long_first_doc_paragraph = "warn"
|
||||
too_many_arguments = "warn"
|
||||
trailing_empty_array = "warn"
|
||||
trait_duplication_in_bounds = "warn"
|
||||
transmute_ptr_to_ptr = "warn"
|
||||
tuple_array_conversions = "warn"
|
||||
unchecked_time_subtraction = "warn"
|
||||
undocumented_unsafe_blocks = "warn"
|
||||
unimplemented = "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_doc = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
unnecessary_semicolon = "warn"
|
||||
unnecessary_struct_initialization = "warn"
|
||||
unnecessary_trailing_comma = "warn"
|
||||
unnecessary_wraps = "warn"
|
||||
unnested_or_patterns = "warn"
|
||||
unused_async = "warn"
|
||||
unused_peekable = "warn"
|
||||
unused_rounding = "warn"
|
||||
unused_self = "warn"
|
||||
unused_trait_names = "warn"
|
||||
unwrap_used = "warn"
|
||||
use_self = "warn"
|
||||
useless_let_if_seq = "warn"
|
||||
useless_transmute = "warn"
|
||||
verbose_file_reads = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
zero_sized_map_values = "warn"
|
||||
|
||||
|
||||
# TODO(emilk): maybe enable more of these lints?
|
||||
cast_possible_wrap = "allow"
|
||||
# Pedantic lints we opt out of, with the number of hits at the time we enabled `pedantic`:
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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
|
||||
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
|
||||
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
|
||||
significant_drop_tightening = "allow" # Too many false positives
|
||||
wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports
|
||||
|
||||
@@ -66,7 +66,7 @@ ui.image(egui::include_image!("ferris.png"));
|
||||
|
||||
## Quick start
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
## 0.36.1 - 2026-08-07
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.36.0 - 2026-08-05
|
||||
Nothing new
|
||||
|
||||
## 0.35.0 - 2026-06-25
|
||||
Nothing new
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
|
||||
use crate::{Rgba, fast_round, mul_frac_round};
|
||||
|
||||
/// This format is used for space-efficient color representation (32 bits).
|
||||
///
|
||||
@@ -30,15 +30,15 @@ use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
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!
|
||||
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;
|
||||
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;
|
||||
|
||||
#[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]
|
||||
fn index_mut(&mut self, index: usize) -> &mut u8 {
|
||||
&mut self.0[index]
|
||||
@@ -131,35 +131,10 @@ impl Color32 {
|
||||
/// but for transparent colors what you get back might be slightly different (rounding errors).
|
||||
#[inline]
|
||||
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
use std::sync::OnceLock;
|
||||
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)
|
||||
}
|
||||
}
|
||||
Self::from_rgba_unmultiplied_const(r, g, b, a)
|
||||
}
|
||||
|
||||
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
|
||||
///
|
||||
/// It is slightly slower when operating on non-const data.
|
||||
/// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts.
|
||||
#[inline]
|
||||
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
|
||||
match a {
|
||||
@@ -170,9 +145,9 @@ impl Color32 {
|
||||
255 => Self::from_rgb(r, g, b),
|
||||
|
||||
a => {
|
||||
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
|
||||
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
|
||||
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
|
||||
let r = mul_frac_round(r, a);
|
||||
let g = mul_frac_round(g, a);
|
||||
let b = mul_frac_round(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;
|
||||
|
||||
/// 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;
|
||||
|
||||
#[inline]
|
||||
@@ -489,7 +464,7 @@ mod test {
|
||||
} else {
|
||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||
// 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:?}");
|
||||
}
|
||||
}
|
||||
@@ -535,4 +510,14 @@ mod test {
|
||||
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mul_frac_round_vs_old() {
|
||||
for x in (0..=255u8).step_by(4) {
|
||||
for a in (1..=255u8).step_by(4) {
|
||||
let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a));
|
||||
assert_eq!(old, mul_frac_round(x, a));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! Supports the 3, 4, 6, and 8-digit formats, according to the specification in
|
||||
//! <https://drafts.csswg.org/css-color-4/#hex-color>
|
||||
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
use core::{fmt::Display, str::FromStr};
|
||||
|
||||
use crate::Color32;
|
||||
|
||||
@@ -31,7 +31,7 @@ pub enum HexColor {
|
||||
pub enum ParseHexColorError {
|
||||
MissingHash,
|
||||
InvalidLength,
|
||||
InvalidInt(std::num::ParseIntError),
|
||||
InvalidInt(core::num::ParseIntError),
|
||||
}
|
||||
|
||||
impl FromStr for HexColor {
|
||||
@@ -45,7 +45,7 @@ impl FromStr 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 {
|
||||
Self::Hex3(color) => {
|
||||
let [r, g, b, _] = color.to_srgba_unmultiplied().map(|u| u >> 4);
|
||||
|
||||
@@ -41,7 +41,6 @@ impl Hsva {
|
||||
/// From linear RGBA with premultiplied alpha
|
||||
#[inline]
|
||||
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 r == 0.0 && b == 0.0 && a == 0.0 {
|
||||
Self::default()
|
||||
@@ -57,7 +56,6 @@ impl Hsva {
|
||||
/// From linear RGBA without premultiplied alpha
|
||||
#[inline]
|
||||
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]);
|
||||
Self { h, s, v, a }
|
||||
}
|
||||
@@ -189,7 +187,6 @@ impl From<Color32> for Hsva {
|
||||
/// All ranges in 0-1, rgb is linear.
|
||||
#[inline]
|
||||
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 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.
|
||||
#[inline]
|
||||
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 s = s.clamp(0.0, 1.0);
|
||||
|
||||
|
||||
@@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 {
|
||||
(r + 0.5) as _ // rust does a saturating cast since 1.45
|
||||
}
|
||||
|
||||
/// 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]
|
||||
pub fn test_srgba_conversion() {
|
||||
for b in 0..=255 {
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::Color32;
|
||||
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
|
||||
pub struct Rgba(pub(crate) [f32; 4]);
|
||||
|
||||
impl std::ops::Index<usize> for Rgba {
|
||||
impl core::ops::Index<usize> for Rgba {
|
||||
type Output = f32;
|
||||
|
||||
#[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]
|
||||
fn index_mut(&mut self, index: usize) -> &mut f32 {
|
||||
&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.
|
||||
#[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 {
|
||||
state.write_u8(0);
|
||||
} else if f.is_nan() {
|
||||
state.write_u8(1);
|
||||
} else {
|
||||
use std::hash::Hash as _;
|
||||
use core::hash::Hash as _;
|
||||
f.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for Rgba {
|
||||
impl core::hash::Hash for Rgba {
|
||||
#[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[1]);
|
||||
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;
|
||||
|
||||
#[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;
|
||||
|
||||
#[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;
|
||||
|
||||
#[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;
|
||||
|
||||
#[inline]
|
||||
@@ -336,7 +336,7 @@ mod test {
|
||||
} else {
|
||||
// There will be small rounding errors whenever the alpha is not 0 or 255,
|
||||
// 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:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
## 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
|
||||
### ⭐ 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)
|
||||
|
||||
@@ -28,6 +28,7 @@ workspace = true
|
||||
default = [
|
||||
"accesskit",
|
||||
"default_fonts",
|
||||
"links",
|
||||
"wayland", # Required for Linux support (including CI!)
|
||||
"web_screen_reader",
|
||||
"wgpu",
|
||||
@@ -66,7 +67,7 @@ experimental = ["egui/experimental"]
|
||||
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
|
||||
|
||||
## 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.
|
||||
##
|
||||
@@ -128,6 +129,9 @@ __screenshot = []
|
||||
## and capture screenshots. Off unless the env var is set; no-op on wasm.
|
||||
inspection = ["dep:egui_inspection", "accesskit"]
|
||||
|
||||
## Enables the `links` feature on `egui-winit`, allowing for links to open in browser.
|
||||
links = ["egui-winit/links"]
|
||||
|
||||
[dependencies]
|
||||
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
|
||||
|
||||
@@ -151,7 +155,7 @@ serde = { workspace = true, optional = true }
|
||||
# -------------------------------------------
|
||||
# native:
|
||||
[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
|
||||
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
|
||||
|
||||
@@ -167,7 +171,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe
|
||||
"egl",
|
||||
"wgl",
|
||||
] }
|
||||
home = { workspace = true, optional = true }
|
||||
|
||||
# mac:
|
||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||
@@ -212,7 +215,6 @@ image = { workspace = true, features = ["png"] } # For copying images
|
||||
js-sys.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures.workspace = true
|
||||
web-sys = { workspace = true, features = [
|
||||
"AddEventListenerOptions",
|
||||
"BinaryType",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#![warn(missing_docs)] // Let's keep `epi` well-documented.
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use std::any::Any;
|
||||
use core::any::Any;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[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"))]
|
||||
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.
|
||||
///
|
||||
@@ -73,7 +73,7 @@ pub struct CreationContext<'s> {
|
||||
/// The `get_proc_address` wrapper of underlying GL context
|
||||
#[cfg(feature = "glow")]
|
||||
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.
|
||||
///
|
||||
@@ -155,6 +155,12 @@ pub trait App {
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// 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:
|
||||
|
||||
/// Time between automatic calls to [`Self::save`]
|
||||
fn auto_save_interval(&self) -> std::time::Duration {
|
||||
std::time::Duration::from_secs(30)
|
||||
fn auto_save_interval(&self) -> core::time::Duration {
|
||||
core::time::Duration::from_secs(30)
|
||||
}
|
||||
|
||||
/// 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"))]
|
||||
impl std::fmt::Display for Renderer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Display for Renderer {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
#[cfg(feature = "glow")]
|
||||
Self::Glow => "glow".fmt(f),
|
||||
@@ -628,7 +634,7 @@ impl std::fmt::Display for Renderer {
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
|
||||
impl std::str::FromStr for Renderer {
|
||||
impl core::str::FromStr for Renderer {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, String> {
|
||||
|
||||
@@ -503,7 +503,7 @@ pub fn run_ui_native(
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// 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`].
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -519,7 +519,7 @@ pub enum Error {
|
||||
|
||||
/// An error from [`glutin`] when using [`glow`].
|
||||
#[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`].
|
||||
#[cfg(feature = "glow")]
|
||||
@@ -530,7 +530,7 @@ pub enum Error {
|
||||
Wgpu(egui_wgpu::WgpuError),
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
impl core::error::Error for Error {}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl From<winit::error::OsError> for Error {
|
||||
@@ -572,8 +572,8 @@ impl From<egui_wgpu::WgpuError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
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>`.
|
||||
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
|
||||
pub type Result<T = (), E = Error> = core::result::Result<T, E>;
|
||||
|
||||
@@ -123,7 +123,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return std::ptr::null_mut();
|
||||
return core::ptr::null_mut();
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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!
|
||||
} else {
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
WM_SETICON,
|
||||
ICON_BIG as usize,
|
||||
icon_big as isize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
WM_SETICON,
|
||||
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() {
|
||||
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
|
||||
return AppIconStatus::NotSetIgnored;
|
||||
} else {
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
WM_SETICON,
|
||||
ICON_SMALL as usize,
|
||||
icon_small as isize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
|
||||
unsafe {
|
||||
SendMessageW(
|
||||
window_handle,
|
||||
WM_SETICON,
|
||||
ICON_SMALL as usize,
|
||||
icon_small as isize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ pub fn viewport_builder(
|
||||
}
|
||||
}
|
||||
|
||||
match std::mem::take(&mut native_options.window_builder) {
|
||||
match core::mem::take(&mut native_options.window_builder) {
|
||||
Some(hook) => hook(viewport_builder),
|
||||
None => viewport_builder,
|
||||
}
|
||||
@@ -156,6 +156,11 @@ pub struct EpiIntegration {
|
||||
pub beginning: Instant,
|
||||
is_first_frame: bool,
|
||||
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,
|
||||
|
||||
/// When set, it is time to close the native window.
|
||||
@@ -215,6 +220,7 @@ impl EpiIntegration {
|
||||
Self {
|
||||
frame,
|
||||
last_auto_save: Instant::now(),
|
||||
pending_raw_input: Default::default(),
|
||||
pending_full_output: Default::default(),
|
||||
close: 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!
|
||||
///
|
||||
/// 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(
|
||||
&mut self,
|
||||
app: &mut dyn epi::App,
|
||||
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
|
||||
mut raw_input: egui::RawInput,
|
||||
is_visible: bool,
|
||||
raw_input: egui::RawInput,
|
||||
) -> 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();
|
||||
|
||||
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| {
|
||||
if let Some(viewport_ui_cb) = viewport_ui_cb {
|
||||
// Child viewport
|
||||
if is_visible {
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
}
|
||||
profiling::scope!("viewport_callback");
|
||||
viewport_ui_cb(ui);
|
||||
} else {
|
||||
{
|
||||
profiling::scope!("App::logic");
|
||||
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 {
|
||||
let canceled = full_output.viewport_output[&ViewportId::ROOT]
|
||||
.commands
|
||||
.contains(&egui::ViewportCommand::CancelClose);
|
||||
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;
|
||||
}
|
||||
self.handle_close_request(canceled);
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -321,7 +379,7 @@ impl EpiIntegration {
|
||||
|
||||
pub fn post_rendering(&mut self, window: &winit::window::Window) {
|
||||
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
|
||||
window.set_visible(true);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::cell::Cell;
|
||||
use core::cell::Cell;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
|
||||
thread_local! {
|
||||
@@ -14,7 +14,7 @@ impl EventLoopGuard {
|
||||
cell.get().is_none(),
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
||||
OS::Nix => var_os("XDG_DATA_HOME")
|
||||
.map(PathBuf::from)
|
||||
.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| {
|
||||
p.join(
|
||||
app_id
|
||||
@@ -29,7 +29,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
|
||||
.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")
|
||||
.join("Application Support")
|
||||
.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")))]
|
||||
#[expect(unsafe_code)]
|
||||
fn roaming_appdata() -> Option<PathBuf> {
|
||||
use core::ptr;
|
||||
use core::slice;
|
||||
use std::ffi::OsString;
|
||||
use std::os::windows::ffi::OsStringExt as _;
|
||||
use std::ptr;
|
||||
use std::slice;
|
||||
|
||||
use windows_sys::Win32::Foundation::S_OK;
|
||||
use windows_sys::Win32::System::Com::CoTaskMemFree;
|
||||
@@ -66,8 +66,8 @@ fn roaming_appdata() -> Option<PathBuf> {
|
||||
SHGetKnownFolderPath(
|
||||
&FOLDERID_RoamingAppData,
|
||||
KF_FLAG_DONT_VERIFY as u32,
|
||||
std::ptr::null_mut(),
|
||||
&mut path_raw,
|
||||
core::ptr::null_mut(),
|
||||
&raw mut path_raw,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
#![expect(clippy::undocumented_unsafe_blocks)]
|
||||
#![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 glutin::{
|
||||
@@ -31,16 +32,17 @@ use egui::{
|
||||
};
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
|
||||
};
|
||||
use log::warn;
|
||||
|
||||
use super::{
|
||||
epi_integration, event_loop_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:
|
||||
@@ -73,6 +75,16 @@ struct GlowWinitRunning<'app> {
|
||||
|
||||
// NOTE: one painter shared by all viewports.
|
||||
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.
|
||||
@@ -114,6 +126,9 @@ struct Viewport {
|
||||
info: ViewportInfo,
|
||||
actions_requested: Vec<egui_winit::ActionRequested>,
|
||||
|
||||
/// Any not yet applied deltas for this viewport.
|
||||
pending_delta: TexturesDelta,
|
||||
|
||||
/// The user-callback that shows the ui.
|
||||
/// None for immediate viewports.
|
||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||
@@ -125,6 +140,34 @@ struct Viewport {
|
||||
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> {
|
||||
@@ -296,7 +339,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
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");
|
||||
|
||||
crate::maybe_attach_inspection_plugin(&integration.egui_ctx, Some(self.app_name.clone()));
|
||||
@@ -353,6 +396,7 @@ impl<'app> GlowWinitApp<'app> {
|
||||
app,
|
||||
glutin,
|
||||
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 egui_ctx = glutin.egui_ctx.clone();
|
||||
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 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);
|
||||
|
||||
self.integration.pre_update();
|
||||
@@ -588,9 +632,58 @@ impl GlowWinitRunning<'_> {
|
||||
.map(|(id, viewport)| (*id, viewport.info.clone()))
|
||||
.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
|
||||
// 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.
|
||||
@@ -639,12 +732,9 @@ impl GlowWinitRunning<'_> {
|
||||
// The update function, which could call immediate viewports,
|
||||
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
|
||||
|
||||
let full_output = self.integration.update(
|
||||
self.app.as_mut(),
|
||||
viewport_ui_cb.as_deref(),
|
||||
raw_input,
|
||||
run_ui,
|
||||
);
|
||||
let full_output =
|
||||
self.integration
|
||||
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -653,6 +743,7 @@ impl GlowWinitRunning<'_> {
|
||||
app,
|
||||
glutin,
|
||||
painter,
|
||||
pending_deltas,
|
||||
..
|
||||
} = self;
|
||||
|
||||
@@ -666,6 +757,7 @@ impl GlowWinitRunning<'_> {
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
pending_deltas.append(textures_delta);
|
||||
|
||||
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);
|
||||
|
||||
// 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 {
|
||||
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();
|
||||
|
||||
if !clear_before_update {
|
||||
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(..) {
|
||||
@@ -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);
|
||||
|
||||
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));
|
||||
|
||||
if 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));
|
||||
}
|
||||
sleep_if_invisible_or_minimized(Some(&window));
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::CloseRequested)
|
||||
@@ -1120,6 +1198,7 @@ impl GlutinWindowContext {
|
||||
deferred_commands: vec![],
|
||||
info: viewport_info,
|
||||
actions_requested: Default::default(),
|
||||
pending_delta: Default::default(),
|
||||
viewport_ui_cb: None,
|
||||
gl_surface: None,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1362,7 +1441,7 @@ impl GlutinWindowContext {
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
@@ -1377,25 +1456,18 @@ impl GlutinWindowContext {
|
||||
viewport_ui_cb,
|
||||
);
|
||||
|
||||
if let Some(window) = &viewport.window {
|
||||
let old_inner_size = window.inner_size();
|
||||
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
viewport.process_commands(egui_ctx, commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size {
|
||||
self.resize(viewport_id, new_inner_size);
|
||||
}
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux")
|
||||
&& let Some(window) = &viewport.window
|
||||
&& let Some(old_inner_size) = old_inner_size
|
||||
{
|
||||
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![],
|
||||
info: Default::default(),
|
||||
actions_requested: Default::default(),
|
||||
pending_delta: Default::default(),
|
||||
viewport_ui_cb,
|
||||
window: None,
|
||||
egui_winit: None,
|
||||
@@ -1584,8 +1657,10 @@ fn render_immediate_viewport(
|
||||
} = &mut *glutin;
|
||||
|
||||
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
||||
warn!("Viewport disappeared unexpectedly!");
|
||||
return;
|
||||
};
|
||||
viewport.pending_delta.append(textures_delta);
|
||||
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
|
||||
@@ -1621,7 +1696,7 @@ fn render_immediate_viewport(
|
||||
screen_size_in_pixels,
|
||||
pixels_per_point,
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
&mut viewport.pending_delta,
|
||||
);
|
||||
|
||||
{
|
||||
@@ -1645,7 +1720,7 @@ fn save_screenshot_and_exit(
|
||||
screen_size_in_pixels: [u32; 2],
|
||||
) {
|
||||
assert!(
|
||||
path.ends_with(".png"),
|
||||
egui::load::has_extension(path, "png"),
|
||||
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
|
||||
);
|
||||
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::time::{Duration, Instant};
|
||||
use core::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use winit::{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ fn with_event_loop<R>(
|
||||
mut native_options: epi::NativeOptions,
|
||||
f: impl FnOnce(&mut EventLoop<UserEvent>, epi::NativeOptions) -> 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| {
|
||||
// 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);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
} 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();
|
||||
if let Some(next_repaint_time) = next_repaint_time {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
|
||||
}
|
||||
event_loop.set_control_flow(match 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(
|
||||
&mut self,
|
||||
event_loop: &mut EventLoop<UserEvent>,
|
||||
timeout: Option<std::time::Duration>,
|
||||
timeout: Option<core::time::Duration>,
|
||||
) -> EframePumpStatus {
|
||||
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//! There is a bunch of improvements we could do,
|
||||
//! 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 parking_lot::Mutex;
|
||||
@@ -17,19 +18,20 @@ use winit::{
|
||||
|
||||
use ahash::HashMap;
|
||||
use egui::{
|
||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
|
||||
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, TexturesDelta,
|
||||
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
|
||||
ViewportOutput,
|
||||
};
|
||||
#[cfg(feature = "accesskit")]
|
||||
use egui_winit::accesskit_winit;
|
||||
use log::warn;
|
||||
use winit_integration::UserEvent;
|
||||
|
||||
use crate::{
|
||||
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
|
||||
native::{
|
||||
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.
|
||||
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.\
|
||||
@@ -91,6 +102,9 @@ pub struct Viewport {
|
||||
info: ViewportInfo,
|
||||
actions_requested: Vec<ActionRequested>,
|
||||
|
||||
/// Any not yet applied deltas for this viewport.
|
||||
pending_delta: TexturesDelta,
|
||||
|
||||
/// `None` for sync viewports.
|
||||
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
|
||||
|
||||
@@ -102,6 +116,13 @@ pub struct Viewport {
|
||||
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> {
|
||||
@@ -289,7 +310,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
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");
|
||||
|
||||
crate::maybe_attach_inspection_plugin(&egui_ctx, Some(self.app_name.clone()));
|
||||
@@ -328,6 +349,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
viewport_ui_cb: None,
|
||||
window: Some(window),
|
||||
egui_winit: Some(egui_winit),
|
||||
pending_delta: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -358,6 +380,7 @@ impl<'app> WgpuWinitApp<'app> {
|
||||
integration,
|
||||
app,
|
||||
shared,
|
||||
pending_deltas: Default::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -596,12 +619,13 @@ impl WgpuWinitRunning<'_> {
|
||||
app,
|
||||
integration,
|
||||
shared,
|
||||
pending_deltas,
|
||||
} = self;
|
||||
|
||||
let mut frame_timer = crate::stopwatch::Stopwatch::new();
|
||||
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");
|
||||
let mut shared_lock = shared.borrow_mut();
|
||||
|
||||
@@ -657,7 +681,7 @@ impl WgpuWinitRunning<'_> {
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -669,15 +693,67 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
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,
|
||||
// so make sure we hold no locks here!
|
||||
let full_output =
|
||||
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, run_ui);
|
||||
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
|
||||
@@ -699,6 +775,8 @@ impl WgpuWinitRunning<'_> {
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
|
||||
pending_deltas.append(textures_delta);
|
||||
|
||||
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
|
||||
|
||||
let Some(viewport) = viewports.get_mut(&viewport_id) else {
|
||||
@@ -735,7 +813,7 @@ impl WgpuWinitRunning<'_> {
|
||||
pixels_per_point,
|
||||
app.clear_color(&egui_ctx.global_style().visuals),
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
pending_deltas,
|
||||
screenshot_commands,
|
||||
window,
|
||||
);
|
||||
@@ -796,16 +874,7 @@ impl WgpuWinitRunning<'_> {
|
||||
|
||||
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
|
||||
|
||||
if let Some(window) = window
|
||||
&& 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));
|
||||
}
|
||||
sleep_if_invisible_or_minimized(window.map(|window| window.as_ref()));
|
||||
|
||||
if integration.should_close() {
|
||||
Ok(EventResult::CloseRequested)
|
||||
@@ -960,6 +1029,25 @@ impl WgpuWinitRunning<'_> {
|
||||
}
|
||||
|
||||
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.
|
||||
fn initialize_window(
|
||||
&mut self,
|
||||
@@ -1125,8 +1213,11 @@ fn render_immediate_viewport(
|
||||
} = &mut *shared_mut;
|
||||
|
||||
let Some(viewport) = viewports.get_mut(&ids.this) else {
|
||||
warn!("Viewport disappeared unexpectedly!");
|
||||
return;
|
||||
};
|
||||
viewport.pending_delta.append(textures_delta);
|
||||
|
||||
viewport.info.events.clear(); // they should have been processed
|
||||
let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else {
|
||||
return;
|
||||
@@ -1149,7 +1240,7 @@ fn render_immediate_viewport(
|
||||
pixels_per_point,
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
&clipped_primitives,
|
||||
&textures_delta,
|
||||
&mut viewport.pending_delta,
|
||||
vec![],
|
||||
window,
|
||||
);
|
||||
@@ -1194,7 +1285,7 @@ fn handle_viewport_output(
|
||||
class,
|
||||
builder,
|
||||
viewport_ui_cb,
|
||||
mut commands,
|
||||
commands,
|
||||
repaint_delay: _, // ignored - we listened to the repaint callback instead
|
||||
},
|
||||
) in viewport_output.clone()
|
||||
@@ -1204,30 +1295,23 @@ fn handle_viewport_output(
|
||||
let viewport =
|
||||
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
|
||||
|
||||
if let Some(window) = viewport.window.as_ref() {
|
||||
let old_inner_size = window.inner_size();
|
||||
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
|
||||
|
||||
viewport.deferred_commands.append(&mut commands);
|
||||
viewport.process_commands(egui_ctx, commands);
|
||||
|
||||
egui_winit::process_viewport_commands(
|
||||
egui_ctx,
|
||||
&mut viewport.info,
|
||||
std::mem::take(&mut viewport.deferred_commands),
|
||||
window,
|
||||
&mut viewport.actions_requested,
|
||||
);
|
||||
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux") {
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(new_inner_size.width),
|
||||
NonZeroU32::new(new_inner_size.height),
|
||||
)
|
||||
{
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
// For Wayland : https://github.com/emilk/egui/issues/4196
|
||||
if cfg!(target_os = "linux")
|
||||
&& let Some(window) = viewport.window.as_ref()
|
||||
&& let Some(old_inner_size) = old_inner_size
|
||||
{
|
||||
let new_inner_size = window.inner_size();
|
||||
if new_inner_size != old_inner_size
|
||||
&& let (Some(width), Some(height)) = (
|
||||
NonZeroU32::new(new_inner_size.width),
|
||||
NonZeroU32::new(new_inner_size.height),
|
||||
)
|
||||
{
|
||||
painter.on_window_resized(viewport_id, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1268,6 +1352,7 @@ fn initialize_or_update_viewport<'a>(
|
||||
viewport_ui_cb,
|
||||
window: None,
|
||||
egui_winit: None,
|
||||
pending_delta: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,18 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
|
||||
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
|
||||
profiling::function_scope!();
|
||||
|
||||
@@ -280,52 +280,71 @@ impl AppRunner {
|
||||
.and_then(|v| v.visible())
|
||||
.unwrap_or(true);
|
||||
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.logic(ui.ctx(), &mut self.frame);
|
||||
|
||||
if is_visible {
|
||||
if is_visible {
|
||||
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
|
||||
self.app.logic(ui.ctx(), &mut self.frame);
|
||||
self.app.ui(ui, &mut self.frame);
|
||||
}
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
});
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point,
|
||||
viewport_output,
|
||||
} = full_output;
|
||||
|
||||
if viewport_output.len() > 1 {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
if viewport_output.len() > 1 {
|
||||
log::warn!("Multiple viewports not yet supported on the web");
|
||||
}
|
||||
}
|
||||
self.handle_viewport_commands(
|
||||
viewport_output
|
||||
.into_values()
|
||||
.flat_map(|viewport_output| viewport_output.commands),
|
||||
);
|
||||
|
||||
self.handle_platform_output(platform_output);
|
||||
if is_visible || !textures_delta.is_empty() {
|
||||
self.handle_platform_output(platform_output);
|
||||
self.textures_delta.append(textures_delta);
|
||||
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`].
|
||||
pub fn paint(&mut self) {
|
||||
let textures_delta = std::mem::take(&mut self.textures_delta);
|
||||
let clipped_primitives = std::mem::take(&mut self.clipped_primitives);
|
||||
let clipped_primitives = core::mem::take(&mut self.clipped_primitives);
|
||||
|
||||
if let Some(clipped_primitives) = clipped_primitives {
|
||||
let mut screenshot_commands = vec![];
|
||||
@@ -347,7 +366,7 @@ impl AppRunner {
|
||||
self.app.clear_color(&self.egui_ctx.global_style().visuals),
|
||||
&clipped_primitives,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
&textures_delta,
|
||||
&mut self.textures_delta,
|
||||
screenshot_commands,
|
||||
) {
|
||||
log::error!("Failed to paint: {}", super::string_from_js_value(&err));
|
||||
@@ -395,7 +414,10 @@ impl AppRunner {
|
||||
|
||||
if self.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.
|
||||
self.text_agent.focus();
|
||||
} else {
|
||||
@@ -407,7 +429,7 @@ impl AppRunner {
|
||||
|
||||
if let Err(err) = self
|
||||
.text_agent
|
||||
.move_to(ime, self.canvas(), self.egui_ctx.zoom_factor())
|
||||
.update(ime, self.canvas(), self.egui_ctx.zoom_factor())
|
||||
{
|
||||
log::error!(
|
||||
"failed to update text agent position: {}",
|
||||
|
||||
45
crates/eframe/src/web/dropped_file.rs
Normal file
45
crates/eframe/src/web/dropped_file.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use core::{future::Future, pin::Pin};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WebFile {
|
||||
file: web_sys::File,
|
||||
// We store a `PathBuf` here so that we can hand out `Path`s
|
||||
// without allocating each time.
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl From<web_sys::File> for WebFile {
|
||||
fn from(file: web_sys::File) -> Self {
|
||||
let path = file.name().into();
|
||||
Self { file, path }
|
||||
}
|
||||
}
|
||||
|
||||
impl egui::DroppedFile for WebFile {
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>> {
|
||||
let file = self.file.clone();
|
||||
Box::pin(async move {
|
||||
if file.size() > f64::from(u32::MAX) {
|
||||
return Err(format!(
|
||||
"File is too large: browser file reads are limited to {} bytes",
|
||||
u32::MAX
|
||||
));
|
||||
}
|
||||
|
||||
let array_buffer = file
|
||||
.array_buffer()
|
||||
.await
|
||||
.map_err(|err| crate::web::string_from_js_value(&err))?;
|
||||
Ok(js_sys::Uint8Array::new(&array_buffer).to_vec())
|
||||
})
|
||||
}
|
||||
|
||||
fn web_file(&self) -> Option<&web_sys::File> {
|
||||
Some(&self.file)
|
||||
}
|
||||
}
|
||||
@@ -190,11 +190,6 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
runner.input.set_modifiers(modifiers);
|
||||
|
||||
@@ -978,62 +973,25 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
|
||||
event.prevent_default();
|
||||
})?;
|
||||
|
||||
runner_ref.add_event_listener(target, "drop", {
|
||||
let runner_ref = runner_ref.clone();
|
||||
runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| {
|
||||
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(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();
|
||||
if let Some(files) = data_transfer.files() {
|
||||
for i in 0..files.length() {
|
||||
if let Some(file) = files.get(i) {
|
||||
log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size());
|
||||
|
||||
if let Some(files) = data_transfer.files() {
|
||||
for i in 0..files.length() {
|
||||
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);
|
||||
}
|
||||
runner.input.raw.dropped_files.push(std::sync::Arc::new(
|
||||
super::dropped_file::WebFile::from(file),
|
||||
));
|
||||
}
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ pub fn primary_touch_pos(
|
||||
event: &web_sys::TouchEvent,
|
||||
) -> Option<(egui::Pos2, web_sys::Touch)> {
|
||||
// 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.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)),
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
mod app_runner;
|
||||
mod backend;
|
||||
mod dropped_file;
|
||||
mod events;
|
||||
mod input;
|
||||
mod panic_handler;
|
||||
@@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) {
|
||||
return;
|
||||
}
|
||||
let promise = window.navigator().clipboard().write_text(s);
|
||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
||||
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));
|
||||
}
|
||||
};
|
||||
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 promise = window.navigator().clipboard().write(&items);
|
||||
let future = wasm_bindgen_futures::JsFuture::from(promise);
|
||||
let future = async move {
|
||||
if let Err(err) = future.await {
|
||||
if let Err(err) = promise.await {
|
||||
log::error!(
|
||||
"Copy/cut image action failed: {}",
|
||||
string_from_js_value(&err)
|
||||
);
|
||||
}
|
||||
};
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
js_sys::futures::spawn_local(future);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
//! The text agent is a hidden `<input>` element used to capture
|
||||
//! IME and mobile keyboard input events.
|
||||
|
||||
use std::cell::Cell;
|
||||
use core::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::Document;
|
||||
|
||||
use super::{AppRunner, WebRunner};
|
||||
|
||||
pub struct TextAgent {
|
||||
input: web_sys::HtmlInputElement,
|
||||
prev_ime_output: Cell<Option<egui::output::IMEOutput>>,
|
||||
input_state: Rc<RefCell<InputState>>,
|
||||
}
|
||||
|
||||
impl TextAgent {
|
||||
@@ -19,7 +19,8 @@ impl TextAgent {
|
||||
runner_ref: &WebRunner,
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
) -> 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
|
||||
let input = document
|
||||
@@ -27,11 +28,11 @@ impl TextAgent {
|
||||
.dyn_into::<web_sys::HtmlInputElement>()?;
|
||||
input.set_type("text");
|
||||
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
|
||||
// of the page into view.
|
||||
let canvas_rect = super::canvas_content_rect(canvas);
|
||||
let style = input.style();
|
||||
style.set_property("background-color", "transparent")?;
|
||||
style.set_property("border", "none")?;
|
||||
@@ -40,21 +41,22 @@ impl TextAgent {
|
||||
style.set_property("height", "1px")?;
|
||||
style.set_property("caret-color", "transparent")?;
|
||||
style.set_property("position", "absolute")?;
|
||||
style.set_property("top", &format!("{}px", canvas_rect.min.y))?;
|
||||
style.set_property("left", &format!("{}px", canvas_rect.min.x))?;
|
||||
style.set_property("top", &format!("{}px", canvas.offset_top()))?;
|
||||
style.set_property("left", &format!("{}px", canvas.offset_left()))?;
|
||||
// Prevent auto-zoom on mobile browsers (requires at least 16px).
|
||||
style.set_property("font-size", "16px")?;
|
||||
|
||||
let root = canvas.get_root_node();
|
||||
if root.has_type::<Document>() {
|
||||
// root object is a document, append to its body
|
||||
root.dyn_into::<Document>()?
|
||||
.body()
|
||||
.unwrap()
|
||||
.append_child(&input)?;
|
||||
} else {
|
||||
// append input into root directly
|
||||
root.append_child(&input)?;
|
||||
// Insert the input as a sibling of the canvas, so that its
|
||||
// `position: absolute` resolves against the same containing block
|
||||
// as the canvas' `offset_top`/`offset_left`.
|
||||
// This anchors the input to the canvas regardless of how the page
|
||||
// is scrolled or how the canvas is embedded, and also works when
|
||||
// the canvas is inside a shadow DOM.
|
||||
if let Some(parent) = canvas.parent_node() {
|
||||
parent.insert_before(&input, canvas.next_sibling().as_ref())?;
|
||||
} else if let Some(body) = document.body() {
|
||||
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.
|
||||
@@ -66,152 +68,67 @@ impl TextAgent {
|
||||
|
||||
// attach event listeners
|
||||
|
||||
let on_input = {
|
||||
let input = input.clone();
|
||||
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
|
||||
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 = {
|
||||
runner_ref.add_event_listener(
|
||||
&input,
|
||||
"compositionstart",
|
||||
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
||||
// 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_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);
|
||||
}
|
||||
};
|
||||
|
||||
let on_composition_end = {
|
||||
let input = input.clone();
|
||||
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
||||
let Some(text) = event.data() else { return };
|
||||
input.set_value("");
|
||||
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, "compositionend", {
|
||||
let input_state = Rc::clone(&input_state);
|
||||
move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| {
|
||||
input_state
|
||||
.borrow_mut()
|
||||
.handle_composition_end_event(runner);
|
||||
}
|
||||
};
|
||||
})?;
|
||||
|
||||
runner_ref.add_event_listener(&input, "input", on_input)?;
|
||||
runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?;
|
||||
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
|
||||
runner_ref.add_event_listener(&input, "keydown", {
|
||||
let input_state = Rc::clone(&input_state);
|
||||
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,
|
||||
// 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(),
|
||||
})
|
||||
Ok(Self { input, input_state })
|
||||
}
|
||||
|
||||
pub fn move_to(
|
||||
pub fn update(
|
||||
&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.prev_ime_output.get() == ime {
|
||||
return Ok(());
|
||||
}
|
||||
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(())
|
||||
self.input_state
|
||||
.borrow_mut()
|
||||
.update(ime, canvas, zoom_factor)
|
||||
}
|
||||
|
||||
pub fn set_focus(&self, on: bool) {
|
||||
@@ -248,6 +165,11 @@ impl TextAgent {
|
||||
if let Err(err) = self.input.blur() {
|
||||
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.
|
||||
fn is_mobile_safari() -> bool {
|
||||
(|| {
|
||||
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
|
||||
let is_ios = user_agent.contains("iPhone")
|
||||
|| user_agent.contains("iPad")
|
||||
|| user_agent.contains("iPod");
|
||||
let is_safari = user_agent.contains("Safari");
|
||||
Some(is_ios && is_safari)
|
||||
})()
|
||||
.unwrap_or(false)
|
||||
struct InputState {
|
||||
input: web_sys::HtmlInputElement,
|
||||
last_text: String,
|
||||
ime_output: Option<egui::output::IMEOutput>,
|
||||
keydown_special_case: KeydownSpecialCase,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum KeydownSpecialCase {
|
||||
None,
|
||||
|
||||
/// On Android Gboard 14.7.09, when suggestions remain visible while typing
|
||||
/// letters without IME composition (e.g., Latin or Cyrillic), pressing
|
||||
/// Backspace produces key code 229 instead of the expected Backspace key
|
||||
/// code.
|
||||
/// Without the workaround, users have to press Backspace twice before text
|
||||
/// starts being deleted.
|
||||
///
|
||||
/// This workaround is also required for Android Gboard corrections and
|
||||
/// completions (e.g., `tex|` -> `Texas`) to work correctly. In these
|
||||
/// cases, a `deleteContentBackward` input event fires first (e.g., to
|
||||
/// delete `tex`), followed by an `insertText` input event (e.g., to insert
|
||||
/// `Texas`).
|
||||
///
|
||||
/// Since it is difficult to distinguish between a Backspace press and a
|
||||
/// correction or completion (e.g., when the state is `t|`, it is unclear
|
||||
/// whether the user wants to delete `t` or replace it with `Texas`), we
|
||||
/// send a `DeleteSurrounding` IME event in all cases instead of
|
||||
/// synthetically generating Backspace press and release events.
|
||||
AndroidKeycode229,
|
||||
|
||||
/// iOS (18.6)'s built-in Korean keyboard uses `deleteContentBackward` to
|
||||
/// compose Hangul characters. In these cases, the key code is 0.
|
||||
IosKeycode0,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
fn new(input: web_sys::HtmlInputElement) -> Self {
|
||||
Self {
|
||||
input,
|
||||
last_text: String::new(),
|
||||
ime_output: None,
|
||||
keydown_special_case: KeydownSpecialCase::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
ime: Option<egui::output::IMEOutput>,
|
||||
canvas: &web_sys::HtmlCanvasElement,
|
||||
zoom_factor: f32,
|
||||
) -> Result<(), JsValue> {
|
||||
// Don't move the text agent unless the position actually changed:
|
||||
if self.ime_output == ime {
|
||||
return Ok(());
|
||||
}
|
||||
self.ime_output = ime;
|
||||
|
||||
let Some(ime) = ime else { return Ok(()) };
|
||||
|
||||
// NOTE: we don't set the input's `type` to `password` based on
|
||||
// `ime.purpose`, because that would confuse some password managers.
|
||||
// For example, Chrome's password manager will always think the last
|
||||
// letter typed in the password field is the password.
|
||||
|
||||
let style = self.input.style();
|
||||
let native_ppp = super::native_pixels_per_point();
|
||||
|
||||
// The input is a sibling of the canvas (see `attach`), so we position
|
||||
// it relative to the same containing block using the canvas offset.
|
||||
// Unlike `get_bounding_client_rect`, the offset is unaffected by page
|
||||
// scrolling, and doesn't flap when the virtual keyboard is shown on
|
||||
// mobile Safari.
|
||||
|
||||
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
|
||||
let logical_canvas_width = canvas.width() as f32 / native_ppp;
|
||||
let visible_x = ime.cursor_rect.center().x * zoom_factor;
|
||||
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
|
||||
|
||||
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
|
||||
let logical_canvas_height = canvas.height() as f32 / native_ppp;
|
||||
let visible_y = ime.cursor_rect.center().y * zoom_factor;
|
||||
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
|
||||
|
||||
// This is where the IME input will point to:
|
||||
style.set_property(
|
||||
"left",
|
||||
&format!("{}px", canvas.offset_left() as f32 + clamped_x),
|
||||
)?;
|
||||
style.set_property(
|
||||
"top",
|
||||
&format!("{}px", canvas.offset_top() as f32 + clamped_y),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.input.set_value("");
|
||||
self.last_text.clear();
|
||||
}
|
||||
|
||||
fn handle_input_event(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
|
||||
if self
|
||||
.ime_output
|
||||
.as_ref()
|
||||
.is_some_and(|ime| ime.purpose == egui::IMEPurpose::Password)
|
||||
{
|
||||
self.handle_input_event_password(event, runner);
|
||||
return;
|
||||
}
|
||||
|
||||
let input_type = event.input_type();
|
||||
|
||||
if !event.is_composing()
|
||||
&& input_type != "insertText"
|
||||
// iOS uses this for corrections and completions (e.g., `tex|` ->
|
||||
// `Texas`).
|
||||
&& input_type != "insertReplacementText"
|
||||
&& (matches!(self.keydown_special_case, KeydownSpecialCase::None)
|
||||
|| input_type != "deleteContentBackward")
|
||||
{
|
||||
self.clear();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.input.value();
|
||||
|
||||
let prefix_len = longest_common_prefix_length(&text, &self.last_text);
|
||||
let last_text_len = self.last_text.chars().count();
|
||||
if prefix_len < last_text_len {
|
||||
let out_event = egui::Event::Ime(egui::ImeEvent::DeleteSurrounding {
|
||||
before_chars: last_text_len - prefix_len,
|
||||
after_chars: 0,
|
||||
});
|
||||
runner.input.raw.events.push(out_event);
|
||||
}
|
||||
|
||||
let preedit_text: String = text.chars().skip(prefix_len).collect();
|
||||
let out_event = if event.is_composing() {
|
||||
// We handle the composition update here instead of in a
|
||||
// `compositionupdate` event because the selection range
|
||||
// has not yet been updated when `compositionupdate` fires.
|
||||
let active_range_chars = self.active_range_chars(&text, prefix_len);
|
||||
egui::Event::Ime(egui::ImeEvent::Preedit {
|
||||
text: preedit_text,
|
||||
active_range_chars,
|
||||
})
|
||||
} else {
|
||||
egui::Event::Text(preedit_text)
|
||||
};
|
||||
runner.input.raw.events.push(out_event);
|
||||
|
||||
if event.is_composing() {
|
||||
self.last_text = text.chars().take(prefix_len).collect();
|
||||
} else {
|
||||
self.last_text = text;
|
||||
}
|
||||
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
||||
fn handle_input_event_password(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
|
||||
let input_type = event.input_type();
|
||||
|
||||
if input_type != "insertText" {
|
||||
return;
|
||||
}
|
||||
|
||||
let text = self.input.value();
|
||||
|
||||
runner.input.raw.events.push(egui::Event::Text(text));
|
||||
self.clear();
|
||||
}
|
||||
|
||||
/// Compute the active range (cursor or conversion segment) within the
|
||||
/// preedit text, based on the selection in the input element.
|
||||
///
|
||||
/// `text` is the full `input.value()`, and `prefix_len_chars` is the
|
||||
/// number of chars at the start of `text` that are committed (not part
|
||||
/// of the preedit). `selectionStart`/`selectionEnd` are UTF-16 offsets
|
||||
/// within the full `input.value()`, so they are adjusted to be relative
|
||||
/// to the preedit text.
|
||||
fn active_range_chars(
|
||||
&self,
|
||||
text: &str,
|
||||
prefix_len_chars: usize,
|
||||
) -> Option<core::ops::Range<usize>> {
|
||||
let selection_start = self.input.selection_start().unwrap_or(None)? as usize;
|
||||
let selection_end = self.input.selection_end().unwrap_or(None)? as usize;
|
||||
|
||||
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
|
||||
if selection_start > text_utf16.len() || selection_end > text_utf16.len() {
|
||||
// This can occur on Android Chrome. see discussion in:
|
||||
// <https://github.com/emilk/egui/pull/8045>.
|
||||
return None;
|
||||
}
|
||||
|
||||
let text_before_selection = String::from_utf16_lossy(&text_utf16[..selection_start]);
|
||||
let text_in_selection =
|
||||
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
|
||||
let count_before_selection = text_before_selection.chars().count();
|
||||
let count_in_selection = text_in_selection.chars().count();
|
||||
|
||||
// Adjust for the committed prefix to get the range within the preedit text.
|
||||
let start = count_before_selection.saturating_sub(prefix_len_chars);
|
||||
let end = start + count_in_selection;
|
||||
Some(start..end)
|
||||
}
|
||||
|
||||
fn handle_composition_end_event(&mut self, runner: &mut AppRunner) {
|
||||
let text = self.input.value();
|
||||
|
||||
let commit_text = {
|
||||
let prefix_len = self.last_text.chars().count();
|
||||
text.chars().skip(prefix_len).collect::<String>()
|
||||
};
|
||||
let out_event = egui::Event::Ime(egui::ImeEvent::Commit(commit_text));
|
||||
runner.input.raw.events.push(out_event);
|
||||
|
||||
self.last_text = text;
|
||||
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
||||
/// ## Returns
|
||||
/// Whether the event is consumed. If `true`, the caller should not do
|
||||
/// further processing for this event.
|
||||
fn handle_keydown_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
|
||||
// Platform-sniffing methods are unreliable, so they are not used as
|
||||
// guards here.
|
||||
let special_case = match event.key_code() {
|
||||
229 => KeydownSpecialCase::AndroidKeycode229,
|
||||
0 => KeydownSpecialCase::IosKeycode0,
|
||||
_ => KeydownSpecialCase::None,
|
||||
};
|
||||
input_state.borrow_mut().keydown_special_case = special_case;
|
||||
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
if event.is_composing() || !matches!(special_case, KeydownSpecialCase::None) {
|
||||
true
|
||||
} else {
|
||||
if event.key().chars().count() > 1
|
||||
|| event.ctrl_key()
|
||||
|| event.alt_key()
|
||||
|| event.meta_key()
|
||||
{
|
||||
input_state.borrow_mut().clear();
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Returns
|
||||
/// Whether the event is consumed. If `true`, the caller should not do
|
||||
/// further processing for this event.
|
||||
fn handle_keyup_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
|
||||
input_state.borrow_mut().keydown_special_case = KeydownSpecialCase::None;
|
||||
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
event.is_composing() || event.key_code() == 229
|
||||
}
|
||||
}
|
||||
|
||||
fn longest_common_prefix_length(a: &str, b: &str) -> usize {
|
||||
core::iter::zip(a.chars(), b.chars())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count()
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) trait WebPainter {
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
textures_delta: &mut egui::TexturesDelta,
|
||||
capture: Vec<UserData>,
|
||||
) -> Result<(), JsValue>;
|
||||
|
||||
|
||||
@@ -61,13 +61,16 @@ impl WebPainter for WebPainterGlow {
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
textures_delta: &mut egui::TexturesDelta,
|
||||
capture: Vec<UserData>,
|
||||
) -> Result<(), JsValue> {
|
||||
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
|
||||
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
self.painter.set_texture(*id, image_delta);
|
||||
#[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 {
|
||||
self.painter.set_texture(id, &image_delta);
|
||||
}
|
||||
}
|
||||
|
||||
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
|
||||
@@ -79,7 +82,8 @@ impl WebPainter for WebPainterGlow {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu {
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[egui::ClippedPrimitive],
|
||||
pixels_per_point: f32,
|
||||
textures_delta: &egui::TexturesDelta,
|
||||
textures_delta: &mut egui::TexturesDelta,
|
||||
capture_data: Vec<UserData>,
|
||||
) -> Result<(), JsValue> {
|
||||
let capture = !capture_data.is_empty();
|
||||
@@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu {
|
||||
|
||||
let user_cmd_bufs = {
|
||||
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,
|
||||
);
|
||||
#[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(
|
||||
@@ -365,7 +368,7 @@ impl WebPainter for WebPainterWgpu {
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
render_state
|
||||
.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(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.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||
for id in textures_delta.free.drain() {
|
||||
renderer.free_texture(&id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use core::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -107,7 +108,7 @@ impl WebRunner {
|
||||
|
||||
fn unsubscribe_from_all_events(&self) {
|
||||
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() {
|
||||
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.
|
||||
/// 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() {
|
||||
// Unsubscribe from all events so that we don't get any more callbacks
|
||||
// that will try to access the poisoned runner.
|
||||
@@ -147,7 +148,7 @@ impl WebRunner {
|
||||
None
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -158,9 +159,9 @@ impl WebRunner {
|
||||
/// and return `None` if this runner has panicked.
|
||||
pub fn app_mut<ConcreteApp: 'static + App>(
|
||||
&self,
|
||||
) -> Option<std::cell::RefMut<'_, ConcreteApp>> {
|
||||
) -> Option<core::cell::RefMut<'_, ConcreteApp>> {
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
## 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
|
||||
* 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)
|
||||
|
||||
@@ -255,7 +255,7 @@ struct BufferPadding {
|
||||
|
||||
impl BufferPadding {
|
||||
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 padded_bytes_per_row =
|
||||
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
|
||||
|
||||
@@ -98,20 +98,31 @@ fn vs_main(
|
||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||
@group(1) @binding(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> {
|
||||
if r_locals.predictable_texture_filtering == 0 {
|
||||
// Hardware filtering: fast, but varies across GPUs and drivers.
|
||||
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
} 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_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_fract = fract(pixel_coord);
|
||||
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
||||
|
||||
// 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 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);
|
||||
|
||||
@@ -115,6 +115,9 @@ pub struct RenderState {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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.
|
||||
pub device: wgpu::Device,
|
||||
|
||||
@@ -218,7 +221,7 @@ impl RenderState {
|
||||
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 {
|
||||
instance_descriptor: _,
|
||||
display_handle: _,
|
||||
@@ -253,14 +256,14 @@ impl RenderState {
|
||||
.await?
|
||||
};
|
||||
|
||||
(adapter, device, queue)
|
||||
(instance.clone(), adapter, device, queue)
|
||||
}
|
||||
WgpuSetup::Existing(WgpuSetupExisting {
|
||||
instance: _,
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
}) => (adapter, device, queue),
|
||||
}) => (instance, adapter, device, queue),
|
||||
};
|
||||
|
||||
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.
|
||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
Ok(Self {
|
||||
instance,
|
||||
adapter,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
available_adapters,
|
||||
@@ -354,8 +358,8 @@ fn wgpu_config_impl_send_sync() {
|
||||
assert_send_sync::<WgpuConfiguration>();
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuConfiguration {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for WgpuConfiguration {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
surface,
|
||||
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: "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:?}");
|
||||
|
||||
|
||||
@@ -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 bytemuck::Zeroable as _;
|
||||
@@ -244,6 +245,12 @@ pub struct Renderer {
|
||||
uniform_bind_group: wgpu::BindGroup,
|
||||
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 +
|
||||
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
|
||||
/// sampler.
|
||||
@@ -299,7 +306,9 @@ impl Renderer {
|
||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
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,
|
||||
},
|
||||
count: None,
|
||||
@@ -344,10 +353,28 @@ impl Renderer {
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
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 {
|
||||
label: Some("egui_pipeline_layout"),
|
||||
bind_group_layouts: &[
|
||||
@@ -434,9 +461,9 @@ impl Renderer {
|
||||
};
|
||||
|
||||
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 =
|
||||
(std::mem::size_of::<u32>() * 1024 * 3) as _;
|
||||
(core::mem::size_of::<u32>() * 1024 * 3) as _;
|
||||
|
||||
Self {
|
||||
pipeline,
|
||||
@@ -455,6 +482,7 @@ impl Renderer {
|
||||
previous_uniform_buffer_content: UniformBuffer::zeroed(),
|
||||
uniform_bind_group,
|
||||
texture_bind_group_layout,
|
||||
nearest_filtering_flag_buffers,
|
||||
textures: HashMap::default(),
|
||||
next_user_texture_id: 0,
|
||||
samplers: HashMap::default(),
|
||||
@@ -706,6 +734,8 @@ impl Renderer {
|
||||
};
|
||||
|
||||
let bind_group = bind_group.unwrap_or_else(|| {
|
||||
let nearest =
|
||||
image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
|
||||
let sampler = self
|
||||
.samplers
|
||||
.entry(image_delta.options)
|
||||
@@ -724,6 +754,11 @@ impl Renderer {
|
||||
binding: 1,
|
||||
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 {
|
||||
profiling::function_scope!();
|
||||
|
||||
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
compare: None,
|
||||
..sampler_descriptor
|
||||
@@ -843,6 +879,11 @@ impl Renderer {
|
||||
binding: 1,
|
||||
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)
|
||||
.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 {
|
||||
compare: None,
|
||||
..sampler_descriptor
|
||||
@@ -899,6 +941,11 @@ impl Renderer {
|
||||
binding: 1,
|
||||
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();
|
||||
|
||||
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 {
|
||||
// Resize index buffer if needed.
|
||||
self.index_buffer.capacity =
|
||||
@@ -989,7 +1036,7 @@ impl Renderer {
|
||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
||||
match primitive {
|
||||
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);
|
||||
index_buffer_staging
|
||||
.slice(slice.clone())
|
||||
@@ -1006,7 +1053,8 @@ impl Renderer {
|
||||
|
||||
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 {
|
||||
// Resize vertex buffer if needed.
|
||||
self.vertex_buffer.capacity =
|
||||
@@ -1034,7 +1082,7 @@ impl Renderer {
|
||||
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
|
||||
match primitive {
|
||||
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);
|
||||
vertex_buffer_staging
|
||||
.slice(slice.clone())
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::sync::Arc;
|
||||
/// Automatically implemented for all types that satisfy the bounds
|
||||
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
|
||||
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`].
|
||||
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
|
||||
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> {
|
||||
Box::new(self.clone())
|
||||
@@ -77,8 +77,8 @@ impl WgpuSetup {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuSetup {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for WgpuSetup {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => f
|
||||
.debug_tuple("WgpuSetup::CreateNew")
|
||||
@@ -295,8 +295,8 @@ impl Clone for WgpuSetupCreateNew {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WgpuSetupCreateNew {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for WgpuSetupCreateNew {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
instance_descriptor,
|
||||
display_handle,
|
||||
|
||||
@@ -8,8 +8,9 @@ use crate::{
|
||||
RendererOptions,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use core::num::NonZeroU32;
|
||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||
use std::{num::NonZeroU32, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct SurfaceState {
|
||||
surface: wgpu::Surface<'static>,
|
||||
@@ -478,7 +479,7 @@ impl Painter {
|
||||
pixels_per_point: f32,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||
textures_delta: &epaint::textures::TexturesDelta,
|
||||
textures_delta: &mut epaint::textures::TexturesDelta,
|
||||
capture_data: Vec<UserData>,
|
||||
window: &Arc<winit::window::Window>,
|
||||
) -> f32 {
|
||||
@@ -545,21 +546,6 @@ impl Painter {
|
||||
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 {
|
||||
return vsync_sec;
|
||||
};
|
||||
@@ -579,6 +565,18 @@ impl Painter {
|
||||
|
||||
let user_cmd_bufs = {
|
||||
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(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
@@ -730,7 +728,7 @@ impl Painter {
|
||||
let start = web_time::Instant::now();
|
||||
render_state
|
||||
.queue
|
||||
.submit(std::iter::chain(user_cmd_bufs, [encoded]));
|
||||
.submit(core::iter::chain(user_cmd_bufs, [encoded]));
|
||||
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.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
for id in &textures_delta.free {
|
||||
renderer.free_texture(id);
|
||||
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||
for id in textures_delta.free.drain() {
|
||||
renderer.free_texture(&id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@ This file is updated upon each release.
|
||||
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
|
||||
|
||||
|
||||
## 0.36.1 - 2026-08-07
|
||||
Nothing new
|
||||
|
||||
|
||||
## 0.36.0 - 2026-08-05
|
||||
Nothing new
|
||||
|
||||
|
||||
## 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)
|
||||
* Always enable windows undecorated shadows [#8169](https://github.com/emilk/egui/pull/8169) by [@Wumpf](https://github.com/Wumpf)
|
||||
|
||||
22
crates/egui-winit/src/dropped_file.rs
Normal file
22
crates/egui-winit/src/dropped_file.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct NativeFile {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl From<PathBuf> for NativeFile {
|
||||
fn from(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl egui::DroppedFile for NativeFile {
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn bytes(&self) -> Result<Vec<u8>, String> {
|
||||
std::fs::read(&self.path).map_err(|err| err.to_string())
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
|
||||
pub use winit;
|
||||
|
||||
pub mod clipboard;
|
||||
mod dropped_file;
|
||||
mod safe_area;
|
||||
mod window_settings;
|
||||
|
||||
@@ -28,6 +29,8 @@ pub use window_settings::WindowSettings;
|
||||
|
||||
use raw_window_handle::HasDisplayHandle;
|
||||
|
||||
use dropped_file::NativeFile;
|
||||
|
||||
use winit::{
|
||||
dpi::{PhysicalPosition, PhysicalSize},
|
||||
event::ElementState,
|
||||
@@ -121,6 +124,7 @@ pub struct State {
|
||||
|
||||
allow_ime: bool,
|
||||
ime_rect_px: Option<egui::Rect>,
|
||||
old_ime_purpose: egui::IMEPurpose,
|
||||
|
||||
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
|
||||
/// release events that should be filtered out. See comments in that method
|
||||
@@ -171,6 +175,7 @@ impl State {
|
||||
|
||||
allow_ime: false,
|
||||
ime_rect_px: None,
|
||||
old_ime_purpose: egui::IMEPurpose::Normal,
|
||||
#[cfg(target_os = "windows")]
|
||||
pressed_processed_physical_keys: HashSet::new(),
|
||||
};
|
||||
@@ -468,10 +473,9 @@ impl State {
|
||||
}
|
||||
WindowEvent::DroppedFile(path) => {
|
||||
self.egui_input.hovered_files.clear();
|
||||
self.egui_input.dropped_files.push(egui::DroppedFile {
|
||||
path: Some(path.clone()),
|
||||
..Default::default()
|
||||
});
|
||||
self.egui_input
|
||||
.dropped_files
|
||||
.push(std::sync::Arc::new(NativeFile::from(path.clone())));
|
||||
EventResponse {
|
||||
repaint: true,
|
||||
consumed: false,
|
||||
@@ -1158,6 +1162,11 @@ impl State {
|
||||
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 ime_rect_px = pixels_per_point * ime.rect;
|
||||
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::IMEPurpose(p) => window.set_ime_purpose(match 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::IMEPurpose(p) => window.set_ime_purpose(to_winit_ime_purpose(p)),
|
||||
ViewportCommand::Focus => {
|
||||
if !window.has_focus() {
|
||||
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.
|
||||
///
|
||||
/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`.
|
||||
|
||||
@@ -19,6 +19,7 @@ workspace = true
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--generate-link-to-definition"]
|
||||
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -96,3 +97,9 @@ document-features = { workspace = true, optional = true }
|
||||
|
||||
ron = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
|
||||
|
||||
|
||||
# web:
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# For `DroppedFile`, which hands web apps a file handle instead of its contents.
|
||||
web-sys = { workspace = true, features = ["File"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
|
||||
use core::fmt::Debug;
|
||||
use emath::Vec2;
|
||||
use epaint::text::TextWrapMode;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Args passed when sizing an [`super::Atom`]
|
||||
pub struct IntoSizedArgs {
|
||||
@@ -90,7 +90,7 @@ impl Clone 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 {
|
||||
AtomKind::Empty => write!(f, "AtomKind::Empty"),
|
||||
AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),
|
||||
|
||||
@@ -2,11 +2,11 @@ use crate::{
|
||||
AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense,
|
||||
SizedAtom, SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
|
||||
};
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
|
||||
use epaint::text::TextWrapMode;
|
||||
use epaint::{Color32, Galley};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 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>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
*kind = f(core::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{Atom, AtomKind, Image, WidgetText};
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use std::borrow::Cow;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// 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.
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ impl<'a> Atoms<'a> {
|
||||
|
||||
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
|
||||
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)
|
||||
@@ -136,7 +136,7 @@ impl<'a> Atoms<'a> {
|
||||
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
|
||||
{
|
||||
for kind in self.iter_kinds_mut() {
|
||||
*kind = f(std::mem::take(kind));
|
||||
*kind = f(core::mem::take(kind));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
10
crates/egui/src/cache/cache_storage.rs
vendored
10
crates/egui/src/cache/cache_storage.rs
vendored
@@ -23,18 +23,18 @@ use super::CacheTrait;
|
||||
/// ```
|
||||
#[derive(Default)]
|
||||
pub struct CacheStorage {
|
||||
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
|
||||
caches: ahash::HashMap<core::any::TypeId, Box<dyn CacheTrait>>,
|
||||
}
|
||||
|
||||
impl CacheStorage {
|
||||
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
|
||||
let cache = self
|
||||
.caches
|
||||
.entry(std::any::TypeId::of::<Cache>())
|
||||
.entry(core::any::TypeId::of::<Cache>())
|
||||
.or_insert_with(|| Box::<Cache>::default());
|
||||
|
||||
#[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>()
|
||||
.unwrap()
|
||||
}
|
||||
@@ -60,8 +60,8 @@ impl Clone for CacheStorage {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CacheStorage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for CacheStorage {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"FrameCacheStorage[{} caches with {} elements]",
|
||||
|
||||
2
crates/egui/src/cache/cache_trait.rs
vendored
2
crates/egui/src/cache/cache_trait.rs
vendored
@@ -1,6 +1,6 @@
|
||||
/// A cache, storing some value for some length of time.
|
||||
#[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.
|
||||
fn update(&mut self);
|
||||
|
||||
|
||||
2
crates/egui/src/cache/frame_cache.rs
vendored
2
crates/egui/src/cache/frame_cache.rs
vendored
@@ -48,7 +48,7 @@ impl<Value, Computer> FrameCache<Value, Computer> {
|
||||
/// or recompute and store in the cache.
|
||||
pub fn get<Key>(&mut self, key: Key) -> &Value
|
||||
where
|
||||
Key: Copy + std::hash::Hash,
|
||||
Key: Copy + core::hash::Hash,
|
||||
Computer: ComputerMut<Key, Value>,
|
||||
{
|
||||
let hash = crate::util::hash(key);
|
||||
|
||||
2
crates/egui/src/cache/frame_publisher.rs
vendored
2
crates/egui/src/cache/frame_publisher.rs
vendored
@@ -1,4 +1,4 @@
|
||||
use std::hash::Hash;
|
||||
use core::hash::Hash;
|
||||
|
||||
use super::CacheTrait;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::fmt::Write as _;
|
||||
use core::fmt::Write as _;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Frame {
|
||||
@@ -239,7 +239,7 @@ fn test_shorten_path() {
|
||||
),
|
||||
("/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();
|
||||
assert_eq!(shorten_source_file_path(&before), after);
|
||||
}
|
||||
|
||||
@@ -454,14 +454,12 @@ impl Area {
|
||||
state.size = None;
|
||||
}
|
||||
state.pivot = pivot;
|
||||
state.interactable = interactable;
|
||||
if let Some(new_pos) = new_pos {
|
||||
state.pivot_pos = Some(new_pos);
|
||||
}
|
||||
state.pivot_pos.get_or_insert_with(|| {
|
||||
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
|
||||
});
|
||||
state.interactable = interactable;
|
||||
|
||||
let size = *state.size.get_or_insert_with(|| {
|
||||
sizing_pass = true;
|
||||
@@ -484,6 +482,10 @@ impl Area {
|
||||
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
|
||||
let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#[expect(unused_imports)]
|
||||
use crate::{Ui, UiBuilder};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use core::sync::atomic::AtomicBool;
|
||||
|
||||
/// A tag to mark a container as closable.
|
||||
///
|
||||
@@ -18,11 +18,12 @@ impl ClosableTag {
|
||||
|
||||
/// Set close to `true`
|
||||
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.
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.close.load(std::sync::atomic::Ordering::Relaxed)
|
||||
self.close.load(core::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
|
||||
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
|
||||
let rect = rect.expand(visuals.expansion);
|
||||
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));
|
||||
for p in &mut points {
|
||||
*p = rect.center() + rotation * (*p - rect.center());
|
||||
|
||||
@@ -143,12 +143,12 @@ pub struct Frame {
|
||||
#[test]
|
||||
fn frame_size() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Frame>(),
|
||||
core::mem::size_of::<Frame>(),
|
||||
32,
|
||||
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
|
||||
);
|
||||
assert!(
|
||||
std::mem::size_of::<Frame>() <= 64,
|
||||
core::mem::size_of::<Frame>() <= 64,
|
||||
"Frame is getting way too big!"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,14 +18,24 @@
|
||||
use emath::GuiRounding as _;
|
||||
|
||||
use crate::{
|
||||
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect,
|
||||
Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
|
||||
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, Margin, NumExt as _,
|
||||
Order, Rangef, Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
|
||||
};
|
||||
|
||||
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
|
||||
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.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[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.
|
||||
fn set_rect_size(self, rect: &mut Rect, size: f32) {
|
||||
match self {
|
||||
@@ -182,6 +208,7 @@ pub struct Panel {
|
||||
id: Id,
|
||||
frame: Option<Frame>,
|
||||
resizable: bool,
|
||||
drag_to_open: bool,
|
||||
show_separator_line: bool,
|
||||
|
||||
/// _Outer_ size (including [`Frame`] margin & border):
|
||||
@@ -267,6 +294,7 @@ impl Panel {
|
||||
id: id.into(),
|
||||
frame: None,
|
||||
resizable: true,
|
||||
drag_to_open: true,
|
||||
show_separator_line: true,
|
||||
default_outer_size,
|
||||
outer_size_range,
|
||||
@@ -296,8 +324,39 @@ impl Panel {
|
||||
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?
|
||||
///
|
||||
/// 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`.
|
||||
#[inline]
|
||||
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.
|
||||
/// When [`Self::resizable`] is `true`, double-clicking the resize edge also
|
||||
/// 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>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
@@ -395,10 +457,11 @@ impl Panel {
|
||||
let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded);
|
||||
|
||||
if how_expanded == 0.0 {
|
||||
// Panel is fully closed. If the user is still dragging the resize handle
|
||||
// from a previous frame, keep its widget id alive so they can drag the
|
||||
// panel back out without releasing.
|
||||
self.keep_drag_alive_for_reopen(ui, is_expanded);
|
||||
// Panel is fully closed, but we still leave a grab handle at its fixed
|
||||
// edge so the user can drag it back open.
|
||||
if self.resizable && self.drag_to_open {
|
||||
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:
|
||||
ui.skip_ahead_auto_ids(1);
|
||||
@@ -407,7 +470,7 @@ impl Panel {
|
||||
|
||||
// Don't lose the drag during the slide-back-open animation:
|
||||
let drag_in_progress = ui
|
||||
.read_response(self.id.with("__resize"))
|
||||
.read_response(self.resize_id())
|
||||
.is_some_and(|r| r.dragged());
|
||||
|
||||
let panel = if how_expanded < 1.0 {
|
||||
@@ -520,20 +583,11 @@ impl Panel {
|
||||
|
||||
// Is the resize handle currently being dragged?
|
||||
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());
|
||||
|
||||
let animation_id = expanded_panel.id.with("animation");
|
||||
// While the user is dragging, snap the animation to the target so the
|
||||
// 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)
|
||||
};
|
||||
let how_expanded = animate_expansion(ui, animation_id, *is_expanded);
|
||||
|
||||
// When expanding, the user sees the expanded content the moment animation starts.
|
||||
// When collapsing, keep showing the expanded content until past the midpoint,
|
||||
@@ -556,7 +610,19 @@ impl Panel {
|
||||
let panel = if how_expanded < 1.0 {
|
||||
// Animate the visible size from collapsed_size to expanded_size,
|
||||
// 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 slide_fraction = if 0.0 < expanded_size {
|
||||
visible_size / expanded_size
|
||||
@@ -673,7 +739,7 @@ impl Panel {
|
||||
// released size gets persisted into [`PanelState`] — without this the
|
||||
// store-skipped-during-drag rule would leave the stored size at the
|
||||
// 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);
|
||||
|
||||
// Double-click on the resize edge toggles `*is_expanded` for the
|
||||
@@ -831,21 +897,34 @@ impl Panel {
|
||||
.store(parent_ui, id);
|
||||
}
|
||||
|
||||
// Hide the separator once the panel is mostly slid off — at that point
|
||||
// the line would just be a stray dash hovering near the parent edge.
|
||||
if 0.01 < self.slide_fraction {
|
||||
let stroke = if is_resizing {
|
||||
parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||
} else if resize_hover {
|
||||
parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||
} else if show_separator_line {
|
||||
// TODO(emilk): distinguish resizable from non-resizable
|
||||
parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
|
||||
} else {
|
||||
Stroke::NONE
|
||||
};
|
||||
// The highlight follows the pointer all the way down to zero size, where
|
||||
// `collapsed_resize_handle` picks it straight up again — so the user never
|
||||
// loses sight of the edge they are dragging. The dim idle separator does
|
||||
// get hidden once the panel is mostly slid off, since there it would just
|
||||
// be a stray dash hovering near the parent edge.
|
||||
let stroke = if is_resizing {
|
||||
parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
|
||||
} else if resize_hover {
|
||||
parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
|
||||
} else if show_separator_line && 0.01 < self.slide_fraction {
|
||||
// TODO(emilk): distinguish resizable from non-resizable
|
||||
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
|
||||
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());
|
||||
if axis == 0 {
|
||||
parent_ui.painter().vline(line_pos, cross_range, stroke);
|
||||
@@ -857,54 +936,123 @@ impl Panel {
|
||||
inner_response
|
||||
}
|
||||
|
||||
/// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
|
||||
fn resolve_frame(&self, ui: &Ui) -> Frame {
|
||||
self.frame
|
||||
.unwrap_or_else(|| Frame::side_top_panel(ui.style()))
|
||||
/// [`Id`] of this panel's resize-handle widget.
|
||||
///
|
||||
/// See [`resize_widget_id`] for why open and collapsed panels must share it.
|
||||
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
|
||||
/// from the frame the panel closed on, keep its widget id registered so the
|
||||
/// drag survives, and reopen if they drag back past the minimum size.
|
||||
fn keep_drag_alive_for_reopen(&self, ui: &Ui, is_expanded: &mut bool) {
|
||||
let resize_id = self.id.with("__resize");
|
||||
let Some(resize_response) = ui.read_response(resize_id) else {
|
||||
return;
|
||||
};
|
||||
if !resize_response.dragged() {
|
||||
return;
|
||||
}
|
||||
let Some(pointer) = resize_response.interact_pointer_pos() else {
|
||||
return;
|
||||
};
|
||||
/// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
|
||||
fn resolve_frame(&self, ui: &Ui) -> Frame {
|
||||
let mut frame = self
|
||||
.frame
|
||||
.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
|
||||
|
||||
if self.show_separator_line {
|
||||
// Reserve room for the separator line in the frame's _outer_ margin, so the line
|
||||
// lands just outside the frame's outline instead of painting on top of it:
|
||||
//
|
||||
// contents | `inner_margin` | outline | separator line | `outer_margin`
|
||||
//
|
||||
// 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 fixed_edge_pos = self.side.fixed_pos(available_rect);
|
||||
let cross_range = available_rect.range_along(self.side.cross_axis());
|
||||
let resize_rect = if self.side.axis() == 0 {
|
||||
let fixed_edge_pos = side.fixed_pos(available_rect);
|
||||
let cross_range = available_rect.range_along(side.cross_axis());
|
||||
|
||||
// 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)
|
||||
} else {
|
||||
Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos))
|
||||
};
|
||||
let grab = ui.style().interaction.resize_grab_radius_side;
|
||||
let resize_rect = resize_rect.expand2(grab * self.side.axis_unit());
|
||||
ui.interact(resize_rect, resize_id, Sense::drag());
|
||||
side.set_rect_size(
|
||||
&mut resize_rect,
|
||||
ui.style().interaction.resize_grab_radius_side,
|
||||
);
|
||||
|
||||
// Keep the resize cursor while the user is still holding the drag.
|
||||
// Otherwise the cursor would snap back to the default the moment the
|
||||
// panel closed, even though the gesture is still ongoing.
|
||||
ui.set_cursor_icon(self.cursor_icon(0.0));
|
||||
let resize_id = self.resize_id();
|
||||
let response = ui.interact(resize_rect, resize_id, Sense::click_and_drag());
|
||||
|
||||
// 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.
|
||||
let dragged_size = -self.side.sign() * (pointer[self.side.axis()] - fixed_edge_pos);
|
||||
if self.outer_size_range.min < dragged_size {
|
||||
if response.double_clicked() {
|
||||
*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),
|
||||
@@ -939,7 +1087,7 @@ impl Panel {
|
||||
|
||||
// Use `resize_id_source` so collapsed/expanded panels in
|
||||
// `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);
|
||||
ui.interact(resize_rect, resize_id, Sense::click_and_drag())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::iter::once;
|
||||
use core::iter::once;
|
||||
|
||||
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
|
||||
|
||||
@@ -179,7 +179,9 @@ pub struct Popup<'a> {
|
||||
|
||||
/// Default width passed to the Area
|
||||
width: Option<f32>,
|
||||
sizing_pass: bool,
|
||||
sense: Sense,
|
||||
interactable: bool,
|
||||
layout: Layout,
|
||||
frame: Option<Frame>,
|
||||
style: StyleModifier,
|
||||
@@ -201,7 +203,9 @@ impl<'a> Popup<'a> {
|
||||
alternative_aligns: None,
|
||||
gap: 0.0,
|
||||
width: None,
|
||||
sizing_pass: false,
|
||||
sense: Sense::click(),
|
||||
interactable: true,
|
||||
layout: Layout::default(),
|
||||
frame: None,
|
||||
style: StyleModifier::default(),
|
||||
@@ -369,6 +373,15 @@ impl<'a> Popup<'a> {
|
||||
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.
|
||||
#[inline]
|
||||
pub fn sense(mut self, sense: Sense) -> Self {
|
||||
@@ -390,6 +403,19 @@ impl<'a> Popup<'a> {
|
||||
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.
|
||||
#[inline]
|
||||
pub fn id(mut self, id: Id) -> Self {
|
||||
@@ -472,12 +498,12 @@ impl<'a> Popup<'a> {
|
||||
RectAlign::find_best_align(
|
||||
#[expect(clippy::iter_on_empty_collections)]
|
||||
#[expect(clippy::or_fun_call)]
|
||||
std::iter::chain(
|
||||
core::iter::chain(
|
||||
once(self.rect_align),
|
||||
self.alternative_aligns
|
||||
// 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()))
|
||||
.unwrap_or(std::iter::chain(
|
||||
.map(|a| core::iter::chain(a.iter().copied(), [].iter().copied()))
|
||||
.unwrap_or(core::iter::chain(
|
||||
self.rect_align.symmetries().iter().copied(),
|
||||
RectAlign::MENU_ALIGNS.iter().copied(),
|
||||
)),
|
||||
@@ -545,7 +571,9 @@ impl<'a> Popup<'a> {
|
||||
alternative_aligns: _,
|
||||
gap,
|
||||
width,
|
||||
sizing_pass,
|
||||
sense,
|
||||
interactable,
|
||||
layout,
|
||||
frame,
|
||||
style,
|
||||
@@ -570,8 +598,9 @@ impl<'a> Popup<'a> {
|
||||
.pivot(pivot)
|
||||
.fixed_pos(anchor)
|
||||
.sense(sense)
|
||||
.interactable(interactable)
|
||||
.layout(layout)
|
||||
.sizing_pass(!was_open_last_frame)
|
||||
.sizing_pass(sizing_pass || !was_open_last_frame)
|
||||
.info(info.unwrap_or_else(|| {
|
||||
UiStackInfo::new(kind.into()).with_tag_value(
|
||||
MenuConfig::MENU_CONFIG_TAG,
|
||||
|
||||
@@ -289,16 +289,16 @@ impl Resize {
|
||||
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.
|
||||
// After laying out the contents, we might be much bigger.
|
||||
// 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!
|
||||
// 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(
|
||||
inner_rect.min + state.last_content_size + Vec2::splat(ui.visuals().clip_rect_margin),
|
||||
);
|
||||
content_clip_rect.max = content_clip_rect
|
||||
.max
|
||||
.max(inner_rect.min + state.last_content_size);
|
||||
|
||||
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
#![expect(clippy::needless_range_loop)]
|
||||
|
||||
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||
use core::ops::{Add, AddAssign, BitOr, BitOrAssign};
|
||||
|
||||
use emath::GuiRounding as _;
|
||||
use epaint::{Color32, Direction, Margin, Shape};
|
||||
@@ -810,12 +810,11 @@ impl ScrollArea {
|
||||
|
||||
{
|
||||
// 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();
|
||||
for d in 0..2 {
|
||||
if direction_enabled[d] {
|
||||
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
|
||||
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
|
||||
content_clip_rect.min[d] = inner_rect.min[d];
|
||||
content_clip_rect.max[d] = inner_rect.max[d];
|
||||
} else {
|
||||
// Nice handling of forced resizing beyond the possible:
|
||||
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
|
||||
.ctx()
|
||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
|
||||
.pass_state_mut(|state| core::mem::take(&mut state.scroll_target));
|
||||
|
||||
Prepared {
|
||||
id,
|
||||
@@ -986,7 +985,7 @@ impl ScrollArea {
|
||||
ui: &mut Ui,
|
||||
row_height_sans_spacing: f32,
|
||||
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> {
|
||||
let spacing = ui.spacing().item_spacing;
|
||||
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 scroll_delta = content_ui
|
||||
.ctx()
|
||||
.pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
|
||||
|
||||
let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
|
||||
|
||||
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
|
||||
// is to avoid them leaking to other scroll areas.
|
||||
let scroll_target = content_ui
|
||||
@@ -1100,6 +1091,17 @@ impl Prepared {
|
||||
.pass_state_mut(|state| state.scroll_target[d].take());
|
||||
|
||||
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 {
|
||||
let pass_state::ScrollTarget {
|
||||
range,
|
||||
@@ -1133,8 +1135,8 @@ impl Prepared {
|
||||
0.0
|
||||
};
|
||||
|
||||
delta += delta_update;
|
||||
animation = animation_update;
|
||||
delta += delta_update;
|
||||
}
|
||||
|
||||
if delta != 0.0 {
|
||||
@@ -1158,10 +1160,10 @@ impl Prepared {
|
||||
}
|
||||
ui.request_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
if delta != 0.0 {
|
||||
had_explicit_scroll_adjustment[d] = true;
|
||||
if delta != 0.0 {
|
||||
had_explicit_scroll_adjustment[d] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1306,8 +1308,6 @@ impl Prepared {
|
||||
// * When one ScrollArea is nested inside another, and the outer
|
||||
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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 paint_rect = inner_rect
|
||||
.intersect(ui.min_rect())
|
||||
.expand(ui.visuals().clip_rect_margin);
|
||||
let paint_rect = inner_rect.intersect(ui.min_rect());
|
||||
|
||||
// Top fade: animate opacity based on how far we've scrolled down.
|
||||
if 0.0 < offset.y {
|
||||
|
||||
@@ -129,7 +129,15 @@ impl Tooltip<'_> {
|
||||
});
|
||||
|
||||
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| {
|
||||
// By default, the text in tooltips aren't selectable.
|
||||
@@ -192,6 +200,20 @@ impl Tooltip<'_> {
|
||||
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?
|
||||
///
|
||||
/// 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:
|
||||
|
||||
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
|
||||
&& response.ctx.viewport(|vp| {
|
||||
vp.prev_pass
|
||||
.widgets
|
||||
.get_layer(tooltip_layer_id)
|
||||
.any(|w| w.enabled && w.sense.interactive())
|
||||
});
|
||||
&& Self::had_interactive_widgets(&response.ctx, tooltip_id);
|
||||
|
||||
if tooltip_has_interactive_widget {
|
||||
// We keep the tooltip open if hovered,
|
||||
|
||||
@@ -84,6 +84,7 @@ pub struct Window<'a> {
|
||||
open: Option<&'a mut bool>,
|
||||
area: Area,
|
||||
frame: Option<Frame>,
|
||||
title_frame: Option<Frame>,
|
||||
resize: Resize,
|
||||
scroll: ScrollArea,
|
||||
collapsible: bool,
|
||||
@@ -106,6 +107,7 @@ impl<'a> Window<'a> {
|
||||
open: None,
|
||||
area,
|
||||
frame: None,
|
||||
title_frame: None,
|
||||
resize: Resize::default()
|
||||
.with_stroke(false)
|
||||
.min_size([96.0, 32.0])
|
||||
@@ -265,6 +267,13 @@ impl<'a> Window<'a> {
|
||||
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.
|
||||
#[inline]
|
||||
pub fn min_width(mut self, min_width: f32) -> Self {
|
||||
@@ -549,6 +558,7 @@ impl Window<'_> {
|
||||
mut open,
|
||||
area,
|
||||
frame,
|
||||
title_frame,
|
||||
resize,
|
||||
scroll,
|
||||
collapsible,
|
||||
@@ -616,10 +626,12 @@ impl Window<'_> {
|
||||
|
||||
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_title_frame = title_frame.unwrap_or(window_frame);
|
||||
|
||||
// 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 is_explicitly_closed = matches!(open, Some(false));
|
||||
@@ -711,7 +723,7 @@ impl Window<'_> {
|
||||
title_ui(
|
||||
ui,
|
||||
title,
|
||||
window_frame.inner_margin(window_margin),
|
||||
window_title_frame,
|
||||
&mut collapsing,
|
||||
collapsible,
|
||||
on_top,
|
||||
@@ -725,12 +737,12 @@ impl Window<'_> {
|
||||
.show_body_unindented(ui, |ui| {
|
||||
if scroll.is_any_scroll_enabled() {
|
||||
scroll
|
||||
.content_margin(window_margin)
|
||||
.content_margin(window_content_margin)
|
||||
.show(ui, add_contents)
|
||||
.inner
|
||||
} else {
|
||||
crate::Frame::NONE
|
||||
.inner_margin(window_margin)
|
||||
.inner_margin(window_content_margin)
|
||||
.show(ui, add_contents)
|
||||
.inner
|
||||
}
|
||||
@@ -909,7 +921,7 @@ impl SideResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for SideResponse {
|
||||
impl core::ops::BitAnd for SideResponse {
|
||||
type Output = Self;
|
||||
|
||||
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) {
|
||||
*self = Self {
|
||||
hover: self.hover || rhs.hover,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![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 epaint::{
|
||||
@@ -32,7 +33,7 @@ use crate::{
|
||||
load::{self, Bytes, Loaders, SizedTexture},
|
||||
memory::{Options, Theme},
|
||||
os::OperatingSystem,
|
||||
output::FullOutput,
|
||||
output::{FullOutput, LogicOutput},
|
||||
pass_state::PassState,
|
||||
plugin::{self, TypedPluginHandle},
|
||||
resize, response, scroll_area, theme,
|
||||
@@ -99,7 +100,7 @@ impl ContextImpl {
|
||||
fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) {
|
||||
let viewport = self.viewports.entry(viewport_id).or_default();
|
||||
|
||||
std::mem::swap(
|
||||
core::mem::swap(
|
||||
&mut viewport.repaint.prev_causes,
|
||||
&mut viewport.repaint.causes,
|
||||
);
|
||||
@@ -244,6 +245,12 @@ pub struct ViewportState {
|
||||
// ----------------------
|
||||
// Cross-frame statistics:
|
||||
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`]?
|
||||
@@ -259,14 +266,14 @@ pub struct RepaintCause {
|
||||
pub reason: Cow<'static, str>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for RepaintCause {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for RepaintCause {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}:{} {}", self.file, self.line, self.reason)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RepaintCause {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Display for RepaintCause {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}:{} {}", self.file, self.line, self.reason)
|
||||
}
|
||||
}
|
||||
@@ -456,7 +463,7 @@ impl ContextImpl {
|
||||
|
||||
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,
|
||||
viewport.repaint.requested_immediate_repaint_prev_pass(),
|
||||
pixels_per_point,
|
||||
@@ -469,7 +476,13 @@ impl ContextImpl {
|
||||
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));
|
||||
|
||||
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
|
||||
@@ -644,7 +657,7 @@ impl ContextImpl {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -712,13 +725,13 @@ impl ContextImpl {
|
||||
#[derive(Clone)]
|
||||
pub struct Context(Arc<RwLock<ContextImpl>>);
|
||||
|
||||
impl std::fmt::Debug for Context {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for Context {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
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 {
|
||||
Arc::ptr_eq(&self.0, &other.0)
|
||||
}
|
||||
@@ -728,7 +741,7 @@ impl Default for Context {
|
||||
fn default() -> Self {
|
||||
let ctx_impl = ContextImpl {
|
||||
embed_viewports: true,
|
||||
viewports: std::iter::once((ViewportId::ROOT, ViewportState::default())).collect(),
|
||||
viewports: core::iter::once((ViewportId::ROOT, ViewportState::default())).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let ctx = Self(Arc::new(RwLock::new(ctx_impl)));
|
||||
@@ -778,6 +791,7 @@ impl Context {
|
||||
/// ui.label("Hello egui!");
|
||||
/// });
|
||||
/// // handle full_output
|
||||
/// # full_output.drop_without_applying_deltas();
|
||||
/// ```
|
||||
#[must_use]
|
||||
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| {
|
||||
let viewport = ctx.viewport_for(viewport_id);
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -878,6 +892,57 @@ impl Context {
|
||||
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`].
|
||||
///
|
||||
/// It is usually better to use [`Self::run_ui`], because
|
||||
@@ -895,6 +960,7 @@ impl Context {
|
||||
///
|
||||
/// let full_output = ctx.end_pass();
|
||||
/// // handle full_output
|
||||
/// # full_output.drop_without_applying_deltas();
|
||||
/// ```
|
||||
pub fn begin_pass(&self, mut new_input: RawInput) {
|
||||
profiling::function_scope!();
|
||||
@@ -1698,11 +1764,11 @@ impl Context {
|
||||
.get(&id)
|
||||
.map(|v| v.repaint.cumulative_frame_nr)
|
||||
.unwrap_or_else(|| {
|
||||
if cfg!(debug_assertions) {
|
||||
panic!("cumulative_frame_nr_for failed to find the viewport {id:?}");
|
||||
} else {
|
||||
0
|
||||
}
|
||||
debug_assert!(
|
||||
false,
|
||||
"cumulative_frame_nr_for failed to find the viewport {id:?}"
|
||||
);
|
||||
0
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1813,7 +1879,7 @@ impl Context {
|
||||
/// See [`Self::request_repaint_after`] for details.
|
||||
#[track_caller]
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1996,7 +2062,7 @@ impl Context {
|
||||
&self,
|
||||
f: impl FnOnce(&mut T) -> 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()))
|
||||
}
|
||||
|
||||
@@ -2008,13 +2074,13 @@ impl Context {
|
||||
if let Some(plugin) = self.plugin_opt() {
|
||||
plugin
|
||||
} 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.
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -2430,6 +2496,8 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
self.sync_window_theme();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
self.debug_painting();
|
||||
|
||||
@@ -2441,11 +2509,43 @@ impl Context {
|
||||
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.
|
||||
#[cfg(debug_assertions)]
|
||||
fn debug_painting(&self) {
|
||||
#![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 rect = widget.interact_rect;
|
||||
@@ -2626,7 +2726,7 @@ impl ContextImpl {
|
||||
// Inform the backend of all textures that have been updated (including font atlas).
|
||||
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()
|
||||
&& let Some(ime) = &mut platform_output.ime
|
||||
@@ -2686,7 +2786,7 @@ impl ContextImpl {
|
||||
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 {
|
||||
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.
|
||||
// This can make things easier for the backend, as otherwise we may get commands
|
||||
// that affect a viewport while its egui logic is running.
|
||||
std::mem::take(&mut viewport.commands)
|
||||
core::mem::take(&mut viewport.commands)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
@@ -4233,13 +4333,13 @@ fn warn_if_rect_changes_id(
|
||||
struct OrderedRect(Rect);
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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 rhs = other.0;
|
||||
lhs.min
|
||||
@@ -4341,6 +4441,7 @@ mod test {
|
||||
assert_eq!(num_calls, 1);
|
||||
assert_eq!(output.platform_output.num_completed_passes, 1);
|
||||
assert!(!output.platform_output.requested_discard());
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
// A single call, with a denied request to discard:
|
||||
@@ -4366,6 +4467,7 @@ mod test {
|
||||
.reason,
|
||||
"test"
|
||||
);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4386,6 +4488,7 @@ mod test {
|
||||
assert_eq!(num_calls, 1);
|
||||
assert_eq!(output.platform_output.num_completed_passes, 1);
|
||||
assert!(!output.platform_output.requested_discard());
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
// Request discard once:
|
||||
@@ -4408,6 +4511,7 @@ mod test {
|
||||
!output.platform_output.requested_discard(),
|
||||
"The request should have been cleared when fulfilled"
|
||||
);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
// Request discard twice:
|
||||
@@ -4432,6 +4536,7 @@ mod test {
|
||||
output.platform_output.requested_discard(),
|
||||
"The unfulfilled request should be reported"
|
||||
);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4460,6 +4565,7 @@ mod test {
|
||||
!output.platform_output.requested_discard(),
|
||||
"The request should have been cleared when fulfilled"
|
||||
);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,51 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use core::{future::Future, pin::Pin};
|
||||
|
||||
/// A file dropped into egui.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DroppedFile {
|
||||
/// Set by the `egui-winit` backend.
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
///
|
||||
/// The integration owns the concrete file handle, letting egui remain independent of windowing
|
||||
/// backends and file APIs.
|
||||
pub trait DroppedFile: core::fmt::Debug {
|
||||
/// 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.
|
||||
pub name: String,
|
||||
/// Read the file contents.
|
||||
///
|
||||
/// 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).
|
||||
pub mime: String,
|
||||
/// Read the file contents.
|
||||
///
|
||||
/// # 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.
|
||||
pub last_modified: Option<std::time::SystemTime>,
|
||||
|
||||
/// Set by the `eframe` web backend.
|
||||
pub bytes: Option<std::sync::Arc<[u8]>>,
|
||||
/// The browser file handle, if this file was dropped on the web.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn web_file(&self) -> Option<&web_sys::File> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A shared reference to a dropped file.
|
||||
#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))]
|
||||
pub type DroppedFileHandle = Arc<dyn DroppedFile + Send + Sync>;
|
||||
|
||||
/// A shared reference to a dropped file.
|
||||
///
|
||||
/// This is not necessarily `Send + Sync` when wasm threads are enabled, because
|
||||
/// [`web_sys::File`] is not thread-safe in that configuration.
|
||||
#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
|
||||
pub type DroppedFileHandle = Arc<dyn DroppedFile>;
|
||||
|
||||
@@ -14,7 +14,7 @@ pub enum ImeEvent {
|
||||
/// a non-empty preedit string indicates that the IME is active.
|
||||
Preedit {
|
||||
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.
|
||||
@@ -22,6 +22,15 @@ pub enum ImeEvent {
|
||||
/// The IME is considered dismissed after this event.
|
||||
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.
|
||||
#[deprecated = "No longer used by egui"]
|
||||
Disabled,
|
||||
|
||||
@@ -16,7 +16,7 @@ mod touch;
|
||||
mod viewport_info;
|
||||
|
||||
pub use self::{
|
||||
dropped_file::DroppedFile,
|
||||
dropped_file::{DroppedFile, DroppedFileHandle},
|
||||
event::Event,
|
||||
event_filter::EventFilter,
|
||||
hovered_file::HoveredFile,
|
||||
|
||||
@@ -37,8 +37,8 @@ pub struct Modifiers {
|
||||
pub command: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Modifiers {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for Modifiers {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
if self.is_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;
|
||||
|
||||
#[inline]
|
||||
@@ -396,7 +396,7 @@ impl std::ops::BitOr for Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOrAssign for Modifiers {
|
||||
impl core::ops::BitOrAssign for Modifiers {
|
||||
#[inline]
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = *self | rhs;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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.
|
||||
///
|
||||
@@ -13,7 +13,7 @@ use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
|
||||
///
|
||||
/// Ii "points" can be calculated from native physical pixels
|
||||
/// 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))]
|
||||
pub struct RawInput {
|
||||
/// The id of the active viewport.
|
||||
@@ -65,9 +65,20 @@ pub struct RawInput {
|
||||
|
||||
/// 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
|
||||
/// 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).
|
||||
///
|
||||
@@ -84,7 +95,7 @@ impl Default for RawInput {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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,
|
||||
max_texture_side: None,
|
||||
time: None,
|
||||
@@ -123,9 +134,9 @@ impl RawInput {
|
||||
max_texture_side: self.max_texture_side.take(),
|
||||
time: self.time,
|
||||
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(),
|
||||
dropped_files: std::mem::take(&mut self.dropped_files),
|
||||
dropped_files: core::mem::take(&mut self.dropped_files),
|
||||
focused: self.focused,
|
||||
system_theme: self.system_theme,
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::emath::Rect;
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct SafeAreaInsets(pub MarginF32);
|
||||
|
||||
impl std::ops::Sub<SafeAreaInsets> for Rect {
|
||||
impl core::ops::Sub<SafeAreaInsets> for Rect {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {
|
||||
|
||||
@@ -117,7 +117,7 @@ impl ViewportInfo {
|
||||
Self {
|
||||
parent: self.parent,
|
||||
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,
|
||||
monitor_size: self.monitor_size,
|
||||
inner_rect: self.inner_rect,
|
||||
@@ -209,7 +209,7 @@ impl ViewportInfo {
|
||||
}
|
||||
|
||||
#[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:?}"))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! 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;
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct FullOutput {
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
@@ -76,6 +98,9 @@ impl FullOutput {
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct IMEOutput {
|
||||
/// IME's purpose.
|
||||
pub purpose: crate::IMEPurpose,
|
||||
|
||||
/// Where the [`crate::TextEdit`] is located on screen.
|
||||
pub rect: crate::Rect,
|
||||
|
||||
@@ -217,7 +242,7 @@ impl PlatformOutput {
|
||||
/// Take everything ephemeral (everything except `cursor_icon` and
|
||||
/// `cursor_image` currently)
|
||||
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_image = taken.cursor_image.clone(); // sticky between frames
|
||||
taken
|
||||
@@ -302,8 +327,8 @@ pub struct CustomCursorImage {
|
||||
pub hotspot: [u16; 2],
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CustomCursorImage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for CustomCursorImage {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("CustomCursorImage")
|
||||
.field("size", &self.size)
|
||||
.field("hotspot", &self.hotspot)
|
||||
@@ -519,8 +544,8 @@ impl OutputEvent {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OutputEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for OutputEvent {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
|
||||
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
|
||||
@@ -566,8 +591,8 @@ pub struct WidgetInfo {
|
||||
pub hint_text: Option<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WidgetInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for WidgetInfo {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
typ,
|
||||
enabled,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
use core::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A wrapper around `dyn Any`, used for passing custom user data
|
||||
/// to [`crate::ViewportCommand::Screenshot`].
|
||||
@@ -30,8 +31,8 @@ impl PartialEq for UserData {
|
||||
|
||||
impl Eq for UserData {}
|
||||
|
||||
impl std::hash::Hash for UserData {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
impl core::hash::Hash for UserData {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn print(ctx: &Context, text: impl Into<WidgetText>) {
|
||||
return;
|
||||
}
|
||||
|
||||
let location = std::panic::Location::caller();
|
||||
let location = core::panic::Location::caller();
|
||||
let location = format!("{}:{}", location.file(), location.line());
|
||||
|
||||
let plugin = ctx.plugin::<DebugTextPlugin>();
|
||||
@@ -58,7 +58,7 @@ impl Plugin for DebugTextPlugin {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
use core::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Context, CursorIcon, Plugin, Ui};
|
||||
|
||||
|
||||
@@ -75,6 +75,11 @@ pub(crate) struct GridLayout {
|
||||
curr_state: State,
|
||||
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:
|
||||
num_columns: Option<usize>,
|
||||
spacing: Vec2,
|
||||
@@ -90,6 +95,10 @@ pub(crate) struct GridLayout {
|
||||
impl GridLayout {
|
||||
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
|
||||
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();
|
||||
|
||||
// TODO(emilk): respect current layout
|
||||
@@ -110,6 +119,7 @@ impl GridLayout {
|
||||
prev_state,
|
||||
curr_state: State::default(),
|
||||
initial_available,
|
||||
sizing_pass,
|
||||
|
||||
num_columns: None,
|
||||
spacing: ui.spacing().item_spacing,
|
||||
@@ -180,7 +190,11 @@ impl GridLayout {
|
||||
}
|
||||
|
||||
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 size = child_size.max(vec2(width, height));
|
||||
Rect::from_min_size(cursor.min, size).round_ui()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
|
||||
|
||||
use std::num::NonZeroU64;
|
||||
use core::num::NonZeroU64;
|
||||
|
||||
use crate::{AsIdSalt, IdSalt};
|
||||
|
||||
@@ -8,9 +8,9 @@ use crate::{AsIdSalt, IdSalt};
|
||||
///
|
||||
/// This is all types implementing `Hash` and `Debug`,
|
||||
/// 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.
|
||||
///
|
||||
@@ -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`.
|
||||
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
|
||||
#[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);
|
||||
|
||||
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.
|
||||
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();
|
||||
hasher.write_u64(self.value());
|
||||
hasher.write_u64(IdSalt::new(&salt).value());
|
||||
@@ -124,8 +131,8 @@ impl Id {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Id {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for Id {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
if *self == Self::NULL {
|
||||
return write!(f, "Id::NULL");
|
||||
}
|
||||
@@ -204,8 +211,8 @@ mod id_source {
|
||||
|
||||
#[test]
|
||||
fn id_size() {
|
||||
assert_eq!(std::mem::size_of::<Id>(), 8);
|
||||
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
|
||||
assert_eq!(core::mem::size_of::<Id>(), 8);
|
||||
assert_eq!(core::mem::size_of::<Option<Id>>(), 8);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::num::NonZeroU64;
|
||||
use core::num::NonZeroU64;
|
||||
|
||||
/// Types that can be converted to an [`IdSalt`].
|
||||
///
|
||||
/// This is all types implementing `Hash` and `Debug`,
|
||||
/// 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.
|
||||
///
|
||||
@@ -57,8 +57,8 @@ impl IdSalt {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for IdSalt {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for IdSalt {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(source) = id_salt_source::get(*self) {
|
||||
return write!(f, "IdSalt::new({source})");
|
||||
|
||||
@@ -13,10 +13,8 @@ use crate::{
|
||||
},
|
||||
input_state::wheel_state::WheelState,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
use core::time::Duration;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
pub use crate::Key;
|
||||
pub use touch_state::MultiTouchInfo;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{collections::BTreeMap, fmt::Debug};
|
||||
use core::fmt::Debug;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{
|
||||
Event, RawInput, TouchId, TouchPhase,
|
||||
@@ -305,7 +306,7 @@ impl TouchState {
|
||||
|
||||
impl Debug for TouchState {
|
||||
// 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 {
|
||||
f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
|
||||
}
|
||||
|
||||
@@ -197,7 +197,24 @@ pub(crate) fn interact(
|
||||
// This widget is sensitive to both clicks and drags.
|
||||
// When the mouse first is pressed, it could be either,
|
||||
// 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 {
|
||||
// This widget is just sensitive to drags, so we can mark it as dragged right away:
|
||||
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 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)
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -96,8 +96,8 @@ impl LayerId {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LayerId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for LayerId {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self { order, id } = self;
|
||||
write!(f, "LayerId {{ {order:?} {id:?} }}")
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use emath::GuiRounding as _;
|
||||
use emath::{GuiRounding as _, fast_midpoint};
|
||||
|
||||
use crate::{
|
||||
Align, Direction,
|
||||
@@ -477,12 +477,12 @@ impl Layout {
|
||||
|
||||
// Make sure it isn't negative:
|
||||
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.max.x = x;
|
||||
}
|
||||
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.max.y = y;
|
||||
}
|
||||
|
||||
@@ -474,7 +474,7 @@ pub use self::{
|
||||
Key, UserData,
|
||||
input::*,
|
||||
output::{
|
||||
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand,
|
||||
self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand,
|
||||
PlatformOutput, UserAttentionType, WidgetInfo,
|
||||
},
|
||||
},
|
||||
@@ -680,18 +680,20 @@ pub enum WidgetType {
|
||||
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
|
||||
let ctx = Context::default();
|
||||
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
|
||||
let _ = ctx.run_ui(Default::default(), |ui| {
|
||||
let output = ctx.run_ui(Default::default(), |ui| {
|
||||
run_ui(ui.ctx());
|
||||
});
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
/// For use in tests; especially doctests.
|
||||
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
|
||||
let ctx = Context::default();
|
||||
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);
|
||||
});
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
|
||||
pub fn accesskit_root_id() -> Id {
|
||||
|
||||
@@ -55,12 +55,11 @@
|
||||
mod bytes_loader;
|
||||
mod texture_loader;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
use core::{
|
||||
fmt::{Debug, Display},
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use ahash::HashMap;
|
||||
|
||||
@@ -108,13 +107,13 @@ impl LoadError {
|
||||
detected_format.as_ref().map_or(0, |s| s.len())
|
||||
}
|
||||
Self::Loading(message) => message.len(),
|
||||
_ => std::mem::size_of::<Self>(),
|
||||
_ => core::mem::size_of::<Self>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Self::NoImageLoaders => f.write_str(
|
||||
"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.
|
||||
///
|
||||
@@ -209,7 +208,7 @@ pub enum 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 {
|
||||
Self::Static(arg0) => f.debug_tuple("Static").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;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// 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`].
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
|
||||
@@ -640,3 +652,14 @@ impl Loaders {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_extension() {
|
||||
assert!(has_extension("cat.svg", "svg"));
|
||||
assert!(has_extension("cat.SVG", "svg"));
|
||||
assert!(has_extension("http://example.com/cat.gif#frame=2", "gif"));
|
||||
assert!(!has_extension("cat.svg.png", "svg"));
|
||||
assert!(!has_extension("svg", "svg"));
|
||||
assert!(!has_extension("cat.jpeg", "jpg"));
|
||||
assert!(!has_extension("cat.svg?v=1", "svg"));
|
||||
}
|
||||
|
||||
@@ -150,5 +150,5 @@ impl TextureLoader for DefaultTextureLoader {
|
||||
}
|
||||
|
||||
fn is_svg(uri: &str) -> bool {
|
||||
uri.ends_with(".svg")
|
||||
super::has_extension(uri, "svg")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use core::num::NonZeroUsize;
|
||||
|
||||
use ahash::{HashMap, HashSet};
|
||||
use epaint::emath::TSTransform;
|
||||
@@ -216,6 +216,18 @@ pub struct Options {
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
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.
|
||||
///
|
||||
/// This is used to calculate the `pixels_per_point`
|
||||
@@ -318,6 +330,7 @@ impl Default for Options {
|
||||
theme_preference: Default::default(),
|
||||
fallback_theme: Theme::Dark,
|
||||
system_theme: None,
|
||||
sync_window_theme: true,
|
||||
zoom_factor: 1.0,
|
||||
zoom_with_keyboard: true,
|
||||
quit_shortcuts: vec![crate::KeyboardShortcut::new(
|
||||
@@ -381,6 +394,7 @@ impl Options {
|
||||
theme_preference,
|
||||
fallback_theme: _,
|
||||
system_theme: _,
|
||||
sync_window_theme,
|
||||
zoom_factor,
|
||||
zoom_with_keyboard,
|
||||
quit_shortcuts: _, // not shown in ui
|
||||
@@ -429,6 +443,8 @@ impl Options {
|
||||
.show(ui, |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 {
|
||||
Theme::Dark => dark_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) {
|
||||
matches!(
|
||||
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 {
|
||||
true
|
||||
@@ -966,7 +982,7 @@ impl Memory {
|
||||
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame)
|
||||
&& matches!(
|
||||
self.areas().compare_order(layer_id, current),
|
||||
std::cmp::Ordering::Less
|
||||
core::cmp::Ordering::Less
|
||||
)
|
||||
{
|
||||
return;
|
||||
@@ -1194,6 +1210,11 @@ impl Areas {
|
||||
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.
|
||||
pub(crate) fn order(&self) -> &[LayerId] {
|
||||
&self.order
|
||||
@@ -1202,12 +1223,12 @@ impl Areas {
|
||||
/// 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.
|
||||
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.
|
||||
// If `order_map` only contains one layer ID, then the other one will be
|
||||
// lower because `None < Some(x)`.
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1255,7 +1276,7 @@ impl Areas {
|
||||
}
|
||||
|
||||
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
|
||||
std::iter::chain(
|
||||
core::iter::chain(
|
||||
&self.visible_areas_last_frame,
|
||||
&self.visible_areas_current_frame,
|
||||
)
|
||||
@@ -1344,7 +1365,7 @@ impl Areas {
|
||||
..
|
||||
} = 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();
|
||||
|
||||
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:
|
||||
// (it doesn't matter in which order we replace parents with their children)
|
||||
#[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…
|
||||
|
||||
order.retain(|l| {
|
||||
@@ -1462,14 +1483,14 @@ fn order_map_total_ordering() {
|
||||
let mut i = 0;
|
||||
for &[a, b] in layers.array_windows() {
|
||||
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;
|
||||
}
|
||||
equivalence_classes.push(i);
|
||||
}
|
||||
assert_eq!(layers.len(), equivalence_classes.len());
|
||||
for (&l1, c1) in std::iter::zip(&layers, &equivalence_classes) {
|
||||
for (&l2, c2) in std::iter::zip(&layers, &equivalence_classes) {
|
||||
for (&l1, c1) in core::iter::zip(&layers, &equivalence_classes) {
|
||||
for (&l2, c2) in core::iter::zip(&layers, &equivalence_classes) {
|
||||
assert_eq!(
|
||||
c1.cmp(c2),
|
||||
areas.compare_order(l1, l2),
|
||||
|
||||
@@ -280,7 +280,7 @@ impl Painter {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
|
||||
pub fn error(&self, pos: Pos2, text: impl core::fmt::Display) -> Rect {
|
||||
let color = self.ctx.global_style().visuals.error_fg_color;
|
||||
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()`.
|
||||
pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
|
||||
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 = origin + vec;
|
||||
let dir = vec.normalized();
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
||||
/// Plugins should not hold a reference to the [`Context`], since this would create a cycle
|
||||
/// (which would prevent the [`Context`] from being dropped).
|
||||
#[expect(unused_variables)]
|
||||
pub trait Plugin: Send + Sync + std::any::Any + 'static {
|
||||
pub trait Plugin: Send + Sync + core::any::Any + 'static {
|
||||
/// Plugin name.
|
||||
///
|
||||
/// Used when profiling.
|
||||
@@ -60,14 +60,14 @@ pub(crate) struct PluginHandle {
|
||||
/// Use [`Self::lock`] to access the plugin.
|
||||
pub struct TypedPluginHandle<P: Plugin> {
|
||||
handle: Arc<Mutex<PluginHandle>>,
|
||||
_type: std::marker::PhantomData<P>,
|
||||
_type: core::marker::PhantomData<P>,
|
||||
}
|
||||
|
||||
impl<P: Plugin> TypedPluginHandle<P> {
|
||||
pub(crate) fn new(handle: Arc<Mutex<PluginHandle>>) -> Self {
|
||||
Self {
|
||||
handle,
|
||||
_type: std::marker::PhantomData,
|
||||
_type: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ impl<P: Plugin> TypedPluginHandle<P> {
|
||||
pub fn lock(&self) -> TypedPluginGuard<'_, P> {
|
||||
TypedPluginGuard {
|
||||
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`].
|
||||
pub struct TypedPluginGuard<'a, P: Plugin> {
|
||||
guard: MutexGuard<'a, PluginHandle>,
|
||||
_type: std::marker::PhantomData<P>,
|
||||
_type: core::marker::PhantomData<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;
|
||||
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -120,13 +120,13 @@ impl PluginHandle {
|
||||
}
|
||||
|
||||
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>()
|
||||
.expect("PluginHandle: plugin is not of the expected type")
|
||||
}
|
||||
|
||||
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>()
|
||||
.expect("PluginHandle: plugin is not of the expected type")
|
||||
}
|
||||
@@ -135,7 +135,7 @@ impl PluginHandle {
|
||||
/// User-registered plugins.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct Plugins {
|
||||
plugins: HashMap<std::any::TypeId, Arc<Mutex<PluginHandle>>>,
|
||||
plugins: HashMap<core::any::TypeId, Arc<Mutex<PluginHandle>>>,
|
||||
plugins_ordered: PluginsOrdered,
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ impl Plugins {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
use core::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui,
|
||||
@@ -77,7 +78,7 @@ pub struct Response {
|
||||
#[test]
|
||||
fn test_response_size() {
|
||||
assert_eq!(
|
||||
std::mem::size_of::<Response>(),
|
||||
core::mem::size_of::<Response>(),
|
||||
88,
|
||||
"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.
|
||||
/// `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)]
|
||||
pub fn hovered(&self) -> bool {
|
||||
self.flags.contains(Flags::HOVERED)
|
||||
@@ -403,11 +410,21 @@ impl Response {
|
||||
/// 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 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.
|
||||
///
|
||||
/// 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`.
|
||||
/// [`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.
|
||||
///
|
||||
/// 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)]
|
||||
pub fn is_pointer_button_down_on(&self) -> bool {
|
||||
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.
|
||||
impl std::ops::BitOr for Response {
|
||||
impl core::ops::BitOr for Response {
|
||||
type Output = 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"); }
|
||||
/// # });
|
||||
/// ```
|
||||
impl std::ops::BitOrAssign for Response {
|
||||
impl core::ops::BitOrAssign for Response {
|
||||
fn bitor_assign(&mut self, rhs: Self) {
|
||||
*self = self.union(rhs);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ bitflags::bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Sense {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for Sense {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "Sense {{")?;
|
||||
if self.senses_click() {
|
||||
write!(f, " click")?;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
//! egui theme (spacing, colors, etc).
|
||||
|
||||
use core::ops::RangeInclusive;
|
||||
use emath::Align;
|
||||
use epaint::{
|
||||
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
|
||||
text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
|
||||
};
|
||||
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
|
||||
@@ -47,8 +48,8 @@ impl NumberFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for NumberFormatter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for NumberFormatter {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.write_str("NumberFormatter")
|
||||
}
|
||||
}
|
||||
@@ -93,8 +94,8 @@ pub enum TextStyle {
|
||||
Name(std::sync::Arc<str>),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TextStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Display for TextStyle {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Small => "Small".fmt(f),
|
||||
Self::Body => "Body".fmt(f),
|
||||
@@ -192,8 +193,8 @@ impl From<TextStyle> for FontSelection {
|
||||
#[derive(Clone, Default)]
|
||||
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
|
||||
|
||||
impl std::fmt::Debug for StyleModifier {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for StyleModifier {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.write_str("StyleModifier")
|
||||
}
|
||||
}
|
||||
@@ -419,6 +420,9 @@ pub struct Spacing {
|
||||
/// Default width of a [`crate::TextEdit`].
|
||||
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.
|
||||
/// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
|
||||
pub icon_width: f32,
|
||||
@@ -1074,10 +1078,12 @@ pub struct Visuals {
|
||||
/// How the text cursor acts.
|
||||
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.
|
||||
#[deprecated(note = "This is now unused and has no effect")]
|
||||
pub clip_rect_margin: f32,
|
||||
|
||||
/// Show a background behind buttons.
|
||||
@@ -1456,6 +1462,7 @@ impl Default for Spacing {
|
||||
slider_rail_height: 8.0,
|
||||
combo_width: 100.0,
|
||||
text_edit_width: 280.0,
|
||||
extra_text_line_spacing: 0.0,
|
||||
icon_width: 14.0,
|
||||
icon_width_inner: 8.0,
|
||||
icon_spacing: 4.0,
|
||||
@@ -1487,6 +1494,7 @@ impl Default for Interaction {
|
||||
|
||||
impl Visuals {
|
||||
/// Default dark theme.
|
||||
#[expect(deprecated)]
|
||||
pub fn dark() -> Self {
|
||||
Self {
|
||||
dark_mode: true,
|
||||
@@ -1945,6 +1953,7 @@ impl Spacing {
|
||||
slider_rail_height,
|
||||
combo_width,
|
||||
text_edit_width,
|
||||
extra_text_line_spacing,
|
||||
icon_width,
|
||||
icon_width_inner,
|
||||
icon_spacing,
|
||||
@@ -2011,6 +2020,10 @@ impl Spacing {
|
||||
ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0));
|
||||
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.add(DragValue::new(tooltip_width).range(0.0..=1000.0));
|
||||
ui.end_row();
|
||||
@@ -2263,6 +2276,7 @@ impl WidgetVisuals {
|
||||
}
|
||||
|
||||
impl Visuals {
|
||||
#[expect(deprecated)]
|
||||
pub fn ui(&mut self, ui: &mut crate::Ui) {
|
||||
let Self {
|
||||
dark_mode,
|
||||
@@ -2297,7 +2311,7 @@ impl Visuals {
|
||||
|
||||
text_cursor,
|
||||
|
||||
clip_rect_margin,
|
||||
clip_rect_margin: _,
|
||||
button_frame,
|
||||
collapsing_header_frame,
|
||||
indent_has_left_vline,
|
||||
@@ -2484,8 +2498,6 @@ impl Visuals {
|
||||
|
||||
ui.collapsing("Misc", |ui| {
|
||||
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
|
||||
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
|
||||
|
||||
ui.checkbox(button_frame, "Button has a frame");
|
||||
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
|
||||
ui.checkbox(
|
||||
@@ -2684,7 +2696,7 @@ impl DebugOptions {
|
||||
}
|
||||
|
||||
// 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| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
@@ -2753,8 +2765,8 @@ impl NumericColorSpace {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NumericColorSpace {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Display for NumericColorSpace {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::GammaByte => write!(f, "U8"),
|
||||
Self::Linear => write!(f, "F"),
|
||||
|
||||
@@ -49,9 +49,9 @@ impl CCursorRange {
|
||||
}
|
||||
|
||||
/// 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();
|
||||
std::ops::Range {
|
||||
core::ops::Range {
|
||||
start: start.index,
|
||||
end: end.index,
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
|
||||
galley.pos_from_cursor(ccursor).center()
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WidgetTextCursor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for WidgetTextCursor {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
widget_id,
|
||||
ccursor,
|
||||
@@ -271,7 +271,7 @@ impl ViewportLabelSelectionState {
|
||||
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() {
|
||||
ui.copy_text(text_to_copy);
|
||||
}
|
||||
@@ -773,7 +773,7 @@ mod tests {
|
||||
.or_default()
|
||||
.selection = Some(test_selection());
|
||||
|
||||
let _ = ctx.run_ui(RawInput::default(), |_| {});
|
||||
let output = ctx.run_ui(RawInput::default(), |_| {});
|
||||
assert!(
|
||||
plugin
|
||||
.lock()
|
||||
@@ -782,11 +782,13 @@ mod tests {
|
||||
.is_some_and(ViewportLabelSelectionState::has_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!(
|
||||
!plugin.lock().has_selection(),
|
||||
"the selection must be cleared when its labels disappear from the same viewport"
|
||||
);
|
||||
output.drop_without_applying_deltas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ pub fn char_index_from_byte_index(input: &str, byte_index: ByteIndex) -> CharInd
|
||||
CharIndex(input.chars().count())
|
||||
}
|
||||
|
||||
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!(
|
||||
char_range.start <= char_range.end,
|
||||
"Invalid range, start must be less than end, but start = {}, end = {}",
|
||||
|
||||
@@ -139,8 +139,8 @@ pub(crate) fn paint_ime_preedit_text_visuals(
|
||||
painter: &Painter,
|
||||
galley: &Arc<Galley>,
|
||||
row_height: f32,
|
||||
preedit_range: std::ops::Range<CCursor>,
|
||||
mut relative_active_range: Option<std::ops::Range<CCursor>>,
|
||||
preedit_range: core::ops::Range<CCursor>,
|
||||
mut relative_active_range: Option<core::ops::Range<CCursor>>,
|
||||
time_since_last_interaction: f64,
|
||||
) {
|
||||
/// 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
|
||||
/// [`CCursor::prefer_next_row`] makes it difficult to define a clear
|
||||
/// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
|
||||
#![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::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).
|
||||
/// 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 {
|
||||
use std::f32::consts::TAU;
|
||||
use core::f32::consts::TAU;
|
||||
|
||||
let mut taus = *radians / TAU;
|
||||
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 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 child_rect = Rect::from_min_max(
|
||||
pos,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use core::{any::Any, iter::FusedIterator};
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, iter::FusedIterator};
|
||||
|
||||
use crate::widget_style::Classes;
|
||||
use epaint::Color32;
|
||||
|
||||
@@ -16,15 +16,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> std::fmt::Debug for FixedCache<K, V> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl<K, V> core::fmt::Debug for FixedCache<K, V> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "Cache")
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> FixedCache<K, V>
|
||||
where
|
||||
K: std::hash::Hash + PartialEq,
|
||||
K: core::hash::Hash + PartialEq,
|
||||
{
|
||||
pub fn get(&self, key: &K) -> Option<&V> {
|
||||
let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
// For non-serializable types, these simply return `None`.
|
||||
// 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.
|
||||
@@ -14,7 +15,7 @@ pub struct TypeId(u64);
|
||||
impl TypeId {
|
||||
#[inline]
|
||||
pub fn of<T: Any + 'static>() -> Self {
|
||||
std::any::TypeId::of::<T>().into()
|
||||
core::any::TypeId::of::<T>().into()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
@@ -23,9 +24,9 @@ impl TypeId {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::any::TypeId> for TypeId {
|
||||
impl From<core::any::TypeId> for TypeId {
|
||||
#[inline]
|
||||
fn from(id: std::any::TypeId) -> Self {
|
||||
fn from(id: core::any::TypeId) -> Self {
|
||||
Self(epaint::util::hash(id))
|
||||
}
|
||||
}
|
||||
@@ -113,8 +114,8 @@ impl Clone for Element {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Element {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for Element {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match &self {
|
||||
Self::Value { value, .. } => f
|
||||
.debug_struct("Element::Value")
|
||||
@@ -314,7 +315,7 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
|
||||
Err(_err) => {
|
||||
log::warn!(
|
||||
"egui: Failed to deserialize {} from memory: {}, ron error: {:?}",
|
||||
std::any::type_name::<T>(),
|
||||
core::any::type_name::<T>(),
|
||||
_err,
|
||||
ron
|
||||
);
|
||||
@@ -578,7 +579,7 @@ impl IdTypeMap {
|
||||
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
|
||||
let key = RawKey::new::<T>(id);
|
||||
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.
|
||||
|
||||
@@ -67,8 +67,8 @@ pub struct Undoer<State> {
|
||||
flux: Option<Flux<State>>,
|
||||
}
|
||||
|
||||
impl<State> std::fmt::Debug for Undoer<State> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl<State> core::fmt::Debug for Undoer<State> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self { undos, redos, .. } = self;
|
||||
f.debug_struct("Undoer")
|
||||
.field("undo count", &undos.len())
|
||||
|
||||
@@ -71,9 +71,8 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::{Pos2, Vec2};
|
||||
|
||||
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`,
|
||||
// which allows predicatable iteration order, frame-to-frame.
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -139,8 +138,8 @@ impl Default for ViewportId {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ViewportId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for ViewportId {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.0.short_debug_format().fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -199,8 +198,8 @@ impl IconData {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for IconData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl core::fmt::Debug for IconData {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("IconData")
|
||||
.field("width", &self.width)
|
||||
.field("height", &self.height)
|
||||
@@ -1276,7 +1275,7 @@ pub struct ViewportOutput {
|
||||
/// but if you haven't, you can use this instead.
|
||||
///
|
||||
/// If the duration is zero, schedule a repaint immediately.
|
||||
pub repaint_delay: std::time::Duration,
|
||||
pub repaint_delay: core::time::Duration,
|
||||
}
|
||||
|
||||
impl ViewportOutput {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user