1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 22:30:03 -04:00

Merge branch 'main' into lucas/experiments/helpful-id-debug

# Conflicts:
#	crates/egui/src/containers/combo_box.rs
#	crates/egui/src/id.rs
#	crates/egui/src/lib.rs
#	crates/egui/src/ui.rs
#	crates/egui/src/ui_builder.rs
This commit is contained in:
lucasmerlin
2026-02-17 17:13:12 +01:00
594 changed files with 22240 additions and 11164 deletions

View File

@@ -6,6 +6,38 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.3 - 2025-12-11
Nothing new
## 0.33.2 - 2025-11-13
Nothing new
## 0.33.0 - 2025-10-09
* Align `Color32` to 4 bytes [#7318](https://github.com/emilk/egui/pull/7318) by [@anti-social](https://github.com/anti-social)
* Make the `hex_color` macro `const` [#7444](https://github.com/emilk/egui/pull/7444) by [@YgorSouza](https://github.com/YgorSouza)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
Nothing new
## 0.32.0 - 2025-07-10
* Fix semi-transparent colors appearing too bright [#5824](https://github.com/emilk/egui/pull/5824) by [@emilk](https://github.com/emilk)
* Remove things that have been deprecated for over a year [#7099](https://github.com/emilk/egui/pull/7099) by [@emilk](https://github.com/emilk)
* Make `Hsva` derive serde [#7132](https://github.com/emilk/egui/pull/7132) by [@bircni](https://github.com/bircni)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -1,10 +1,7 @@
[package]
name = "ecolor"
version.workspace = true
authors = [
"Emil Ernerfeldt <emil.ernerfeldt@gmail.com>",
"Andreas Reich <reichandreas@gmx.de>",
]
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>", "Andreas Reich <reichandreas@gmx.de>"]
description = "Color structs and color conversion utilities"
edition.workspace = true
rust-version.workspace = true
@@ -39,10 +36,10 @@ emath.workspace = true
bytemuck = { workspace = true, optional = true, features = ["derive"] }
## [`cint`](https://docs.rs/cint) enables interoperability with other color libraries.
cint = { version = "0.3.1", optional = true }
cint = { workspace = true, optional = true }
## Enable the [`hex_color`] macro.
color-hex = { version = "0.2.0", optional = true }
color-hex = { workspace = true, optional = true }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }

View File

@@ -1,4 +1,4 @@
use super::{linear_f32_from_linear_u8, linear_u8_from_linear_f32, Color32, Hsva, HsvaGamma, Rgba};
use super::{Color32, Hsva, HsvaGamma, Rgba, linear_f32_from_linear_u8, linear_u8_from_linear_f32};
use cint::{Alpha, ColorInterop, EncodedSrgb, Hsv, LinearSrgb, PremultipliedAlpha};
// ---- Color32 ----

View File

@@ -1,4 +1,4 @@
use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
/// This format is used for space-efficient color representation (32 bits).
///
@@ -24,6 +24,7 @@ use crate::{fast_round, linear_f32_from_linear_u8, Rgba};
///
/// An `alpha=0` means the color is to be treated as an additive color.
#[repr(C)]
#[repr(align(4))]
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
@@ -102,9 +103,6 @@ impl Color32 {
/// i.e. often taken to mean "no color".
pub const PLACEHOLDER: Self = Self::from_rgba_premultiplied(64, 254, 0, 128);
#[deprecated = "Renamed to PLACEHOLDER"]
pub const TEMPORARY_COLOR: Self = Self::PLACEHOLDER;
/// From RGB with alpha of 255 (opaque).
#[inline]
pub const fn from_rgb(r: u8, g: u8, b: u8) -> Self {
@@ -159,6 +157,27 @@ impl Color32 {
}
}
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
///
/// It is slightly slower when operating on non-const data.
#[inline]
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
match a {
// common-case optimization:
0 => Self::TRANSPARENT,
// common-case optimization:
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));
Self::from_rgba_premultiplied(r, g, b, a)
}
}
}
/// Opaque gray.
#[doc(alias = "from_grey")]
#[inline]
@@ -504,9 +523,11 @@ mod test {
fn to_from_rgba() {
for [r, g, b, a] in test_rgba() {
let original = Color32::from_rgba_unmultiplied(r, g, b, a);
let constfn = Color32::from_rgba_unmultiplied_const(r, g, b, a);
let rgba = Rgba::from(original);
let back = Color32::from(rgba);
assert_eq!(back, original);
assert_eq!(constfn, original);
}
assert_eq!(

View File

@@ -31,10 +31,11 @@
/// let _ = ecolor::hex_color!("#20212x");
/// ```
///
/// The macro cannot be used in a `const` context.
/// The macro can be used in a `const` context.
///
/// ```compile_fail
/// ```
/// const COLOR: ecolor::Color32 = ecolor::hex_color!("#202122");
/// assert_eq!(COLOR, ecolor::Color32::from_rgb(0x20, 0x21, 0x22));
/// ```
#[macro_export]
macro_rules! hex_color {
@@ -42,7 +43,7 @@ macro_rules! hex_color {
let array = $crate::color_hex::color_from_hex!($s);
match array.as_slice() {
[r, g, b] => $crate::Color32::from_rgb(*r, *g, *b),
[r, g, b, a] => $crate::Color32::from_rgba_unmultiplied(*r, *g, *b, *a),
[r, g, b, a] => $crate::Color32::from_rgba_unmultiplied_const(*r, *g, *b, *a),
_ => panic!("Invalid hex color length: expected 3 (RGB) or 4 (RGBA) bytes"),
}
}};

View File

@@ -1,9 +1,10 @@
use crate::{
gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32, Color32, Rgba,
Color32, Rgba, gamma_u8_from_linear_f32, linear_f32_from_gamma_u8, linear_u8_from_linear_f32,
};
/// Hue, saturation, value, alpha. All in the range [0, 1].
/// No premultiplied alpha.
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Hsva {
/// hue 0-1
@@ -40,7 +41,7 @@ impl Hsva {
/// From linear RGBA with premultiplied alpha
#[inline]
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
#![expect(clippy::many_single_char_names)]
if a <= 0.0 {
if r == 0.0 && b == 0.0 && a == 0.0 {
Self::default()
@@ -56,7 +57,7 @@ impl Hsva {
/// From linear RGBA without premultiplied alpha
#[inline]
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![allow(clippy::many_single_char_names)]
#![expect(clippy::many_single_char_names)]
let (h, s, v) = hsv_from_rgb([r, g, b]);
Self { h, s, v, a }
}
@@ -188,7 +189,7 @@ 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) {
#![allow(clippy::many_single_char_names)]
#![expect(clippy::many_single_char_names)]
let min = r.min(g.min(b));
let max = r.max(g.max(b)); // value
@@ -212,7 +213,7 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
/// All ranges in 0-1, rgb is linear.
#[inline]
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
#![allow(clippy::many_single_char_names)]
#![expect(clippy::many_single_char_names)]
let h = (h.fract() + 1.0).fract(); // wrap
let s = s.clamp(0.0, 1.0);
@@ -233,7 +234,7 @@ pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
}
#[test]
#[ignore] // a bit expensive
#[ignore = "too expensive"]
fn test_hsv_roundtrip() {
for r in 0..=255 {
for g in 0..=255 {

View File

@@ -1,4 +1,4 @@
use crate::{gamma_from_linear, linear_from_gamma, Color32, Hsva, Rgba};
use crate::{Color32, Hsva, Rgba, gamma_from_linear, linear_from_gamma};
/// Like Hsva but with the `v` value (brightness) being gamma corrected
/// so that it is somewhat perceptually even.

View File

@@ -19,7 +19,7 @@
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::wrong_self_convention)]
#![expect(clippy::wrong_self_convention)]
#[cfg(feature = "cint")]
mod cint_impl;
@@ -105,7 +105,7 @@ pub fn linear_f32_from_gamma_u8(s: u8) -> f32 {
/// linear [0, 255] -> linear [0, 1].
/// Useful for alpha-channel.
#[inline(always)]
pub fn linear_f32_from_linear_u8(a: u8) -> f32 {
pub const fn linear_f32_from_linear_u8(a: u8) -> f32 {
a as f32 / 255.0
}
@@ -130,7 +130,7 @@ pub fn linear_u8_from_linear_f32(a: f32) -> u8 {
fast_round(a * 255.0)
}
fn fast_round(r: f32) -> u8 {
const fn fast_round(r: f32) -> u8 {
(r + 0.5) as _ // rust does a saturating cast since 1.45
}

View File

@@ -33,12 +33,11 @@ pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
} else if f.is_nan() {
state.write_u8(1);
} else {
use std::hash::Hash;
use std::hash::Hash as _;
f.to_bits().hash(state);
}
}
#[allow(clippy::derived_hash_with_manual_eq)]
impl std::hash::Hash for Rgba {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {

View File

@@ -7,6 +7,73 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.3 - 2025-12-11
Nothing new
## 0.33.2 - 2025-11-13
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
* Make sure `native_pixels_per_point` is set during app creation [#7683](https://github.com/emilk/egui/pull/7683) by [@emilk](https://github.com/emilk)
## 0.33.0 - 2025-10-09
### ⭐ Added
* Add an option to limit the repaint rate in the web runner [#7482](https://github.com/emilk/egui/pull/7482) by [@s-nie](https://github.com/s-nie)
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
### 🔧 Changed
* Replace `winapi` with `windows-sys` crate [#7416](https://github.com/emilk/egui/pull/7416) by [@unlimitedsola](https://github.com/unlimitedsola)
* Prevent default action on command-comma in eframe web [#7547](https://github.com/emilk/egui/pull/7547) by [@emilk](https://github.com/emilk)
* Warn if `DYLD_LIBRARY_PATH` is set and we find no wgpu adapter [#7572](https://github.com/emilk/egui/pull/7572) by [@emilk](https://github.com/emilk)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
### 🐛 Fixed
* Properly end winit event loop [#7565](https://github.com/emilk/egui/pull/7565) by [@tye-exe](https://github.com/tye-exe)
* Fix eframe window not being focused on mac on startup [#7593](https://github.com/emilk/egui/pull/7593) by [@emilk](https://github.com/emilk)
* Fix black flash on start in glow eframe backend [#7616](https://github.com/emilk/egui/pull/7616) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
* Enable wgpu default features in eframe / egui_wgpu default features [#7344](https://github.com/emilk/egui/pull/7344) by [@lucasmerlin](https://github.com/lucasmerlin)
* Request a redraw when the url change through the `popstate` event listener [#7403](https://github.com/emilk/egui/pull/7403) by [@irevoire](https://github.com/irevoire)
## 0.32.0 - 2025-07-10
### ⭐ Added
* Add pointer events and focus handling for apps run in a Shadow DOM [#5627](https://github.com/emilk/egui/pull/5627) by [@xxvvii](https://github.com/xxvvii)
* MacOS: Add `movable_by_window_background` option to viewport [#5412](https://github.com/emilk/egui/pull/5412) by [@jim-ec](https://github.com/jim-ec)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Add external eventloop support [#6750](https://github.com/emilk/egui/pull/6750) by [@wpbrown](https://github.com/wpbrown)
### 🔧 Changed
* Update MSRV to 1.85 [#7279](https://github.com/emilk/egui/pull/7279) by [@emilk](https://github.com/emilk)
* Use Rust edition 2024 [#7280](https://github.com/emilk/egui/pull/7280) by [@emilk](https://github.com/emilk)
* Rename `should_propagate_event` and add `should_prevent_default` [#5779](https://github.com/emilk/egui/pull/5779) by [@th0rex](https://github.com/th0rex)
* Clarify platform-specific details for `Viewport` positioning [#5715](https://github.com/emilk/egui/pull/5715) by [@aspiringLich](https://github.com/aspiringLich)
* Enhance stability on Windows [#5723](https://github.com/emilk/egui/pull/5723) by [@rustbasic](https://github.com/rustbasic)
* Set `web-sys` min version to `0.3.73` [#5862](https://github.com/emilk/egui/pull/5862) by [@wareya](https://github.com/wareya)
* Bump `ron` to `0.10.1` [#6861](https://github.com/emilk/egui/pull/6861) by [@torokati44](https://github.com/torokati44)
* Disallow `accesskit` on Android NativeActivity, making `hello_android` working again [#6855](https://github.com/emilk/egui/pull/6855) by [@podusowski](https://github.com/podusowski)
* Respect and detect `prefers-color-scheme: no-preference` [#7293](https://github.com/emilk/egui/pull/7293) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Mark all keys as up if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Fix text distortion on mobile devices/browsers with `glow` backend [#6893](https://github.com/emilk/egui/pull/6893) by [@wareya](https://github.com/wareya)
* Workaround libpng crash on macOS by not creating `NSImage` from png data [#7252](https://github.com/emilk/egui/pull/7252) by [@Wumpf](https://github.com/Wumpf)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
* Web: only consume copy/cut events if the canvas has focus [#7270](https://github.com/emilk/egui/pull/7270) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -5,19 +5,13 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "egui framework - write GUI apps that compiles to web and/or natively"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/eframe"
homepage = "https://github.com/emilk/egui/tree/main/crates/eframe"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/eframe"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
"data/icon.png",
]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
[package.metadata.docs.rs]
all-features = true
@@ -34,16 +28,15 @@ workspace = true
default = [
"accesskit",
"default_fonts",
"glow",
"wayland", # Required for Linux support (including CI!)
"wayland", # Required for Linux support (including CI!)
"web_screen_reader",
"wgpu",
"winit/default",
"x11",
"egui-wgpu?/fragile-send-sync-non-atomic-wasm",
]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["egui/accesskit", "egui-winit/accesskit"]
accesskit = ["egui-winit/accesskit"]
# Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can
@@ -59,17 +52,15 @@ android-native-activity = ["egui-winit/android-native-activity"]
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
## Use [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow).
## Enable [`glow`](https://github.com/grovesNL/glow) for painting, via [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow).
##
## There is generally no need to enable both the `wgpu` and `glow` features,
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
## and `WebOptions::renderer`.
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
persistence = [
"dep:home",
"egui-winit/serde",
"egui/persistence",
"ron",
"serde",
]
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
## Enables wayland support and fixes clipboard issue.
##
@@ -85,25 +76,30 @@ wayland = [
## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web.
##
## For other platforms, use the `accesskit` feature instead.
web_screen_reader = [
"web-sys/SpeechSynthesis",
"web-sys/SpeechSynthesisUtterance",
]
web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtterance"]
## Use [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/master/crates/egui-wgpu)).
## Enable [`wgpu`](https://docs.rs/wgpu) for painting (via [`egui-wgpu`](https://github.com/emilk/egui/tree/main/crates/egui-wgpu)).
##
## This overrides the `glow` feature.
## There is generally no need to enable both the `wgpu` and `glow` features,
## but if you do you can pick the renderer to use with [`NativeOptions::renderer`]
## and `WebOptions::renderer`.
##
## By default, only WebGPU is enabled on web.
## If you want to enable WebGL, you need to turn on the `webgl` feature of crate `wgpu`:
##
## ```toml
## wgpu = { version = "*", features = ["webgpu", "webgl"] }
## ```
## Switching from `wgpu (the default)` to `glow` can significantly reduce your binary size
## (including the .wasm of a web app).
## See <https://github.com/emilk/egui/issues/5889> for more details.
##
## By default, eframe will prefer WebGPU over WebGL, but
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
wgpu = ["wgpu_no_default_features", "egui-wgpu/default"]
## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`.
##
## This means that no `wgpu` backends are enabled. You will need to enable them yourself, e.g. like this:
##
## ```toml
## wgpu = { version = "*", features = ["dx12", "metal", "webgl"] }
## ```
wgpu_no_default_features = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"]
## Enables compiling for x11.
x11 = [
@@ -121,10 +117,7 @@ x11 = [
__screenshot = []
[dependencies]
egui = { workspace = true, default-features = false, features = [
"bytemuck",
"log",
] }
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
ahash.workspace = true
document-features.workspace = true
@@ -132,7 +125,7 @@ log.workspace = true
parking_lot.workspace = true
profiling.workspace = true
raw-window-handle.workspace = true
static_assertions = "1.1.0"
static_assertions.workspace = true
web-time.workspace = true
# Optional dependencies
@@ -145,45 +138,35 @@ 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", "links"] }
image = { workspace = true, features = ["png"] } # Needed for app icon
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
# optional native:
egui-wgpu = { workspace = true, optional = true, features = [
"winit",
"capture",
] } # if wgpu is used, use it with winit
pollster = { workspace = true, optional = true } # needed for wgpu
glutin = { workspace = true, optional = true, default-features = false, features = [
"egl",
"wgl",
] }
glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] }
glutin-winit = { workspace = true, optional = true, default-features = false, features = [
"egl",
"wgl",
] }
home = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true, features = [
# Let's enable some backends so that users can use `eframe` out-of-the-box
# without having to explicitly opt-in to backends
"metal",
"webgpu",
] }
wgpu = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
objc2 = "0.5.1"
objc2-foundation = { version = "0.2.0", default-features = false, features = [
objc2.workspace = true
objc2-foundation = { workspace = true, default-features = false, features = [
"std",
"block2",
"NSData",
"NSString",
] }
objc2-app-kit = { version = "0.2.0", default-features = false, features = [
objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
"NSImage",
@@ -194,11 +177,12 @@ objc2-app-kit = { version = "0.2.0", default-features = false, features = [
# windows:
[target.'cfg(any(target_os = "windows"))'.dependencies]
winapi = { version = "0.3.9", features = ["winuser"] }
windows-sys = { workspace = true, features = [
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_System_Com",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
# -------------------------------------------
@@ -206,8 +190,8 @@ windows-sys = { workspace = true, features = [
[target.'cfg(target_arch = "wasm32")'.dependencies]
bytemuck.workspace = true
image = { workspace = true, features = ["png"] } # For copying images
js-sys = "0.3"
percent-encoding = "2.1"
js-sys.workspace = true
percent-encoding.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
web-sys = { workspace = true, features = [
@@ -267,13 +251,12 @@ web-sys = { workspace = true, features = [
] }
# optional web:
egui-wgpu = { workspace = true, optional = true } # if wgpu is used, use it without (!) winit
wgpu = { workspace = true, optional = true, features = [
# Let's enable some backends so that users can use `eframe` out-of-the-box
# without having to explicitly opt-in to backends
"webgpu",
egui-wgpu = { workspace = true, optional = true, features = [
# if wgpu is used, use it without (!) winit:
"capture",
] }
wgpu = { workspace = true, optional = true }
# Native dev dependencies for testing
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
directories = "5"
directories.workspace = true

View File

@@ -7,7 +7,7 @@
`eframe` is the official framework library for writing apps using [`egui`](https://github.com/emilk/egui). The app can be compiled both to run natively (for Linux, Mac, Windows, and Android) or as a web app (using [Wasm](https://en.wikipedia.org/wiki/WebAssembly)).
To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
There is also a tutorial video at <https://www.youtube.com/watch?v=NtUkr_z7l84>.
@@ -16,7 +16,7 @@ For how to use `egui`, see [the egui docs](https://docs.rs/egui).
---
`eframe` uses [`egui_glow`](https://github.com/emilk/egui/tree/master/crates/egui_glow) for rendering, and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/master/crates/egui-winit).
`eframe` defaults to using [wgpu](https://crates.io/crates/wgpu) for rendering (with an option to change to [glow](https://crates.io/crates/glow)), and on native it uses [`egui-winit`](https://github.com/emilk/egui/tree/main/crates/egui-winit).
To use on Linux, first run:
@@ -24,18 +24,18 @@ To use on Linux, first run:
sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
You need to either use `edition = "2021"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You need to either use `edition = "2024"`, or set `resolver = "2"` in the `[workspace]` section of your to-level `Cargo.toml`. See [this link](https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html) for more info.
You can opt-in to the using [`egui_wgpu`](https://github.com/emilk/egui/tree/master/crates/egui_wgpu) for rendering by enabling the `wgpu` feature and setting `NativeOptions::renderer` to `Renderer::Wgpu`.
You can opt-in to the using [`egui_glow`](https://github.com/emilk/egui/tree/main/crates/egui_glow) for rendering by enabling the `glow` feature and setting `NativeOptions::renderer` to `Renderer::Glow`.
## Alternatives
`eframe` is not the only way to write an app using `egui`! You can also try [`egui-miniquad`](https://github.com/not-fl3/egui-miniquad), [`bevy_egui`](https://github.com/mvlabat/bevy_egui), [`egui_sdl2_gl`](https://github.com/ArjunNair/egui_sdl2_gl), and others.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/master/crates/egui_glow/examples/pure_glow.rs>.
You can also use `egui_glow` and [`winit`](https://github.com/rust-windowing/winit) to build your own app as demonstrated in <https://github.com/emilk/egui/blob/main/crates/egui_glow/examples/pure_glow.rs>.
## Limitations when running egui on the web
`eframe` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and Wasm, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
`eframe` and egui compiles to Wasm using either WebGPU (when available) or WebGL2 for rendering, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
* Rendering: Getting pixel-perfect rendering right on the web is very difficult.
* Search: you cannot search an egui web page like you would a normal web page.

View File

@@ -10,7 +10,7 @@
use std::any::Any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use crate::native::winit_integration::UserEvent;
#[cfg(not(target_arch = "wasm32"))]
@@ -22,7 +22,7 @@ use raw_window_handle::{
use static_assertions::assert_not_impl_any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
/// Hook into the building of an event loop before it is run
@@ -30,7 +30,7 @@ pub use winit::{event_loop::EventLoopBuilder, window::WindowAttributes};
/// You can configure any platform specific details required on top of the default configuration
/// done by `EFrame`.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)>;
/// Hook into the building of a the native window.
@@ -38,7 +38,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
/// You can configure any platform specific details required on top of the default configuration
/// done by `eframe`.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
type DynError = Box<dyn std::error::Error + Send + Sync>;
@@ -79,7 +79,7 @@ pub struct CreationContext<'s> {
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
///
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
/// Raw platform window handle
@@ -91,7 +91,7 @@ pub struct CreationContext<'s> {
pub(crate) raw_display_handle: Result<RawDisplayHandle, HandleError>,
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for CreationContext<'_> {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -100,7 +100,7 @@ impl HasWindowHandle for CreationContext<'_> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for CreationContext<'_> {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -121,7 +121,7 @@ impl CreationContext<'_> {
gl: None,
#[cfg(feature = "glow")]
get_proc_address: None,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
@@ -133,11 +133,36 @@ impl CreationContext<'_> {
// ----------------------------------------------------------------------------
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe).
/// Implement this trait to write apps that can be compiled for both web/wasm and desktop/native using [`eframe`](https://github.com/emilk/egui/tree/main/crates/eframe).
pub trait App {
/// Called once before each call to [`Self::ui`],
/// and additionally also called when the UI is hidden, but [`egui::Context::request_repaint`] was called.
///
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
///
/// 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).
fn logic(&mut self, ctx: &egui::Context, frame: &mut Frame) {
_ = (ctx, frame);
}
/// Called each time the UI needs repainting, which may be many times per second.
///
/// Put your widgets into a [`egui::SidePanel`], [`egui::TopBottomPanel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`].
/// The given [`egui::Ui`] has no margin or background color.
/// You can wrap your UI code in [`egui::CentralPanel`] or a [`egui::Frame::central_panel`] to remedy this.
///
/// The [`egui::Ui::ctx`] can be cloned and saved if you like.
/// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
///
/// This is called for the root viewport ([`egui::ViewportId::ROOT`]).
/// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows).
/// (A "viewport" in egui means an native OS window).
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut Frame);
/// Called each time the UI needs repainting, which may be many times per second.
///
/// Put your widgets into a [`egui::Panel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`].
///
/// The [`egui::Context`] can be cloned and saved if you like.
///
@@ -146,7 +171,10 @@ pub trait App {
/// This is called for the root viewport ([`egui::ViewportId::ROOT`]).
/// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows).
/// (A "viewport" in egui means an native OS window).
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame);
#[deprecated = "Use Self::ui instead"]
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
_ = (ctx, frame);
}
/// Get a handle to the app.
///
@@ -317,7 +345,7 @@ pub struct NativeOptions {
pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub renderer: Renderer,
/// This controls what happens when you close the main eframe window.
@@ -340,7 +368,7 @@ pub struct NativeOptions {
/// event loop before it is run.
///
/// Note: A [`NativeOptions`] clone will not include any `event_loop_builder` hook.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub event_loop_builder: Option<EventLoopBuilderHook>,
/// Hook into the building of a window.
@@ -349,7 +377,7 @@ pub struct NativeOptions {
/// window appearance.
///
/// Note: A [`NativeOptions`] clone will not include any `window_builder` hook.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub window_builder: Option<WindowBuilderHook>,
#[cfg(feature = "glow")]
@@ -367,7 +395,7 @@ pub struct NativeOptions {
pub centered: bool,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
/// Controls whether or not the native window position and size will be
@@ -404,13 +432,13 @@ impl Clone for NativeOptions {
Self {
viewport: self.viewport.clone(),
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
event_loop_builder: None, // Skip any builder callbacks if cloning
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None, // Skip any builder callbacks if cloning
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: self.wgpu_options.clone(),
persistence_path: self.persistence_path.clone(),
@@ -435,15 +463,15 @@ impl Default for NativeOptions {
stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
renderer: Renderer::default(),
run_and_return: true,
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
event_loop_builder: None,
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None,
#[cfg(feature = "glow")]
@@ -451,7 +479,7 @@ impl Default for NativeOptions {
centered: false,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
persist_window: true,
@@ -471,6 +499,10 @@ impl Default for NativeOptions {
/// Options when using `eframe` in a web page.
#[cfg(target_arch = "wasm32")]
pub struct WebOptions {
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub renderer: Renderer,
/// Sets the number of bits in the depth buffer.
///
/// `egui` doesn't need the depth buffer, so the default value is 0.
@@ -484,7 +516,7 @@ pub struct WebOptions {
pub webgl_context_option: WebGlContextOption,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
/// Controls whether to apply dithering to minimize banding artifacts.
@@ -499,27 +531,43 @@ pub struct WebOptions {
/// If the web event corresponding to an egui event should be propagated
/// to the rest of the web page.
///
/// The default is `false`, meaning
/// The default is `true`, meaning
/// [`stopPropagation`](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
/// is called on every event.
pub should_propagate_event: Box<dyn Fn(&egui::Event) -> bool>,
/// is called on every event, and the event is not propagated to the rest of the web page.
pub should_stop_propagation: Box<dyn Fn(&egui::Event) -> bool>,
/// Whether the web event corresponding to an egui event should have `prevent_default` called
/// on it or not.
///
/// Defaults to true.
pub should_prevent_default: Box<dyn Fn(&egui::Event) -> bool>,
/// Maximum rate at which to repaint. This can be used to artificially reduce the repaint rate below
/// vsync in order to save resources.
pub max_fps: Option<u32>,
}
#[cfg(target_arch = "wasm32")]
impl Default for WebOptions {
fn default() -> Self {
Self {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
renderer: Renderer::default(),
depth_buffer: 0,
#[cfg(feature = "glow")]
webgl_context_option: WebGlContextOption::BestFirst,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
dithering: true,
should_propagate_event: Box::new(|_| false),
should_stop_propagation: Box::new(|_| true),
should_prevent_default: Box::new(|_| true),
max_fps: None,
}
}
}
@@ -548,7 +596,7 @@ pub enum WebGlContextOption {
/// What rendering backend to use.
///
/// You need to enable the "glow" and "wgpu" features to have a choice.
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@@ -558,47 +606,49 @@ pub enum Renderer {
Glow,
/// Use [`egui_wgpu`] renderer for [`wgpu`](https://github.com/gfx-rs/wgpu).
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Wgpu,
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl Default for Renderer {
fn default() -> Self {
#[cfg(not(feature = "glow"))]
#[cfg(not(feature = "wgpu"))]
compile_error!("eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'");
#[cfg(not(feature = "wgpu_no_default_features"))]
compile_error!(
"eframe: you must enable at least one of the rendering backend features: 'glow' or 'wgpu'"
);
#[cfg(feature = "glow")]
#[cfg(not(feature = "wgpu"))]
#[cfg(not(feature = "wgpu_no_default_features"))]
return Self::Glow;
#[cfg(not(feature = "glow"))]
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
return Self::Wgpu;
// By default, only the `glow` feature is enabled, so if the user added `wgpu` to the feature list
// they probably wanted to use wgpu:
// It's weird that the user has enabled both glow and wgpu,
// but let's pick the better of the two (wgpu):
#[cfg(feature = "glow")]
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
return Self::Wgpu;
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "glow")]
Self::Glow => "glow".fmt(f),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Self::Wgpu => "wgpu".fmt(f),
}
}
}
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::str::FromStr for Renderer {
type Err = String;
@@ -607,10 +657,12 @@ impl std::str::FromStr for Renderer {
#[cfg(feature = "glow")]
"glow" => Ok(Self::Glow),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
"wgpu" => Ok(Self::Wgpu),
_ => Err(format!("eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."))
_ => Err(format!(
"eframe renderer {name:?} is not available. Make sure that the corresponding eframe feature is enabled."
)),
}
}
}
@@ -638,7 +690,7 @@ pub struct Frame {
Option<Box<dyn FnMut(glow::Texture) -> egui::TextureId>>,
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
#[doc(hidden)]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
@@ -655,7 +707,7 @@ pub struct Frame {
#[cfg(not(target_arch = "wasm32"))]
assert_not_impl_any!(Frame: Clone);
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasWindowHandle for Frame {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
@@ -664,7 +716,7 @@ impl HasWindowHandle for Frame {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
#[cfg(not(target_arch = "wasm32"))]
impl HasDisplayHandle for Frame {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
@@ -688,7 +740,7 @@ impl Frame {
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
storage: None,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
}
}
@@ -696,7 +748,7 @@ impl Frame {
/// True if you are in a web environment.
///
/// Equivalent to `cfg!(target_arch = "wasm32")`
#[allow(clippy::unused_self)]
#[expect(clippy::unused_self)]
pub fn is_web(&self) -> bool {
cfg!(target_arch = "wasm32")
}
@@ -739,6 +791,7 @@ impl Frame {
/// This function will take the ownership of your [`glow::Texture`], so please do not delete your [`glow::Texture`] after registering.
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
pub fn register_native_glow_texture(&mut self, native: glow::Texture) -> egui::TextureId {
#[expect(clippy::unwrap_used)]
self.glow_register_native_texture.as_mut().unwrap()(native)
}
@@ -747,7 +800,7 @@ impl Frame {
/// Only available when compiling with the `wgpu` feature and using [`Renderer::Wgpu`].
///
/// Can be used to manage GPU resources for custom rendering with WGPU using [`egui::PaintCallback`]s.
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
self.wgpu_render_state.as_ref()
}
@@ -882,16 +935,15 @@ pub trait Storage {
#[cfg(feature = "ron")]
pub fn get_value<T: serde::de::DeserializeOwned>(storage: &dyn Storage, key: &str) -> Option<T> {
profiling::function_scope!(key);
storage
.get_string(key)
.and_then(|value| match ron::from_str(&value) {
Ok(value) => Some(value),
Err(err) => {
// This happens on when we break the format, e.g. when updating egui.
log::debug!("Failed to decode RON: {err}");
None
}
})
let value = storage.get_string(key)?;
match ron::from_str(&value) {
Ok(value) => Some(value),
Err(err) => {
// This happens on when we break the format, e.g. when updating egui.
log::debug!("Failed to decode RON: {err}");
None
}
}
}
/// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key.
@@ -900,7 +952,7 @@ pub fn set_value<T: serde::Serialize>(storage: &mut dyn Storage, key: &str, valu
profiling::function_scope!(key);
match ron::ser::to_string(value) {
Ok(string) => storage.set_string(key, string),
Err(err) => log::error!("eframe failed to encode data using ron: {}", err),
Err(err) => log::error!("eframe failed to encode data using ron: {err}"),
}
}

View File

@@ -3,7 +3,7 @@
//! If you are planning to write an app for web or native,
//! and want to use [`egui`] for everything, then `eframe` is for you!
//!
//! To get started, see the [examples](https://github.com/emilk/egui/tree/master/examples).
//! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples).
//! To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!
//!
//! In short, you implement [`App`] (especially [`App::update`]) and then
@@ -35,7 +35,7 @@
//!
//! impl MyEguiApp {
//! fn new(cc: &eframe::CreationContext<'_>) -> Self {
//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
//! // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style.
//! // Restore app state using cc.storage (requires the "persistence" feature).
//! // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
//! // for e.g. egui::PaintCallback.
@@ -44,8 +44,8 @@
//! }
//!
//! impl eframe::App for MyEguiApp {
//! fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
//! egui::CentralPanel::default().show(ctx, |ui| {
//! fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
//! egui::CentralPanel::default().show_inside(ui, |ui| {
//! ui.heading("Hello World!");
//! });
//! }
@@ -69,7 +69,7 @@
//! #[wasm_bindgen]
//! impl WebHandle {
//! /// Installs a panic hook, then returns.
//! #[allow(clippy::new_without_default)]
//! #[expect(clippy::new_without_default)]
//! #[wasm_bindgen(constructor)]
//! pub fn new() -> Self {
//! // Redirect [`log`] message to `console.log` and friends:
@@ -142,7 +142,15 @@
//!
#![warn(missing_docs)] // let's keep eframe well-documented
#![allow(clippy::needless_doctest_main)]
// Limitation imposed by `accesskit_winit`:
// https://github.com/AccessKit/accesskit/tree/accesskit-v0.18.0/platforms/winit#android-activity-compatibility`
#[cfg(all(
target_os = "android",
feature = "accesskit",
feature = "android-native-activity"
))]
compile_error!("`accesskit` feature is only available with `android-game-activity`");
// Re-export all useful libraries:
pub use {egui, egui::emath, egui::epaint};
@@ -150,7 +158,7 @@ pub use {egui, egui::emath, egui::epaint};
#[cfg(feature = "glow")]
pub use {egui_glow, glow};
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub use {egui_wgpu, wgpu};
mod epi;
@@ -179,11 +187,19 @@ pub use web::{WebLogger, WebRunner};
// When compiling natively
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
mod native;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframeWinitApplication;
#[cfg(not(any(target_arch = "wasm32", target_os = "ios")))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframePumpStatus;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[cfg(feature = "persistence")]
pub use native::file_storage::storage_dir;
@@ -192,7 +208,7 @@ pub mod icon_data;
/// This is how you start a native (desktop) app.
///
/// The first argument is name of your app, which is a an identifier
/// The first argument is name of your app, which is an identifier
/// used for the save location of persistence (see [`App::save`]).
/// It is also used as the application id on wayland.
/// If you set no title on the viewport, the app id will be used
@@ -215,7 +231,7 @@ pub mod icon_data;
///
/// impl MyEguiApp {
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_visuals.
/// // Customize egui here with cc.egui_ctx.set_fonts and cc.egui_ctx.set_global_style.
/// // Restore app state using cc.storage (requires the "persistence" feature).
/// // Use the cc.gl (a glow::Context) to create graphics shaders and buffers that you can use
/// // for e.g. egui::PaintCallback.
@@ -224,8 +240,8 @@ pub mod icon_data;
/// }
///
/// impl eframe::App for MyEguiApp {
/// fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show(ctx, |ui| {
/// fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show_inside(ui, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
@@ -235,13 +251,113 @@ pub mod icon_data;
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[allow(clippy::needless_pass_by_value)]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)]
pub fn run_native(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'_>,
) -> Result {
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu_no_default_features")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
}
/// Provides a proxy for your native eframe application to run on your own event loop.
///
/// See `run_native` for details about `app_name`.
///
/// Call from `fn main` like this:
/// ``` no_run
/// use eframe::{egui, UserEvent};
/// use winit::event_loop::{ControlFlow, EventLoop};
///
/// fn main() -> eframe::Result {
/// let native_options = eframe::NativeOptions::default();
/// let eventloop = EventLoop::<UserEvent>::with_user_event().build()?;
/// eventloop.set_control_flow(ControlFlow::Poll);
///
/// let mut winit_app = eframe::create_native(
/// "MyExtApp",
/// native_options,
/// Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))),
/// &eventloop,
/// );
///
/// eventloop.run_app(&mut winit_app)?;
///
/// Ok(())
/// }
///
/// #[derive(Default)]
/// struct MyEguiApp {}
///
/// impl MyEguiApp {
/// fn new(cc: &eframe::CreationContext<'_>) -> Self {
/// Self::default()
/// }
/// }
///
/// impl eframe::App for MyEguiApp {
/// fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show_inside(ui, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
/// }
/// ```
///
/// See the `external_eventloop` example for a more complete example.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn create_native<'a>(
app_name: &str,
mut native_options: NativeOptions,
app_creator: AppCreator<'a>,
event_loop: &winit::event_loop::EventLoop<UserEvent>,
) -> EframeWinitApplication<'a> {
let renderer = init_native(app_name, &mut native_options);
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
EframeWinitApplication::new(native::run::create_glow(
app_name,
native_options,
app_creator,
event_loop,
))
}
#[cfg(feature = "wgpu_no_default_features")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
EframeWinitApplication::new(native::run::create_wgpu(
app_name,
native_options,
app_creator,
event_loop,
))
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
#[cfg(not(feature = "__screenshot"))]
assert!(
std::env::var("EFRAME_SCREENSHOT_TO").is_err(),
@@ -254,32 +370,79 @@ pub fn run_native(
let renderer = native_options.renderer;
#[cfg(all(feature = "glow", feature = "wgpu"))]
#[cfg(all(feature = "glow", feature = "wgpu_no_default_features"))]
{
match renderer {
match native_options.renderer {
Renderer::Glow => "glow",
Renderer::Wgpu => "wgpu",
};
log::info!("Both the glow and wgpu renderers are available. Using {renderer}.");
}
match renderer {
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
renderer
}
// ----------------------------------------------------------------------------
/// The simplest way to get started when writing a native app.
///
/// This does NOT support persistence of custom user data. For that you need to use [`run_native`].
/// However, it DOES support persistence of egui data (window positions and sizes, how far the user has scrolled in a
/// [`ScrollArea`](egui::ScrollArea), etc.) if the persistence feature is enabled.
///
/// # Example
/// ``` no_run
/// fn main() -> eframe::Result {
/// // Our application state:
/// let mut name = "Arthur".to_owned();
/// let mut age = 42;
///
/// let options = eframe::NativeOptions::default();
/// eframe::run_ui_native("My egui App", options, move |ui, _frame| {
/// // Wrap everything in a CentralPanel so we get some margins and a background color:
/// egui::CentralPanel::default().show_inside(ui, |ui| {
/// ui.heading("My egui Application");
/// ui.horizontal(|ui| {
/// let name_label = ui.label("Your name: ");
/// ui.text_edit_singleline(&mut name)
/// .labelled_by(name_label.id);
/// });
/// ui.add(egui::Slider::new(&mut age, 0..=120).text("age"));
/// if ui.button("Increment").clicked() {
/// age += 1;
/// }
/// ui.label(format!("Hello '{name}', age {age}"));
/// });
/// })
/// }
/// ```
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn run_ui_native(
app_name: &str,
native_options: NativeOptions,
ui_fun: impl FnMut(&mut egui::Ui, &mut Frame) + 'static,
) -> Result {
struct SimpleApp<U> {
ui_fun: U,
}
impl<U: FnMut(&mut egui::Ui, &mut Frame) + 'static> App for SimpleApp<U> {
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut Frame) {
(self.ui_fun)(ui, frame);
}
}
run_native(
app_name,
native_options,
Box::new(|_cc| Ok(Box::new(SimpleApp { ui_fun }))),
)
}
/// The simplest way to get started when writing a native app.
///
/// This does NOT support persistence of custom user data. For that you need to use [`run_native`].
@@ -314,8 +477,9 @@ pub fn run_native(
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[deprecated = "Use run_ui_native instead"]
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn run_simple_native(
app_name: &str,
native_options: NativeOptions,
@@ -326,6 +490,8 @@ pub fn run_simple_native(
}
impl<U: FnMut(&egui::Context, &mut Frame) + 'static> App for SimpleApp<U> {
fn ui(&mut self, _ui: &mut egui::Ui, _frame: &mut Frame) {}
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
(self.update_fun)(ctx, frame);
}
@@ -367,7 +533,7 @@ pub enum Error {
OpenGL(egui_glow::PainterError),
/// An error from [`wgpu`].
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Wgpu(egui_wgpu::WgpuError),
}
@@ -405,7 +571,7 @@ impl From<egui_glow::PainterError> for Error {
}
}
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
impl From<egui_wgpu::WgpuError> for Error {
#[inline]
fn from(err: egui_wgpu::WgpuError) -> Self {
@@ -446,7 +612,7 @@ impl std::fmt::Display for Error {
write!(f, "egui_glow: {err}")
}
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
Self::Wgpu(err) => {
write!(f, "WGPU error: {err}")
}

View File

@@ -14,10 +14,10 @@ pub struct AppTitleIconSetter {
impl AppTitleIconSetter {
pub fn new(title: String, mut icon_data: Option<Arc<IconData>>) -> Self {
if let Some(icon) = &icon_data {
if **icon == IconData::default() {
icon_data = None;
}
if let Some(icon) = &icon_data
&& **icon == IconData::default()
{
icon_data = None;
}
Self {
@@ -47,7 +47,7 @@ enum AppIconStatus {
NotSetTryAgain,
/// We successfully set the icon and it should be visible now.
#[allow(dead_code)] // Not used on Linux
#[allow(clippy::allow_attributes, dead_code)] // Not used on Linux
Set,
}
@@ -71,16 +71,20 @@ fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconSta
#[cfg(target_os = "macos")]
return set_title_and_icon_mac(_title, _icon_data);
#[allow(unreachable_code)]
#[allow(clippy::allow_attributes, unreachable_code)]
AppIconStatus::NotSetIgnored
}
/// Set icon for Windows applications.
#[cfg(target_os = "windows")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
use winapi::um::winuser;
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
use windows_sys::Win32::UI::WindowsAndMessaging::{
CreateIconFromResourceEx, GetSystemMetrics, HICON, ICON_BIG, ICON_SMALL, LR_DEFAULTCOLOR,
SM_CXICON, SM_CXSMICON, SendMessageW, WM_SETICON,
};
// We would get fairly far already with winit's `set_window_icon` (which is exposed to eframe) actually!
// However, it only sets ICON_SMALL, i.e. doesn't allow us to set a higher resolution icon for the task bar.
@@ -92,16 +96,13 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
// * using undocumented SetConsoleIcon method (successfully queried via GetProcAddress)
// SAFETY: WinApi function without side-effects.
let window_handle = unsafe { winuser::GetActiveWindow() };
let window_handle = unsafe { GetActiveWindow() };
if window_handle.is_null() {
// The Window isn't available yet. Try again later!
return AppIconStatus::NotSetTryAgain;
}
fn create_hicon_with_scale(
unscaled_image: &image::RgbaImage,
target_size: i32,
) -> winapi::shared::windef::HICON {
fn create_hicon_with_scale(unscaled_image: &image::RgbaImage, target_size: i32) -> HICON {
let image_scaled = image::imageops::resize(
unscaled_image,
target_size as _,
@@ -127,14 +128,14 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
// SAFETY: Creating an HICON which should be readonly on our data.
unsafe {
winuser::CreateIconFromResourceEx(
CreateIconFromResourceEx(
image_scaled_bytes.as_mut_ptr(),
image_scaled_bytes.len() as u32,
1, // Means this is an icon, not a cursor.
0x00030000, // Version number of the HICON
target_size, // Note that this method can scale, but it does so *very* poorly. So let's avoid that!
target_size,
winuser::LR_DEFAULTCOLOR,
LR_DEFAULTCOLOR,
)
}
}
@@ -155,7 +156,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
// Note that ICON_SMALL may be used even if we don't render a title bar as it may be used in alt+tab!
{
// SAFETY: WinAPI getter function with no known side effects.
let icon_size_big = unsafe { winuser::GetSystemMetrics(winuser::SM_CXICON) };
let icon_size_big = unsafe { GetSystemMetrics(SM_CXICON) };
let icon_big = create_hicon_with_scale(&unscaled_image, icon_size_big);
if icon_big.is_null() {
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
@@ -163,10 +164,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
winuser::SendMessageW(
SendMessageW(
window_handle,
winuser::WM_SETICON,
winuser::ICON_BIG as usize,
WM_SETICON,
ICON_BIG as usize,
icon_big as isize,
);
}
@@ -174,7 +175,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
}
{
// SAFETY: WinAPI getter function with no known side effects.
let icon_size_small = unsafe { winuser::GetSystemMetrics(winuser::SM_CXSMICON) };
let icon_size_small = unsafe { GetSystemMetrics(SM_CXSMICON) };
let icon_small = create_hicon_with_scale(&unscaled_image, icon_size_small);
if icon_small.is_null() {
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
@@ -182,10 +183,10 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
winuser::SendMessageW(
SendMessageW(
window_handle,
winuser::WM_SETICON,
winuser::ICON_SMALL as usize,
WM_SETICON,
ICON_SMALL as usize,
icon_small as isize,
);
}
@@ -198,20 +199,25 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
/// Set icon & app title for `MacOS` applications.
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus {
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::ClassType;
use objc2::ClassType as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::{NSData, NSString};
use objc2_foundation::NSString;
let png_bytes = if let Some(icon_data) = icon_data {
match icon_data.to_png_bytes() {
Ok(png_bytes) => Some(png_bytes),
// Do NOT use png even though creating `NSImage` from it is much easier than from raw images data!
//
// Some MacOS versions have a bug where creating an `NSImage` from a png will cause it to load an arbitrary `libpng.dylib`.
// If this dylib isn't the right version, the application will crash with SIGBUS.
// For details see https://github.com/emilk/egui/issues/7155
let image = if let Some(icon_data) = icon_data {
match icon_data.to_image() {
Ok(image) => Some(image),
Err(err) => {
log::warn!("Failed to convert IconData to png: {err}");
log::warn!("Failed to read icon data: {err}");
return AppIconStatus::NotSetIgnored;
}
}
@@ -220,7 +226,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
};
// TODO(madsmtm): Move this into `objc2-app-kit`
extern "C" {
unsafe extern "C" {
static NSApp: Option<&'static NSApplication>;
}
@@ -231,25 +237,50 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
return AppIconStatus::NotSetIgnored;
};
if let Some(png_bytes) = png_bytes {
let data = NSData::from_vec(png_bytes);
if let Some(image) = image {
use objc2_app_kit::{NSBitmapImageRep, NSDeviceRGBColorSpace};
use objc2_foundation::NSSize;
log::trace!("NSImage::initWithData…");
let app_icon = NSImage::initWithData(NSImage::alloc(), &data);
log::trace!(
"NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel"
);
let Some(image_rep) = NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
[image.as_raw().as_ptr().cast_mut()].as_mut_ptr(),
image.width() as isize,
image.height() as isize,
8, // bits per sample
4, // samples per pixel
true, // has alpha
false, // is not planar
NSDeviceRGBColorSpace,
(image.width() * 4) as isize, // bytes per row
32 // bits per pixel
) else {
log::warn!("Failed to create NSBitmapImageRep from app icon data.");
return AppIconStatus::NotSetIgnored;
};
log::trace!("NSImage::initWithSize");
let app_icon = NSImage::initWithSize(
NSImage::alloc(),
NSSize::new(image.width() as f64, image.height() as f64),
);
log::trace!("NSImage::addRepresentation");
app_icon.addRepresentation(&image_rep);
profiling::scope!("setApplicationIconImage_");
log::trace!("setApplicationIconImage…");
app.setApplicationIconImage(app_icon.as_deref());
app.setApplicationIconImage(Some(&app_icon));
}
// Change the title in the top bar - for python processes this would be again "python" otherwise.
if let Some(main_menu) = app.mainMenu() {
if let Some(item) = main_menu.itemAtIndex(0) {
if let Some(app_menu) = item.submenu() {
profiling::scope!("setTitle_");
app_menu.setTitle(&NSString::from_str(title));
}
}
if let Some(main_menu) = app.mainMenu()
&& let Some(item) = main_menu.itemAtIndex(0)
&& let Some(app_menu) = item.submenu()
{
profiling::scope!("setTitle_");
app_menu.setTitle(&NSString::from_str(title));
}
// The title in the Dock apparently can't be changed.

View File

@@ -52,14 +52,13 @@ pub fn viewport_builder(
viewport_builder = viewport_builder.with_position(pos);
}
if clamp_size_to_monitor_size {
if let Some(initial_window_size) = viewport_builder.inner_size {
let initial_window_size = egui::NumExt::at_most(
initial_window_size,
largest_monitor_point_size(egui_zoom_factor, event_loop),
);
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
}
if clamp_size_to_monitor_size && let Some(initial_window_size) = viewport_builder.inner_size
{
let initial_window_size = egui::NumExt::at_most(
initial_window_size,
largest_monitor_point_size(egui_zoom_factor, event_loop),
);
viewport_builder = viewport_builder.with_inner_size(initial_window_size);
}
viewport_builder.inner_size
@@ -136,7 +135,7 @@ pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
None
}
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::allow_attributes, clippy::unnecessary_wraps)]
pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
#[cfg(feature = "persistence")]
return Some(Box::new(
@@ -169,7 +168,7 @@ pub struct EpiIntegration {
}
impl EpiIntegration {
#[allow(clippy::too_many_arguments)]
#[allow(clippy::allow_attributes, clippy::too_many_arguments)]
pub fn new(
egui_ctx: egui::Context,
window: &winit::window::Window,
@@ -180,7 +179,9 @@ impl EpiIntegration {
#[cfg(feature = "glow")] glow_register_native_texture: Option<
Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
>,
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
#[cfg(feature = "wgpu_no_default_features")] wgpu_render_state: Option<
egui_wgpu::RenderState,
>,
) -> Self {
let frame = epi::Frame {
info: epi::IntegrationInfo { cpu_usage: None },
@@ -189,7 +190,7 @@ impl EpiIntegration {
gl,
#[cfg(feature = "glow")]
glow_register_native_texture,
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state,
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
@@ -271,14 +272,27 @@ impl EpiIntegration {
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
profiling::scope!("viewport_callback");
viewport_ui_cb(egui_ctx);
viewport_ui_cb(ui);
} else {
profiling::scope!("App::update");
app.update(egui_ctx, &mut self.frame);
{
profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame);
}
{
profiling::scope!("App::update");
#[expect(deprecated)]
app.update(ui.ctx(), &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
}
});
@@ -326,30 +340,32 @@ impl EpiIntegration {
}
}
#[allow(clippy::unused_self)]
pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
pub fn save(&mut self, app: &mut dyn epi::App, window: Option<&winit::window::Window>) {
#[cfg(not(feature = "persistence"))]
let _ = (self, app, window);
#[cfg(feature = "persistence")]
if let Some(storage) = self.frame.storage_mut() {
profiling::function_scope!();
if let Some(window) = _window {
if self.persist_window {
profiling::scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
);
}
if let Some(window) = window
&& self.persist_window
{
profiling::scope!("native_window");
epi::set_value(
storage,
STORAGE_WINDOW_KEY,
&WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
);
}
if _app.persist_egui_memory() {
if app.persist_egui_memory() {
profiling::scope!("egui_memory");
self.egui_ctx
.memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
}
{
profiling::scope!("App::save");
_app.save(storage);
app.save(storage);
}
profiling::scope!("Storage::flush");
@@ -360,6 +376,7 @@ impl EpiIntegration {
fn load_default_egui_icon() -> egui::IconData {
profiling::function_scope!();
#[expect(clippy::unwrap_used)]
crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap()
}

View File

@@ -27,7 +27,7 @@ impl Drop for EventLoopGuard {
}
// Helper function to safely use the current event loop
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub fn with_current_event_loop<F, R>(f: F) -> Option<R>
where
F: FnOnce(&ActiveEventLoop) -> R,

View File

@@ -1,6 +1,6 @@
use std::{
collections::HashMap,
io::Write,
io::Write as _,
path::{Path, PathBuf},
};
@@ -42,20 +42,20 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
// Adapted from
// https://github.com/rust-lang/cargo/blob/6e11c77384989726bb4f412a0e23b59c27222c34/crates/home/src/windows.rs#L19-L37
#[cfg(all(windows, not(target_vendor = "uwp")))]
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn roaming_appdata() -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
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;
use windows_sys::Win32::UI::Shell::{
FOLDERID_RoamingAppData, SHGetKnownFolderPath, KF_FLAG_DONT_VERIFY,
FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
extern "C" {
unsafe extern "C" {
fn wcslen(buf: *const u16) -> usize;
}
let mut path_raw = ptr::null_mut();
@@ -72,7 +72,7 @@ fn roaming_appdata() -> Option<PathBuf> {
};
let path = if result == S_OK {
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a nullterminated string for us.
// SAFETY: SHGetKnownFolderPath indicated success and is supposed to allocate a null-terminated string for us.
let path_slice = unsafe { slice::from_raw_parts(path_raw, wcslen(path_raw)) };
Some(PathBuf::from(OsString::from_wide(path_slice)))
} else {
@@ -118,7 +118,7 @@ impl FileStorage {
pub(crate) fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
profiling::function_scope!();
let ron_filepath: PathBuf = ron_filepath.into();
log::debug!("Loading app state from {:?}…", ron_filepath);
log::debug!("Loading app state from {}…", ron_filepath.display());
Self {
kv: read_ron(&ron_filepath).unwrap_or_default(),
ron_filepath,
@@ -133,9 +133,8 @@ impl FileStorage {
if let Some(data_dir) = storage_dir(app_id) {
if let Err(err) = std::fs::create_dir_all(&data_dir) {
log::warn!(
"Saving disabled: Failed to create app path at {:?}: {}",
data_dir,
err
"Saving disabled: Failed to create app path at {}: {err}",
data_dir.display()
);
None
} else {
@@ -193,12 +192,11 @@ impl crate::Storage for FileStorage {
fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
profiling::function_scope!();
if let Some(parent_dir) = file_path.parent() {
if !parent_dir.exists() {
if let Err(err) = std::fs::create_dir_all(parent_dir) {
log::warn!("Failed to create directory {parent_dir:?}: {err}");
}
}
if let Some(parent_dir) = file_path.parent()
&& !parent_dir.exists()
&& let Err(err) = std::fs::create_dir_all(parent_dir)
{
log::warn!("Failed to create directory {}: {err}", parent_dir.display());
}
match std::fs::File::create(file_path) {
@@ -207,16 +205,17 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
let config = Default::default();
profiling::scope!("ron::serialize");
if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config)
if let Err(err) = ron::Options::default()
.to_io_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {}", err);
log::warn!("Failed to serialize app state: {err}");
} else {
log::trace!("Persisted to {:?}", file_path);
log::trace!("Persisted to {}", file_path.display());
}
}
Err(err) => {
log::warn!("Failed to create file {file_path:?}: {err}");
log::warn!("Failed to create file {}: {err}", file_path.display());
}
}
}
@@ -234,7 +233,7 @@ where
match ron::de::from_reader(reader) {
Ok(value) => Some(value),
Err(err) => {
log::warn!("Failed to parse RON: {}", err);
log::warn!("Failed to parse RON: {err}");
None
}
}

View File

@@ -5,40 +5,41 @@
//! There is a bunch of improvements we could do,
//! like removing a bunch of `unwraps`.
#![allow(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::unwrap_used)]
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use glutin::{
config::GlConfig,
context::NotCurrentGlContext,
display::GetGlDisplay,
prelude::{GlDisplay, PossiblyCurrentGlContext},
surface::GlSurface,
config::GlConfig as _,
context::NotCurrentGlContext as _,
display::GetGlDisplay as _,
prelude::{GlDisplay as _, PossiblyCurrentGlContext as _},
surface::GlSurface as _,
};
use raw_window_handle::HasWindowHandle;
use raw_window_handle::HasWindowHandle as _;
use winit::{
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet};
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, ImmediateViewport, ViewportBuilder, ViewportClass, ViewportId,
ViewportIdMap, ViewportIdPair, ViewportInfo, ViewportOutput,
DeferredViewportUiCallback, ImmediateViewport, OrderedViewportIdMap, ViewportBuilder,
ViewportClass, ViewportId, ViewportIdPair, ViewportInfo, ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use crate::{
native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions,
Result, Storage,
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::epi_integration::EpiIntegration,
};
use super::{
epi_integration, event_loop_context,
winit_integration::{create_egui_context, EventResult, UserEvent, WinitApp},
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
// ----------------------------------------------------------------------------
@@ -94,9 +95,9 @@ struct GlutinWindowContext {
current_gl_context: Option<glutin::context::PossiblyCurrentContext>,
not_current_gl_context: Option<glutin::context::NotCurrentContext>,
viewports: ViewportIdMap<Viewport>,
viewports: OrderedViewportIdMap<Viewport>,
viewport_from_window: HashMap<WindowId, ViewportId>,
window_from_viewport: ViewportIdMap<WindowId>,
window_from_viewport: OrderedViewportIdMap<WindowId>,
focused_viewport: Option<ViewportId>,
}
@@ -107,7 +108,7 @@ struct Viewport {
builder: ViewportBuilder,
deferred_commands: Vec<egui::viewport::ViewportCommand>,
info: ViewportInfo,
actions_requested: HashSet<egui_winit::ActionRequested>,
actions_requested: Vec<egui_winit::ActionRequested>,
/// The user-callback that shows the ui.
/// None for immediate viewports.
@@ -139,7 +140,7 @@ impl<'app> GlowWinitApp<'app> {
}
}
#[allow(unsafe_code)]
#[expect(unsafe_code)]
fn create_glutin_windowed_context(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
@@ -216,7 +217,7 @@ impl<'app> GlowWinitApp<'app> {
storage.as_deref(),
&mut self.native_options,
)?;
let gl = painter.gl().clone();
let gl = Arc::clone(painter.gl());
let max_texture_side = painter.max_texture_side();
glutin.max_texture_side = Some(max_texture_side);
@@ -234,17 +235,17 @@ impl<'app> GlowWinitApp<'app> {
&self.app_name,
&self.native_options,
storage,
Some(gl.clone()),
Some(Arc::clone(&gl)),
Some(Box::new({
let painter = painter.clone();
let painter = Rc::clone(&painter);
move |native| painter.borrow_mut().register_native_texture(native)
})),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
None,
);
{
let event_loop_proxy = self.repaint_proxy.clone();
let event_loop_proxy = Arc::clone(&self.repaint_proxy);
integration
.egui_ctx
.set_request_repaint_callback(move |info| {
@@ -281,10 +282,9 @@ impl<'app> GlowWinitApp<'app> {
.viewport
.mouse_passthrough
.unwrap_or(false)
&& let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false)
{
if let Err(err) = glutin.window(ViewportId::ROOT).set_cursor_hittest(false) {
log::warn!("set_cursor_hittest(false) failed: {err}");
}
log::warn!("set_cursor_hittest(false) failed: {err}");
}
let app_creator = std::mem::take(&mut self.app_creator)
@@ -302,7 +302,7 @@ impl<'app> GlowWinitApp<'app> {
storage: integration.frame.storage(),
gl: Some(gl),
get_proc_address: Some(&get_proc_address),
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
@@ -336,10 +336,10 @@ impl<'app> GlowWinitApp<'app> {
}
Ok(self.running.insert(GlowWinitRunning {
glutin,
painter,
integration,
app,
glutin,
painter,
}))
}
}
@@ -362,8 +362,12 @@ impl WinitApp for GlowWinitApp<'_> {
fn window_id_from_viewport_id(&self, id: ViewportId) -> Option<WindowId> {
self.running
.as_ref()
.and_then(|r| r.glutin.borrow().window_from_viewport.get(&id).copied())
.as_ref()?
.glutin
.borrow()
.window_from_viewport
.get(&id)
.copied()
}
fn save(&mut self) {
@@ -436,20 +440,20 @@ impl WinitApp for GlowWinitApp<'_> {
_: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) -> crate::Result<EventResult> {
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
if let Some(running) = &mut self.running {
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport) = glutin
.focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let winit::event::DeviceEvent::MouseMotion { delta } = event
&& let Some(running) = &mut self.running
{
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport) = glutin
.focused_viewport
.and_then(|viewport| glutin.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
}
}
@@ -466,7 +470,7 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(running) = &mut self.running {
Ok(running.on_window_event(window_id, &event))
} else {
Ok(EventResult::Wait)
Ok(EventResult::Exit)
}
}
@@ -476,16 +480,15 @@ impl WinitApp for GlowWinitApp<'_> {
if let Some(running) = &self.running {
let mut glutin = running.glutin.borrow_mut();
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied() {
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
if let Some(egui_winit) = &mut viewport.egui_winit {
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
}
if let Some(viewport_id) = glutin.viewport_from_window.get(&event.window_id).copied()
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
&& let Some(egui_winit) = &mut viewport.egui_winit
{
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
}
@@ -523,10 +526,10 @@ impl GlowWinitRunning<'_> {
if is_immediate && viewport_id != ViewportId::ROOT {
// This will only happen if this is an immediate viewport.
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent) {
if let Some(window) = parent_viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(parent_viewport) = glutin.viewports.get(&viewport.ids.parent)
&& let Some(window) = parent_viewport.window.as_ref()
{
return Ok(EventResult::RepaintNext(window.id()));
}
return Ok(EventResult::Wait);
}
@@ -561,9 +564,15 @@ impl GlowWinitRunning<'_> {
(raw_input, viewport_ui_cb)
};
// 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.
self.integration
.egui_ctx
.options_mut(|opt| opt.begin_pass(&raw_input));
let clear_color = self
.app
.clear_color(&self.integration.egui_ctx.style().visuals);
.clear_color(&self.integration.egui_ctx.global_style().visuals);
let has_many_viewports = self.glutin.borrow().viewports.len() > 1;
let clear_before_update = !has_many_viewports; // HACK: for some reason, an early clear doesn't "take" on Mac with multiple viewports.
@@ -671,7 +680,7 @@ impl GlowWinitRunning<'_> {
);
{
for action in viewport.actions_requested.drain() {
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot(user_data) => {
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);
@@ -711,11 +720,11 @@ impl GlowWinitRunning<'_> {
// vsync - don't count as frame-time:
frame_timer.pause();
profiling::scope!("swap_buffers");
let context = current_gl_context
.as_ref()
.ok_or(egui_glow::PainterError::from(
let context = current_gl_context.as_ref().ok_or_else(|| {
egui_glow::PainterError::from(
"failed to get current context to swap buffers".to_owned(),
))?;
)
})?;
gl_surface.swap_buffers(context)?;
frame_timer.resume();
@@ -723,10 +732,10 @@ impl GlowWinitRunning<'_> {
// give it time to settle:
#[cfg(feature = "__screenshot")]
if integration.egui_ctx.cumulative_pass_nr() == 2 {
if let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO") {
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
}
if integration.egui_ctx.cumulative_pass_nr() == 2
&& let Ok(path) = std::env::var("EFRAME_SCREENSHOT_TO")
{
save_screenshot_and_exit(&path, &painter, screen_size_in_pixels);
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
@@ -743,7 +752,7 @@ impl GlowWinitRunning<'_> {
}
if integration.should_close() {
Ok(EventResult::Exit)
Ok(EventResult::CloseRequested)
} else {
Ok(EventResult::Wait)
}
@@ -773,19 +782,33 @@ impl GlowWinitRunning<'_> {
let mut repaint_asap = false;
match event {
winit::event::WindowEvent::Focused(new_focused) => {
glutin.focused_viewport = new_focused.then(|| viewport_id).flatten();
winit::event::WindowEvent::Focused(focused) => {
let focused = if cfg!(target_os = "macos")
&& let Some(viewport_id) = viewport_id
&& let Some(viewport) = glutin.viewports.get(&viewport_id)
&& let Some(window) = &viewport.window
{
// TODO(emilk): remove this work-around once we update winit
// https://github.com/rust-windowing/winit/issues/4371
// https://github.com/emilk/egui/issues/7588
window.has_focus()
} else {
*focused
};
glutin.focused_viewport = focused.then_some(viewport_id).flatten();
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if 0 < physical_size.width && 0 < physical_size.height {
if let Some(viewport_id) = viewport_id {
repaint_asap = true;
glutin.resize(viewport_id, *physical_size);
}
if 0 < physical_size.width
&& 0 < physical_size.height
&& let Some(viewport_id) = viewport_id
{
repaint_asap = true;
glutin.resize(viewport_id, *physical_size);
}
}
@@ -794,44 +817,31 @@ impl GlowWinitRunning<'_> {
log::debug!(
"Received WindowEvent::CloseRequested for main viewport - shutting down."
);
return EventResult::Exit;
return EventResult::CloseRequested;
}
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
if let Some(viewport_id) = viewport_id {
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
if let Some(viewport_id) = viewport_id
&& let Some(viewport) = glutin.viewports.get_mut(&viewport_id)
{
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
self.integration.egui_ctx.request_repaint_of(viewport_id);
self.integration
.egui_ctx
.request_repaint_of(viewport.ids.parent);
}
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
self.integration.egui_ctx.request_repaint_of(viewport_id);
self.integration
.egui_ctx
.request_repaint_of(viewport.ids.parent);
}
}
winit::event::WindowEvent::Destroyed => {
log::debug!(
"Received WindowEvent::Destroyed for viewport {:?}",
viewport_id
);
if viewport_id == Some(ViewportId::ROOT) {
return EventResult::Exit;
} else {
return EventResult::Wait;
}
}
_ => {}
}
if self.integration.should_close() {
return EventResult::Exit;
return EventResult::CloseRequested;
}
let mut event_response = egui_winit::EventResponse {
@@ -901,7 +911,7 @@ fn change_gl_context(
}
impl GlutinWindowContext {
#[allow(unsafe_code)]
#[expect(unsafe_code)]
unsafe fn new(
egui_ctx: &egui::Context,
viewport_builder: ViewportBuilder,
@@ -959,7 +969,6 @@ impl GlutinWindowContext {
.with_preference(glutin_winit::ApiPreference::FallbackEgl)
.with_window_attributes(Some(egui_winit::create_winit_window_attributes(
egui_ctx,
event_loop,
viewport_builder.clone(),
)));
@@ -1016,7 +1025,9 @@ impl GlutinWindowContext {
let gl_context = match gl_context_result {
Ok(it) => it,
Err(err) => {
log::warn!("Failed to create context using default context attributes {context_attributes:?} due to error: {err}");
log::warn!(
"Failed to create context using default context attributes {context_attributes:?} due to error: {err}"
);
log::debug!(
"Retrying with fallback context attributes: {fallback_context_attributes:?}"
);
@@ -1030,15 +1041,27 @@ impl GlutinWindowContext {
let not_current_gl_context = Some(gl_context);
let mut viewport_from_window = HashMap::default();
let mut window_from_viewport = ViewportIdMap::default();
let mut info = ViewportInfo::default();
let mut window_from_viewport = OrderedViewportIdMap::default();
let mut viewport_info = ViewportInfo::default();
if let Some(window) = &window {
viewport_from_window.insert(window.id(), ViewportId::ROOT);
window_from_viewport.insert(ViewportId::ROOT, window.id());
egui_winit::update_viewport_info(&mut info, egui_ctx, window, true);
egui_winit::update_viewport_info(&mut viewport_info, egui_ctx, window, true);
// Tell egui right away about native_pixels_per_point etc,
// so that the app knows about it during app creation:
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
egui_ctx.input_mut(|i| {
i.raw
.viewports
.insert(ViewportId::ROOT, viewport_info.clone());
i.pixels_per_point = pixels_per_point;
});
}
let mut viewports = ViewportIdMap::default();
let mut viewports = OrderedViewportIdMap::default();
viewports.insert(
ViewportId::ROOT,
Viewport {
@@ -1046,7 +1069,7 @@ impl GlutinWindowContext {
class: ViewportClass::Root,
builder: viewport_builder,
deferred_commands: vec![],
info,
info: viewport_info,
actions_requested: Default::default(),
viewport_ui_cb: None,
gl_surface: None,
@@ -1094,7 +1117,7 @@ impl GlutinWindowContext {
}
/// Create a surface, window, and winit integration for the viewport, if missing.
#[allow(unsafe_code)]
#[expect(unsafe_code)]
pub(crate) fn initialize_window(
&mut self,
viewport_id: ViewportId,
@@ -1113,7 +1136,6 @@ impl GlutinWindowContext {
log::debug!("Creating a window for viewport {viewport_id:?}");
let window_attributes = egui_winit::create_winit_window_attributes(
&self.egui_ctx,
event_loop,
viewport.builder.clone(),
);
if window_attributes.transparent()
@@ -1241,21 +1263,21 @@ impl GlutinWindowContext {
let width_px = NonZeroU32::new(physical_size.width).unwrap_or(NonZeroU32::MIN);
let height_px = NonZeroU32::new(physical_size.height).unwrap_or(NonZeroU32::MIN);
if let Some(viewport) = self.viewports.get(&viewport_id) {
if let Some(gl_surface) = &viewport.gl_surface {
change_gl_context(
&mut self.current_gl_context,
&mut self.not_current_gl_context,
gl_surface,
);
gl_surface.resize(
self.current_gl_context
.as_ref()
.expect("failed to get current context to resize surface"),
width_px,
height_px,
);
}
if let Some(viewport) = self.viewports.get(&viewport_id)
&& let Some(gl_surface) = &viewport.gl_surface
{
change_gl_context(
&mut self.current_gl_context,
&mut self.not_current_gl_context,
gl_surface,
);
gl_surface.resize(
self.current_gl_context
.as_ref()
.expect("failed to get current context to resize surface"),
width_px,
height_px,
);
}
}
@@ -1265,7 +1287,7 @@ impl GlutinWindowContext {
pub(crate) fn remove_viewports_not_in(
&mut self,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
// GC old viewports
self.viewports
@@ -1280,7 +1302,7 @@ impl GlutinWindowContext {
&mut self,
event_loop: &ActiveEventLoop,
egui_ctx: &egui::Context,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
profiling::function_scope!();
@@ -1337,14 +1359,16 @@ impl GlutinWindowContext {
}
fn initialize_or_update_viewport(
viewports: &mut ViewportIdMap<Viewport>,
viewports: &mut OrderedViewportIdMap<Viewport>,
ids: ViewportIdPair,
class: ViewportClass,
mut builder: ViewportBuilder,
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
viewport_ui_cb: Option<Arc<dyn Fn(&mut egui::Ui) + Send + Sync>>,
) -> &mut Viewport {
profiling::function_scope!();
use std::collections::btree_map::Entry;
if builder.icon.is_none() {
// Inherit icon from parent
builder.icon = viewports
@@ -1353,7 +1377,7 @@ fn initialize_or_update_viewport(
}
match viewports.entry(ids.this) {
std::collections::hash_map::Entry::Vacant(entry) => {
Entry::Vacant(entry) => {
// New viewport:
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
entry.insert(Viewport {
@@ -1370,7 +1394,7 @@ fn initialize_or_update_viewport(
})
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
Entry::Occupied(mut entry) => {
// Patch an existing viewport:
let viewport = entry.get_mut();
@@ -1471,8 +1495,8 @@ fn render_immediate_viewport(
shapes,
pixels_per_point,
viewport_output,
} = egui_ctx.run(input, |ctx| {
viewport_ui_cb(ctx);
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
// ---------------------------------------------------
@@ -1566,6 +1590,6 @@ fn save_screenshot_and_exit(
});
log::info!("Screenshot saved to {path:?}.");
#[allow(clippy::exit)]
#[expect(clippy::exit)]
std::process::exit(0);
}

View File

@@ -12,5 +12,5 @@ pub(crate) mod winit_integration;
#[cfg(feature = "glow")]
mod glow_integration;
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
mod wgpu_integration;

View File

@@ -10,9 +10,8 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
epi,
Result, epi,
native::{event_loop_context, winit_integration::EventResult},
Result,
};
// ----------------------------------------------------------------------------
@@ -95,15 +94,15 @@ impl<T: WinitApp> WinitAppWrapper<T> {
let mut event_result = event_result;
if cfg!(target_os = "windows") {
if let Ok(EventResult::RepaintNow(window_id)) = event_result {
log::trace!("RepaintNow of {window_id:?}");
self.windows_next_repaint_times
.insert(window_id, Instant::now());
if cfg!(target_os = "windows")
&& let Ok(EventResult::RepaintNow(window_id)) = event_result
{
log::trace!("RepaintNow of {window_id:?}");
self.windows_next_repaint_times
.insert(window_id, Instant::now());
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
event_result = self.winit_app.run_ui_and_paint(event_loop, window_id);
}
// Fix flickering on Windows, see https://github.com/emilk/egui/pull/2280
event_result = self.winit_app.run_ui_and_paint(event_loop, window_id);
}
let combined_result = event_result.map(|event_result| match event_result {
@@ -140,13 +139,18 @@ impl<T: WinitApp> WinitAppWrapper<T> {
exit = true;
event_result
}
EventResult::CloseRequested => {
// The windows need to be dropped whilst the event loop is running to allow for proper cleanup.
self.winit_app.save_and_destroy();
event_result
}
});
if let Err(err) = combined_result {
log::error!("Exiting because of error: {err}");
exit = true;
self.return_result = Err(err);
};
}
if save {
log::debug!("Received an EventResult::Save - saving app state");
@@ -163,7 +167,6 @@ impl<T: WinitApp> WinitAppWrapper<T> {
log::debug!("Exiting with return code 0");
#[allow(clippy::exit)]
std::process::exit(0);
}
}
@@ -178,7 +181,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
.retain(|window_id, repaint_time| {
if now < *repaint_time {
return true; // not yet ready
};
}
event_loop.set_control_flow(ControlFlow::Poll);
@@ -194,7 +197,7 @@ impl<T: WinitApp> WinitAppWrapper<T> {
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));
};
}
}
}
@@ -317,7 +320,7 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
#[cfg(not(target_os = "ios"))]
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, winit_app: impl WinitApp) -> Result {
use winit::platform::run_on_demand::EventLoopExtRunOnDemand;
use winit::platform::run_on_demand::EventLoopExtRunOnDemand as _;
log::trace!("Entering the winit event loop (run_app_on_demand)…");
@@ -346,8 +349,6 @@ pub fn run_glow(
mut native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'_>,
) -> Result {
#![allow(clippy::needless_return_with_question_mark)] // False positive
use super::glow_integration::GlowWinitApp;
#[cfg(not(target_os = "ios"))]
@@ -363,16 +364,27 @@ pub fn run_glow(
run_and_exit(event_loop, glow_eframe)
}
#[cfg(feature = "glow")]
pub fn create_glow<'a>(
app_name: &str,
native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'a>,
event_loop: &EventLoop<UserEvent>,
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::glow_integration::GlowWinitApp;
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
WinitAppWrapper::new(glow_eframe, true)
}
// ----------------------------------------------------------------------------
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
pub fn run_wgpu(
app_name: &str,
mut native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'_>,
) -> Result {
#![allow(clippy::needless_return_with_question_mark)] // False positive
use super::wgpu_integration::WgpuWinitApp;
#[cfg(not(target_os = "ios"))]
@@ -387,3 +399,120 @@ pub fn run_wgpu(
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
run_and_exit(event_loop, wgpu_eframe)
}
#[cfg(feature = "wgpu_no_default_features")]
pub fn create_wgpu<'a>(
app_name: &str,
native_options: epi::NativeOptions,
app_creator: epi::AppCreator<'a>,
event_loop: &EventLoop<UserEvent>,
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::wgpu_integration::WgpuWinitApp;
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
WinitAppWrapper::new(wgpu_eframe, true)
}
// ----------------------------------------------------------------------------
/// A proxy to the eframe application that implements [`ApplicationHandler`].
///
/// This can be run directly on your own [`EventLoop`] by itself or with other
/// windows you manage outside of eframe.
pub struct EframeWinitApplication<'a> {
wrapper: Box<dyn ApplicationHandler<UserEvent> + 'a>,
control_flow: ControlFlow,
}
impl ApplicationHandler<UserEvent> for EframeWinitApplication<'_> {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.resumed(event_loop);
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
self.wrapper.window_event(event_loop, window_id, event);
}
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
self.wrapper.new_events(event_loop, cause);
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
self.wrapper.user_event(event_loop, event);
}
fn device_event(
&mut self,
event_loop: &ActiveEventLoop,
device_id: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) {
self.wrapper.device_event(event_loop, device_id, event);
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.about_to_wait(event_loop);
self.control_flow = event_loop.control_flow();
}
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.suspended(event_loop);
}
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.exiting(event_loop);
}
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
self.wrapper.memory_warning(event_loop);
}
}
impl<'a> EframeWinitApplication<'a> {
pub(crate) fn new<T: ApplicationHandler<UserEvent> + 'a>(app: T) -> Self {
Self {
wrapper: Box::new(app),
control_flow: ControlFlow::default(),
}
}
/// Pump the `EventLoop` to check for and dispatch pending events to this application.
///
/// Returns either the exit code for the application or the final state of the [`ControlFlow`]
/// after all events have been dispatched in this iteration.
///
/// This is useful when your [`EventLoop`] is not the main event loop for your application.
/// See the `external_eventloop_async` example.
#[cfg(not(target_os = "ios"))]
pub fn pump_eframe_app(
&mut self,
event_loop: &mut EventLoop<UserEvent>,
timeout: Option<std::time::Duration>,
) -> EframePumpStatus {
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};
match event_loop.pump_app_events(timeout, self) {
PumpStatus::Continue => EframePumpStatus::Continue(self.control_flow),
PumpStatus::Exit(code) => EframePumpStatus::Exit(code),
}
}
}
/// Either an exit code or a [`ControlFlow`] from the [`ActiveEventLoop`].
///
/// The result of [`EframeWinitApplication::pump_eframe_app`].
#[cfg(not(target_os = "ios"))]
pub enum EframePumpStatus {
/// The final state of the [`ControlFlow`] after all events have been dispatched
///
/// Callers should perform the action that is appropriate for the [`ControlFlow`] value.
Continue(ControlFlow),
/// The exit code for the application
Exit(i32),
}

View File

@@ -15,18 +15,19 @@ use winit::{
window::{Window, WindowId},
};
use ahash::{HashMap, HashSet, HashSetExt};
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, ViewportBuilder, ViewportClass,
ViewportId, ViewportIdMap, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput,
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use winit_integration::UserEvent;
use crate::{
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
@@ -70,9 +71,10 @@ pub struct SharedState {
painter: egui_wgpu::winit::Painter,
viewport_from_window: HashMap<WindowId, ViewportId>,
focused_viewport: Option<ViewportId>,
resized_viewport: Option<ViewportId>,
}
pub type Viewports = ViewportIdMap<Viewport>;
pub type Viewports = egui::OrderedViewportIdMap<Viewport>;
pub struct Viewport {
ids: ViewportIdPair,
@@ -80,7 +82,7 @@ pub struct Viewport {
builder: ViewportBuilder,
deferred_commands: Vec<egui::viewport::ViewportCommand>,
info: ViewportInfo,
actions_requested: HashSet<ActionRequested>,
actions_requested: Vec<ActionRequested>,
/// `None` for sync viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -182,24 +184,42 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
self.native_options.multisampling.max(1) as _,
egui_wgpu::depth_format_from_bits(
self.native_options.depth_buffer,
self.native_options.stencil_buffer,
),
self.native_options.viewport.transparent.unwrap_or(false),
self.native_options.dithering,
egui_wgpu::RendererOptions {
msaa_samples: self.native_options.multisampling as _,
depth_stencil_format: egui_wgpu::depth_format_from_bits(
self.native_options.depth_buffer,
self.native_options.stencil_buffer,
),
dithering: self.native_options.dithering,
..Default::default()
},
));
let mut viewport_info = ViewportInfo::default();
egui_winit::update_viewport_info(&mut viewport_info, &egui_ctx, &window, true);
{
// Tell egui right away about native_pixels_per_point etc,
// so that the app knows about it during app creation:
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
egui_ctx.input_mut(|i| {
i.raw
.viewports
.insert(ViewportId::ROOT, viewport_info.clone());
i.pixels_per_point = pixels_per_point;
});
}
let window = Arc::new(window);
{
profiling::scope!("set_window");
pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?;
pollster::block_on(painter.set_window(ViewportId::ROOT, Some(Arc::clone(&window))))?;
}
let wgpu_render_state = painter.render_state();
@@ -218,7 +238,7 @@ impl<'app> WgpuWinitApp<'app> {
);
{
let event_loop_proxy = self.repaint_proxy.clone();
let event_loop_proxy = Arc::clone(&self.repaint_proxy);
egui_ctx.set_request_repaint_callback(move |info| {
log::trace!("request_repaint_callback: {info:?}");
@@ -236,7 +256,7 @@ impl<'app> WgpuWinitApp<'app> {
});
}
#[allow(unused_mut)] // used for accesskit
#[allow(clippy::allow_attributes, unused_mut)] // used for accesskit
let mut egui_winit = egui_winit::State::new(
egui_ctx.clone(),
ViewportId::ROOT,
@@ -274,9 +294,6 @@ impl<'app> WgpuWinitApp<'app> {
let mut viewport_from_window = HashMap::default();
viewport_from_window.insert(window.id(), ViewportId::ROOT);
let mut info = ViewportInfo::default();
egui_winit::update_viewport_info(&mut info, &egui_ctx, &window, true);
let mut viewports = Viewports::default();
viewports.insert(
ViewportId::ROOT,
@@ -285,7 +302,7 @@ impl<'app> WgpuWinitApp<'app> {
class: ViewportClass::Root,
builder,
deferred_commands: vec![],
info,
info: viewport_info,
actions_requested: Default::default(),
viewport_ui_cb: None,
window: Some(window),
@@ -299,6 +316,7 @@ impl<'app> WgpuWinitApp<'app> {
viewports,
painter,
focused_viewport: Some(ViewportId::ROOT),
resized_viewport: None,
}));
{
@@ -333,10 +351,8 @@ impl WinitApp for WgpuWinitApp<'_> {
.as_ref()
.and_then(|r| {
let shared = r.shared.borrow();
shared
.viewport_from_window
.get(&window_id)
.and_then(|id| shared.viewports.get(id).map(|v| v.window.clone()))
let id = shared.viewport_from_window.get(&window_id)?;
shared.viewports.get(id).map(|v| v.window.clone())
})
.flatten()
}
@@ -431,20 +447,20 @@ impl WinitApp for WgpuWinitApp<'_> {
_: winit::event::DeviceId,
event: winit::event::DeviceEvent,
) -> crate::Result<EventResult> {
if let winit::event::DeviceEvent::MouseMotion { delta } = event {
if let Some(running) = &mut self.running {
let mut shared = running.shared.borrow_mut();
if let Some(viewport) = shared
.focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let winit::event::DeviceEvent::MouseMotion { delta } = event
&& let Some(running) = &mut self.running
{
let mut shared = running.shared.borrow_mut();
if let Some(viewport) = shared
.focused_viewport
.and_then(|viewport| shared.viewports.get_mut(&viewport))
{
if let Some(egui_winit) = viewport.egui_winit.as_mut() {
egui_winit.on_mouse_motion(delta);
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
}
}
@@ -463,7 +479,8 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(running) = &mut self.running {
Ok(running.on_window_event(window_id, &event))
} else {
Ok(EventResult::Wait)
// running is removed to get ready for exiting
Ok(EventResult::Exit)
}
}
@@ -479,14 +496,13 @@ impl WinitApp for WgpuWinitApp<'_> {
if let Some(viewport) = viewport_from_window
.get(&event.window_id)
.and_then(|id| viewports.get_mut(id))
&& let Some(egui_winit) = &mut viewport.egui_winit
{
if let Some(egui_winit) = &mut viewport.egui_winit {
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
return Ok(winit_integration::on_accesskit_window_event(
egui_winit,
event.window_id,
&event.window_event,
));
}
}
@@ -564,10 +580,10 @@ impl WgpuWinitRunning<'_> {
if viewport.viewport_ui_cb.is_none() {
// This will only happen if this is an immediate viewport.
// That means that the viewport cannot be rendered by itself and needs his parent to be rendered.
if let Some(viewport) = viewports.get(&viewport.ids.parent) {
if let Some(window) = viewport.window.as_ref() {
return Ok(EventResult::RepaintNext(window.id()));
}
if let Some(viewport) = viewports.get(&viewport.ids.parent)
&& let Some(window) = viewport.window.as_ref()
{
return Ok(EventResult::RepaintNext(window.id()));
}
return Ok(EventResult::Wait);
}
@@ -594,7 +610,7 @@ impl WgpuWinitRunning<'_> {
{
profiling::scope!("set_window");
pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))?;
pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(window))))?;
}
let Some(egui_winit) = egui_winit.as_mut() else {
@@ -674,13 +690,13 @@ impl WgpuWinitRunning<'_> {
let vsync_secs = painter.paint_and_update_textures(
viewport_id,
pixels_per_point,
app.clear_color(&egui_ctx.style().visuals),
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
&textures_delta,
screenshot_commands,
);
for action in viewport.actions_requested.drain() {
for action in viewport.actions_requested.drain(..) {
match action {
ActionRequested::Screenshot { .. } => {
// already handled above
@@ -731,17 +747,17 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window {
if window.is_minimized() == Some(true) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
if let Some(window) = window
&& window.is_minimized() == Some(true)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
if integration.should_close() {
Ok(EventResult::Exit)
Ok(EventResult::CloseRequested)
} else {
Ok(EventResult::Wait)
}
@@ -762,37 +778,68 @@ impl WgpuWinitRunning<'_> {
let viewport_id = shared.viewport_from_window.get(&window_id).copied();
// On Windows, if a window is resized by the user, it should repaint synchronously, inside the
// event handler.
//
// If this is not done, the compositor will assume that the window does not want to redraw,
// and continue ahead.
// event handler. If this is not done, the compositor will assume that the window does not want
// to redraw and continue ahead.
//
// In eframe's case, that causes the window to rapidly flicker, as it struggles to deliver
// new frames to the compositor in time.
//
// The flickering is technically glutin or glow's fault, but we should be responding properly
// new frames to the compositor in time. The flickering is technically glutin or glow's fault, but we should be responding properly
// to resizes anyway, as doing so avoids dropping frames.
//
// See: https://github.com/emilk/egui/issues/903
let mut repaint_asap = false;
// On MacOS the asap repaint is not enough. The drawn frames must be synchronized with
// the CoreAnimation transactions driving the window resize process.
//
// Thus, Painter, responsible for wgpu surfaces and their resize, has to be notified of the
// resize lifecycle, yet winit does not provide any events for that. To work around,
// the last resized viewport is tracked until any next non-resize event is received.
//
// Accidental state change during the resize process due to an unexpected event fire
// is ok, state will switch back upon next resize event.
//
// See: https://github.com/emilk/egui/issues/903
if let Some(id) = viewport_id
&& shared.resized_viewport == viewport_id
{
shared.painter.on_window_resize_state_change(id, false);
shared.resized_viewport = None;
}
match event {
winit::event::WindowEvent::Focused(new_focused) => {
shared.focused_viewport = new_focused.then(|| viewport_id).flatten();
winit::event::WindowEvent::Focused(focused) => {
let focused = if cfg!(target_os = "macos")
&& let Some(viewport_id) = viewport_id
&& let Some(viewport) = shared.viewports.get(&viewport_id)
&& let Some(window) = &viewport.window
{
// TODO(emilk): remove this work-around once we update winit
// https://github.com/rust-windowing/winit/issues/4371
// https://github.com/emilk/egui/issues/7588
window.has_focus()
} else {
*focused
};
shared.focused_viewport = focused.then_some(viewport_id).flatten();
}
winit::event::WindowEvent::Resized(physical_size) => {
// Resize with 0 width and height is used by winit to signal a minimize event on Windows.
// See: https://github.com/rust-windowing/winit/issues/208
// This solves an issue where the app would panic when minimizing on Windows.
if let Some(viewport_id) = viewport_id {
if let (Some(width), Some(height)) = (
if let Some(id) = viewport_id
&& let (Some(width), Some(height)) = (
NonZeroU32::new(physical_size.width),
NonZeroU32::new(physical_size.height),
) {
repaint_asap = true;
shared.painter.on_window_resized(viewport_id, width, height);
)
{
if shared.resized_viewport != viewport_id {
shared.resized_viewport = viewport_id;
shared.painter.on_window_resize_state_change(id, true);
}
shared.painter.on_window_resized(id, width, height);
repaint_asap = true;
}
}
@@ -801,42 +848,41 @@ impl WgpuWinitRunning<'_> {
log::debug!(
"Received WindowEvent::CloseRequested for main viewport - shutting down."
);
return EventResult::Exit;
return EventResult::CloseRequested;
}
log::debug!("Received WindowEvent::CloseRequested for viewport {viewport_id:?}");
if let Some(viewport_id) = viewport_id {
if let Some(viewport) = shared.viewports.get_mut(&viewport_id) {
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
if let Some(viewport_id) = viewport_id
&& let Some(viewport) = shared.viewports.get_mut(&viewport_id)
{
// Tell viewport it should close:
viewport.info.events.push(egui::ViewportEvent::Close);
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
integration.egui_ctx.request_repaint_of(viewport_id);
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
}
// We may need to repaint both us and our parent to close the window,
// and perhaps twice (once to notice the close-event, once again to enforce it).
// `request_repaint_of` does a double-repaint though:
integration.egui_ctx.request_repaint_of(viewport_id);
integration.egui_ctx.request_repaint_of(viewport.ids.parent);
}
}
_ => {}
};
}
let event_response = viewport_id
.and_then(|viewport_id| {
shared.viewports.get_mut(&viewport_id).and_then(|viewport| {
Some(integration.on_window_event(
viewport.window.as_deref()?,
viewport.egui_winit.as_mut()?,
event,
))
})
let viewport = shared.viewports.get_mut(&viewport_id)?;
Some(integration.on_window_event(
viewport.window.as_deref()?,
viewport.egui_winit.as_mut()?,
event,
))
})
.unwrap_or_default();
if integration.should_close() {
EventResult::Exit
EventResult::CloseRequested
} else if event_response.repaint {
if repaint_asap {
EventResult::RepaintNow(window_id)
@@ -873,7 +919,7 @@ impl Viewport {
let window = Arc::new(window);
if let Err(err) =
pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))
pollster::block_on(painter.set_window(viewport_id, Some(Arc::clone(&window))))
{
log::error!("on set_window: viewport_id {viewport_id:?} {err}");
}
@@ -981,8 +1027,8 @@ fn render_immediate_viewport(
shapes,
pixels_per_point,
viewport_output,
} = egui_ctx.run(input, |ctx| {
viewport_ui_cb(ctx);
} = egui_ctx.run_ui(input, |ui| {
viewport_ui_cb(ui);
});
// ------------------------------------------
@@ -1005,7 +1051,8 @@ fn render_immediate_viewport(
{
profiling::scope!("set_window");
if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window.clone()))) {
if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(Arc::clone(window))))
{
log::error!(
"when rendering viewport_id={:?}, set_window Error {err}",
ids.this
@@ -1035,10 +1082,10 @@ fn render_immediate_viewport(
}
pub(crate) fn remove_viewports_not_in(
viewports: &mut ViewportIdMap<Viewport>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
) {
let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
@@ -1051,8 +1098,8 @@ pub(crate) fn remove_viewports_not_in(
/// Add new viewports, and update existing ones:
fn handle_viewport_output(
egui_ctx: &egui::Context,
viewport_output: &ViewportIdMap<ViewportOutput>,
viewports: &mut ViewportIdMap<Viewport>,
viewport_output: &OrderedViewportIdMap<ViewportOutput>,
viewports: &mut Viewports,
painter: &mut egui_wgpu::winit::Painter,
viewport_from_window: &mut HashMap<WindowId, ViewportId>,
) {
@@ -1089,13 +1136,13 @@ fn handle_viewport_output(
// 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 {
if let (Some(width), Some(height)) = (
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);
}
)
{
painter.on_window_resized(viewport_id, width, height);
}
}
}
@@ -1109,9 +1156,11 @@ fn initialize_or_update_viewport<'a>(
ids: ViewportIdPair,
class: ViewportClass,
mut builder: ViewportBuilder,
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
viewport_ui_cb: Option<Arc<dyn Fn(&mut egui::Ui) + Send + Sync>>,
painter: &mut egui_wgpu::winit::Painter,
) -> &'a mut Viewport {
use std::collections::btree_map::Entry;
profiling::function_scope!();
if builder.icon.is_none() {
@@ -1122,7 +1171,7 @@ fn initialize_or_update_viewport<'a>(
}
match viewports.entry(ids.this) {
std::collections::hash_map::Entry::Vacant(entry) => {
Entry::Vacant(entry) => {
// New viewport:
log::debug!("Creating new viewport {:?} ({:?})", ids.this, builder.title);
entry.insert(Viewport {
@@ -1131,14 +1180,14 @@ fn initialize_or_update_viewport<'a>(
builder,
deferred_commands: vec![],
info: Default::default(),
actions_requested: HashSet::new(),
actions_requested: Vec::new(),
viewport_ui_cb,
window: None,
egui_winit: None,
})
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
Entry::Occupied(mut entry) => {
// Patch an existing viewport:
let viewport = entry.get_mut();

View File

@@ -27,7 +27,10 @@ pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Contex
egui_ctx.options_mut(|o| {
// eframe supports multi-pass (Context::request_discard).
o.max_passes = 2.try_into().unwrap();
#[expect(clippy::unwrap_used)]
{
o.max_passes = 2.try_into().unwrap();
}
});
let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default();
@@ -124,6 +127,25 @@ pub enum EventResult {
/// Causes a save of the client state when the persistence feature is enabled.
Save,
/// Starts the process of ending eframe execution whilst allowing for proper
/// clean up of resources.
///
/// # Warning
/// This event **must** occur before [`Exit`] to correctly exit eframe code.
/// If in doubt, return this event.
///
/// [`Exit`]: [EventResult::Exit]
CloseRequested,
/// The event loop will exit, now.
/// The correct circumstance to return this event is in response to a winit "Destroyed" event.
///
/// # Warning
/// The [`CloseRequested`] **must** occur before this event to ensure that winit
/// is able to remove any open windows. Otherwise the window(s) will remain open
/// until the program terminates.
///
/// [`CloseRequested`]: EventResult::CloseRequested
Exit,
}

View File

@@ -1,4 +1,4 @@
#![allow(dead_code)] // not everything is used on wasm
#![allow(clippy::allow_attributes, dead_code)] // not used on all platforms
use web_time::Instant;
@@ -23,7 +23,7 @@ impl Stopwatch {
}
pub fn pause(&mut self) {
let start = self.start.take().unwrap();
let start = self.start.take().expect("Stopwatch is not running");
let duration = start.elapsed();
self.total_time_ns += duration.as_nanos();
}

View File

@@ -1,18 +1,20 @@
use std::sync::Arc;
use egui::{TexturesDelta, UserData, ViewportCommand};
use crate::{epi, App};
use crate::{App, epi, web::web_painter::WebPainter};
use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint};
use super::{NeedRepaint, now_sec, text_agent::TextAgent};
pub struct AppRunner {
#[allow(dead_code)]
#[allow(clippy::allow_attributes, dead_code)]
pub(crate) web_options: crate::WebOptions,
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
painter: super::ActiveWebPainter,
painter: Box<dyn WebPainter>,
pub(crate) input: super::WebInput,
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
pub(crate) needs_repaint: Arc<NeedRepaint>,
last_save_time: f64,
pub(crate) text_agent: TextAgent,
@@ -34,6 +36,10 @@ impl Drop for AppRunner {
impl AppRunner {
/// # Errors
/// Failure to initialize WebGL renderer, or failure to create app.
#[cfg_attr(
not(feature = "wgpu_no_default_features"),
expect(clippy::unused_async)
)]
pub async fn new(
canvas: web_sys::HtmlCanvasElement,
web_options: crate::WebOptions,
@@ -41,7 +47,41 @@ impl AppRunner {
text_agent: TextAgent,
) -> Result<Self, String> {
let egui_ctx = egui::Context::default();
let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?;
#[allow(clippy::allow_attributes, unused_assignments)]
#[cfg(feature = "glow")]
let mut gl = None;
#[allow(clippy::allow_attributes, unused_assignments)]
#[cfg(feature = "wgpu_no_default_features")]
let mut wgpu_render_state = None;
let painter = match web_options.renderer {
#[cfg(feature = "glow")]
epi::Renderer::Glow => {
log::debug!("Using the glow renderer");
let painter = super::web_painter_glow::WebPainterGlow::new(
egui_ctx.clone(),
canvas,
&web_options,
)?;
gl = Some(Arc::clone(painter.gl()));
Box::new(painter) as Box<dyn WebPainter>
}
#[cfg(feature = "wgpu_no_default_features")]
epi::Renderer::Wgpu => {
log::debug!("Using the wgpu renderer");
let painter = super::web_painter_wgpu::WebPainterWgpu::new(
egui_ctx.clone(),
canvas,
&web_options,
)
.await?;
wgpu_render_state = painter.render_state();
Box::new(painter) as Box<dyn WebPainter>
}
};
let info = epi::IntegrationInfo {
web_info: epi::WebInfo {
@@ -65,21 +105,27 @@ impl AppRunner {
o.zoom_factor = 1.0;
});
// Tell egui right away about native_pixels_per_point
// so that the app knows about it during app creation:
egui_ctx.input_mut(|i| {
let viewport_info = i.raw.viewports.entry(egui::ViewportId::ROOT).or_default();
viewport_info.native_pixels_per_point = Some(super::native_pixels_per_point());
i.pixels_per_point = super::native_pixels_per_point();
});
let cc = epi::CreationContext {
egui_ctx: egui_ctx.clone(),
integration_info: info.clone(),
storage: Some(&storage),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
gl: gl.clone(),
#[cfg(feature = "glow")]
get_proc_address: None,
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None,
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: wgpu_render_state.clone(),
};
let app = app_creator(&cc).map_err(|err| err.to_string())?;
@@ -88,17 +134,15 @@ impl AppRunner {
storage: Some(Box::new(storage)),
#[cfg(feature = "glow")]
gl: Some(painter.gl().clone()),
gl,
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
wgpu_render_state: painter.render_state(),
#[cfg(all(feature = "wgpu", feature = "glow"))]
wgpu_render_state: None,
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state,
};
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
let needs_repaint: Arc<NeedRepaint> = Arc::new(NeedRepaint::new(web_options.max_fps));
{
let needs_repaint = needs_repaint.clone();
let needs_repaint = Arc::clone(&needs_repaint);
egui_ctx.set_request_repaint_callback(move |info| {
needs_repaint.repaint_after(info.delay.as_secs_f64());
});
@@ -230,8 +274,13 @@ impl AppRunner {
self.app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
self.app.update(egui_ctx, &mut self.frame);
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame);
#[expect(deprecated)]
self.app.update(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame);
});
let egui::FullOutput {
platform_output,
@@ -288,7 +337,7 @@ impl AppRunner {
}
if let Err(err) = self.painter.paint_and_update_textures(
self.app.clear_color(&self.egui_ctx.style().visuals),
self.app.clear_color(&self.egui_ctx.global_style().visuals),
&clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
@@ -304,8 +353,6 @@ impl AppRunner {
}
fn handle_platform_output(&self, platform_output: egui::PlatformOutput) {
#![allow(deprecated)]
#[cfg(feature = "web_screen_reader")]
if self.egui_ctx.options(|o| o.screen_reader) {
super::screen_reader::speak(&platform_output.events_description());
@@ -314,13 +361,10 @@ impl AppRunner {
let egui::PlatformOutput {
commands,
cursor_icon,
open_url,
copied_text,
events: _, // already handled
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
ime,
#[cfg(feature = "accesskit")]
accesskit_update: _, // not currently implemented
accesskit_update: _, // not currently implemented
num_completed_passes: _, // handled by `Context::run`
request_discard_reasons: _, // handled by `Context::run`
} = platform_output;
@@ -336,22 +380,11 @@ impl AppRunner {
egui::OutputCommand::OpenUrl(open_url) => {
super::open_url(&open_url.url, open_url.new_tab);
}
egui::OutputCommand::SetPointerPosition(_) => {
// Not supported on the web.
}
}
}
super::set_cursor_icon(cursor_icon);
if let Some(open) = open_url {
super::open_url(&open.url, open.new_tab);
}
if !copied_text.is_empty() {
super::set_clipboard_text(&copied_text);
}
if self.has_focus() {
// The eframe app has focus.
if ime.is_some() {

View File

@@ -11,9 +11,15 @@ use super::percent_decode;
/// Data gathered between frames.
#[derive(Default)]
pub(crate) struct WebInput {
/// Required because we don't get a position on touched
/// Required because we don't get a position on touchend
pub primary_touch: Option<egui::TouchId>,
/// Helps to track the delta scale from gesture events
pub accumulated_scale: f32,
/// Helps to track the delta rotation from gesture events
pub accumulated_rotation: f32,
/// The raw input to `egui`.
pub raw: egui::RawInput,
}
@@ -50,11 +56,20 @@ impl WebInput {
// ----------------------------------------------------------------------------
/// Stores when to do the next repaint.
pub(crate) struct NeedRepaint(Mutex<f64>);
pub(crate) struct NeedRepaint {
/// Time in seconds when the next repaint should happen.
next_repaint: Mutex<f64>,
impl Default for NeedRepaint {
fn default() -> Self {
Self(Mutex::new(f64::NEG_INFINITY)) // start with a repaint
/// Rate limit for repaint. 0 means "unlimited". The rate may still be limited by vsync.
max_fps: u32,
}
impl NeedRepaint {
pub fn new(max_fps: Option<u32>) -> Self {
Self {
next_repaint: Mutex::new(f64::NEG_INFINITY), // start with a repaint
max_fps: max_fps.unwrap_or(0),
}
}
}
@@ -62,25 +77,43 @@ impl NeedRepaint {
/// Returns the time (in [`now_sec`] scale) when
/// we should next repaint.
pub fn when_to_repaint(&self) -> f64 {
*self.0.lock()
*self.next_repaint.lock()
}
/// Unschedule repainting.
pub fn clear(&self) {
*self.0.lock() = f64::INFINITY;
*self.next_repaint.lock() = f64::INFINITY;
}
pub fn repaint_after(&self, num_seconds: f64) {
let mut repaint_time = self.0.lock();
*repaint_time = repaint_time.min(super::now_sec() + num_seconds);
let mut time = super::now_sec() + num_seconds;
time = self.round_repaint_time_to_rate(time);
let mut repaint_time = self.next_repaint.lock();
*repaint_time = repaint_time.min(time);
}
/// Request a repaint. Depending on the presence of rate limiting, this may not be instant.
pub fn repaint(&self) {
let time = self.round_repaint_time_to_rate(super::now_sec());
let mut repaint_time = self.next_repaint.lock();
*repaint_time = repaint_time.min(time);
}
pub fn repaint_asap(&self) {
*self.next_repaint.lock() = f64::NEG_INFINITY;
}
pub fn needs_repaint(&self) -> bool {
self.when_to_repaint() <= super::now_sec()
}
pub fn repaint_asap(&self) {
*self.0.lock() = f64::NEG_INFINITY;
fn round_repaint_time_to_rate(&self, time: f64) -> f64 {
if self.max_fps == 0 {
time
} else {
let interval = 1.0 / self.max_fps as f64;
(time / interval).ceil() * interval
}
}
}

View File

@@ -1,13 +1,13 @@
use crate::web::string_from_js_value;
use super::{
button_from_mouse_event, location_hash, modifiers_from_kb_event, modifiers_from_mouse_event,
modifiers_from_wheel_event, native_pixels_per_point, pos_from_mouse_event,
prefers_color_scheme_dark, primary_touch_pos, push_touches, text_from_keyboard_event,
theme_from_dark_mode, translate_key, AppRunner, Closure, JsCast, JsValue, WebRunner,
DEBUG_RESIZE,
AppRunner, Closure, DEBUG_RESIZE, JsCast as _, JsValue, WebRunner, button_from_mouse_event,
location_hash, modifiers_from_kb_event, modifiers_from_mouse_event, modifiers_from_wheel_event,
native_pixels_per_point, pos_from_mouse_event, prefers_color_scheme, primary_touch_pos,
push_touches, text_from_keyboard_event, translate_key,
};
use js_sys::Reflect;
use web_sys::{Document, EventTarget, ShadowRoot};
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagation`
@@ -102,6 +102,7 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
install_touchcancel(runner_ref, &canvas)?;
install_wheel(runner_ref, &canvas)?;
install_gesture(runner_ref, &canvas)?;
install_drag_and_drop(runner_ref, &canvas)?;
install_window_events(runner_ref, &window)?;
install_color_scheme_change_event(runner_ref, &window)?;
@@ -136,20 +137,24 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
&& !modifiers.command
// When text agent is focused, it is responsible for handling input events
&& !runner.text_agent.has_focus()
&& let Some(text) = text_from_keyboard_event(&event)
{
if let Some(text) = text_from_keyboard_event(&event) {
let egui_event = egui::Event::Text(text);
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
let egui_event = egui::Event::Text(text);
let should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default =
(runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
// If this is indeed text, then prevent any other action.
// If this is indeed text, then prevent any other action.
if should_prevent_default {
event.prevent_default();
}
// Use web options to tell if the event should be propagated to parent elements.
if !should_propagate {
event.stop_propagation();
}
// Use web options to tell if the event should be propagated to parent elements.
if should_stop_propagation {
event.stop_propagation();
}
}
@@ -158,7 +163,7 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
)
}
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let has_focus = runner.input.raw.focused;
if !has_focus {
@@ -184,24 +189,26 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
repeat: false, // egui will fill this in for us!
modifiers,
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
let prevent_default = should_prevent_default_for_key(runner, &modifiers, egui_key);
// log::debug!(
// "On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}",
// event.key().as_str(),
// runner.egui_ctx().wants_keyboard_input()
// );
if false {
log::debug!(
"On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}",
event.key().as_str(),
runner.egui_ctx().egui_wants_keyboard_input()
);
}
if prevent_default {
event.prevent_default();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
}
@@ -221,9 +228,10 @@ fn should_prevent_default_for_key(
// Prevent cmd/ctrl plus these keys from triggering the default browser action:
let keys = [
egui::Key::O, // open
egui::Key::P, // print (cmd-P is common for command palette)
egui::Key::S, // save
egui::Key::Comma, // cmd-, opens options on macOS, which egui apps may wanna "steal"
egui::Key::O, // open
egui::Key::P, // print (cmd-P is common for command palette)
egui::Key::S, // save
];
for key in keys {
if egui_key == key && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
@@ -256,12 +264,12 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
runner_ref.add_event_listener(target, "keyup", on_keyup)
}
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
let modifiers = modifiers_from_kb_event(&event);
runner.input.raw.modifiers = modifiers;
let mut propagate_event = false;
let mut should_stop_propagation = true;
if let Some(key) = translate_key(&event.key()) {
let egui_event = egui::Event::Key {
@@ -271,7 +279,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
repeat: false,
modifiers,
};
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
}
@@ -282,6 +290,8 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
// See https://github.com/emilk/egui/issues/4724
let keys_down = runner.egui_ctx().input(|i| i.keys_down.clone());
#[expect(clippy::iter_over_hash_type)]
for key in keys_down {
let egui_event = egui::Event::Key {
key,
@@ -290,7 +300,7 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
repeat: false,
modifiers,
};
propagate_event |= (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
}
}
@@ -299,70 +309,89 @@ pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
let has_focus = runner.input.raw.focused;
if has_focus && !propagate_event {
if has_focus && should_stop_propagation {
event.stop_propagation();
}
}
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "paste", |event: web_sys::ClipboardEvent, runner| {
if let Some(data) = event.clipboard_data() {
if let Ok(text) = data.get_data("text") {
let text = text.replace("\r\n", "\n");
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
let mut should_propagate = false;
if !text.is_empty() && runner.input.raw.focused {
let egui_event = egui::Event::Paste(text);
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
}
if let Some(data) = event.clipboard_data()
&& let Ok(text) = data.get_data("text")
{
let text = text.replace("\r\n", "\n");
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
event.stop_propagation();
}
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if !text.is_empty() {
let egui_event = egui::Event::Paste(text);
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if should_stop_propagation {
event.stop_propagation();
}
if should_prevent_default {
event.prevent_default();
}
}
})?;
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Cut);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !(runner.web_options.should_propagate_event)(&egui::Event::Cut) {
if (runner.web_options.should_stop_propagation)(&egui::Event::Cut) {
event.stop_propagation();
}
event.prevent_default();
if (runner.web_options.should_prevent_default)(&egui::Event::Cut) {
event.prevent_default();
}
})?;
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
if runner.input.raw.focused {
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if !runner.input.raw.focused {
return; // The eframe app is not interested
}
runner.input.raw.events.push(egui::Event::Copy);
// In Safari we are only allowed to write to the clipboard during the
// event callback, which is why we run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !(runner.web_options.should_propagate_event)(&egui::Event::Copy) {
if (runner.web_options.should_stop_propagation)(&egui::Event::Copy) {
event.stop_propagation();
}
event.prevent_default();
if (runner.web_options.should_prevent_default)(&egui::Event::Copy) {
event.prevent_default();
}
})?;
Ok(())
@@ -380,7 +409,7 @@ fn install_window_events(runner_ref: &WebRunner, window: &EventTarget) -> Result
// No need to subscribe to "resize": we already subscribe to the canvas
// size using a ResizeObserver, and we also subscribe to DPR changes of the monitor.
for event_name in &["load", "pagehide", "pageshow"] {
for event_name in &["load", "pagehide", "pageshow", "popstate"] {
runner_ref.add_event_listener(window, event_name, move |_: web_sys::Event, runner| {
if DEBUG_RESIZE {
log::debug!("{event_name:?}");
@@ -444,16 +473,19 @@ fn install_color_scheme_change_event(
runner_ref: &WebRunner,
window: &web_sys::Window,
) -> Result<(), JsValue> {
if let Some(media_query_list) = prefers_color_scheme_dark(window)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|event, runner| {
let theme = theme_from_dark_mode(event.matches());
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
},
)?;
for theme in [egui::Theme::Dark, egui::Theme::Light] {
if let Some(media_query_list) = prefers_color_scheme(window, theme)? {
runner_ref.add_event_listener::<web_sys::MediaQueryListEvent>(
&media_query_list,
"change",
|_event, runner| {
if let Some(theme) = super::system_theme() {
runner.input.raw.system_theme = Some(theme);
runner.needs_repaint.repaint_asap();
}
},
)?;
}
}
Ok(())
@@ -484,7 +516,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
|event: web_sys::PointerEvent, runner: &mut AppRunner| {
let modifiers = modifiers_from_mouse_event(&event);
runner.input.raw.modifiers = modifiers;
let mut should_propagate = false;
let mut should_stop_propagation = true;
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
let modifiers = runner.input.raw.modifiers;
@@ -494,7 +526,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
pressed: true,
modifiers,
};
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
runner.input.raw.events.push(egui_event);
// In Safari we are only allowed to write to the clipboard during the
@@ -506,7 +538,7 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
@@ -527,40 +559,44 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
if is_interested_in_pointer_event(
runner,
egui::pos2(event.client_x() as f32, event.client_y() as f32),
) {
if let Some(button) = button_from_mouse_event(&event) {
let modifiers = runner.input.raw.modifiers;
let egui_event = egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
runner.input.raw.events.push(egui_event);
) && let Some(button) = button_from_mouse_event(&event)
{
let modifiers = runner.input.raw.modifiers;
let egui_event = egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
};
let should_stop_propagation =
(runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default =
(runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
// Previously on iOS, the canvas would not receive focus on
// any touch event, which resulted in the on-screen keyboard
// not working when focusing on a text field in an egui app.
// This attempts to fix that by forcing the focus on any
// click on the canvas.
runner.canvas().focus().ok();
// Previously on iOS, the canvas would not receive focus on
// any touch event, which resulted in the on-screen keyboard
// not working when focusing on a text field in an egui app.
// This attempts to fix that by forcing the focus on any
// click on the canvas.
runner.canvas().focus().ok();
// In Safari we are only allowed to do certain things
// (like playing audio, start a download, etc)
// on user action, such as a click.
// So we need to run the app logic here and now:
runner.logic();
// In Safari we are only allowed to do certain things
// (like playing audio, start a download, etc)
// on user action, such as a click.
// So we need to run the app logic here and now:
runner.logic();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
// Make sure we paint the output of the above logic call asap:
runner.needs_repaint.repaint_asap();
if should_prevent_default {
event.prevent_default();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
event.stop_propagation();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if should_stop_propagation {
event.stop_propagation();
}
}
},
@@ -600,15 +636,19 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
egui::pos2(event.client_x() as f32, event.client_y() as f32),
) {
let egui_event = egui::Event::PointerMoved(pos);
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint_asap();
runner.needs_repaint.repaint();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
}
})
}
@@ -622,10 +662,13 @@ fn install_mouseleave(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !(runner.web_options.should_propagate_event)(&egui::Event::PointerGone) {
if (runner.web_options.should_stop_propagation)(&egui::Event::PointerGone) {
event.stop_propagation();
}
event.prevent_default();
if (runner.web_options.should_prevent_default)(&egui::Event::PointerGone) {
event.prevent_default();
}
},
)
}
@@ -635,7 +678,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
target,
"touchstart",
|event: web_sys::TouchEvent, runner| {
let mut should_propagate = false;
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if let Some((pos, _)) = primary_touch_pos(runner, &event) {
let egui_event = egui::Event::PointerButton {
pos,
@@ -643,7 +687,8 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
pressed: true,
modifiers: runner.input.raw.modifiers,
};
should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
}
@@ -651,32 +696,39 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
if should_prevent_default {
event.prevent_default();
}
},
)
}
fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "touchmove", |event: web_sys::TouchEvent, runner| {
if let Some((pos, touch)) = primary_touch_pos(runner, &event) {
if is_interested_in_pointer_event(
if let Some((pos, touch)) = primary_touch_pos(runner, &event)
&& is_interested_in_pointer_event(
runner,
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
) {
let egui_event = egui::Event::PointerMoved(pos);
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
runner.input.raw.events.push(egui_event);
)
{
let egui_event = egui::Event::PointerMoved(pos);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
push_touches(runner, egui::TouchPhase::Move, &event);
runner.needs_repaint.repaint_asap();
push_touches(runner, egui::TouchPhase::Move, &event);
runner.needs_repaint.repaint();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
event.stop_propagation();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if should_stop_propagation {
event.stop_propagation();
}
if should_prevent_default {
event.prevent_default();
}
}
@@ -685,42 +737,49 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, runner| {
if let Some((pos, touch)) = primary_touch_pos(runner, &event) {
if is_interested_in_pointer_event(
if let Some((pos, touch)) = primary_touch_pos(runner, &event)
&& is_interested_in_pointer_event(
runner,
egui::pos2(touch.client_x() as f32, touch.client_y() as f32),
) {
// First release mouse to click:
let mut should_propagate = false;
let egui_event = egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: runner.input.raw.modifiers,
};
should_propagate |= (runner.web_options.should_propagate_event)(&egui_event);
runner.input.raw.events.push(egui_event);
// Then remove hover effect:
should_propagate |=
(runner.web_options.should_propagate_event)(&egui::Event::PointerGone);
runner.input.raw.events.push(egui::Event::PointerGone);
)
{
// First release mouse to click:
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
let egui_event = egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers: runner.input.raw.modifiers,
};
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
// Then remove hover effect:
should_stop_propagation &=
(runner.web_options.should_stop_propagation)(&egui::Event::PointerGone);
should_prevent_default &=
(runner.web_options.should_prevent_default)(&egui::Event::PointerGone);
runner.input.raw.events.push(egui::Event::PointerGone);
push_touches(runner, egui::TouchPhase::End, &event);
push_touches(runner, egui::TouchPhase::End, &event);
runner.needs_repaint.repaint_asap();
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
event.stop_propagation();
}
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if should_stop_propagation {
event.stop_propagation();
}
if should_prevent_default {
event.prevent_default();
}
// Fix virtual keyboard IOS
// Need call focus at the same time of event
if runner.text_agent.has_focus() {
runner.text_agent.set_focus(false);
runner.text_agent.set_focus(true);
}
// Fix virtual keyboard IOS
// Need call focus at the same time of event
if runner.text_agent.has_focus() {
runner.text_agent.set_focus(false);
runner.text_agent.set_focus(true);
}
}
})
@@ -755,7 +814,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl {
// The browser is saying the ctrl key is down, but it isn't _really_.
// This happens on pinch-to-zoom on a Mac trackpad.
// This happens on pinch-to-zoom on multitouch trackpads
// egui will treat ctrl+scroll as zoom, so it all works.
// However, we explicitly handle it here in order to better match the pinch-to-zoom
// speed of a native app, without being sensitive to egui's `scroll_zoom_speed` setting.
@@ -767,19 +826,91 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV
unit,
delta,
modifiers,
phase: egui::TouchPhase::Move,
}
};
let should_propagate = (runner.web_options.should_propagate_event)(&egui_event);
let should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event);
let should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event);
runner.input.raw.events.push(egui_event);
runner.needs_repaint.repaint();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if should_stop_propagation {
event.stop_propagation();
}
if should_prevent_default {
event.prevent_default();
}
})
}
fn install_gesture(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
runner_ref.add_event_listener(target, "gesturestart", |event: web_sys::Event, runner| {
runner.input.accumulated_scale = 1.0;
runner.input.accumulated_rotation = 0.0;
handle_gesture(event, runner);
})?;
runner_ref.add_event_listener(target, "gesturechange", handle_gesture)?;
runner_ref.add_event_listener(target, "gestureend", |event: web_sys::Event, runner| {
handle_gesture(event, runner);
runner.input.accumulated_scale = 1.0;
runner.input.accumulated_rotation = 0.0;
})?;
Ok(())
}
#[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
fn handle_gesture(event: web_sys::Event, runner: &mut AppRunner) {
// GestureEvent is a non-standard API, so this attempts to get the relevant fields if they exist.
let new_scale = Reflect::get(&event, &JsValue::from_str("scale"))
.ok()
.and_then(|scale| scale.as_f64())
.map_or(1.0, |scale| scale as f32);
let new_rotation = Reflect::get(&event, &JsValue::from_str("rotation"))
.ok()
.and_then(|rotation| rotation.as_f64())
.map_or(0.0, |rotation| rotation.to_radians() as f32);
let scale_delta = new_scale / runner.input.accumulated_scale;
let rotation_delta = new_rotation - runner.input.accumulated_rotation;
runner.input.accumulated_scale *= scale_delta;
runner.input.accumulated_rotation += rotation_delta;
let mut should_stop_propagation = true;
let mut should_prevent_default = true;
if scale_delta != 1.0 {
let zoom_event = egui::Event::Zoom(scale_delta);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&zoom_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&zoom_event);
runner.input.raw.events.push(zoom_event);
}
if rotation_delta != 0.0 {
let rotate_event = egui::Event::Rotate(rotation_delta);
should_stop_propagation &= (runner.web_options.should_stop_propagation)(&rotate_event);
should_prevent_default &= (runner.web_options.should_prevent_default)(&rotate_event);
runner.input.raw.events.push(rotate_event);
}
if scale_delta != 1.0 || rotation_delta != 0.0 {
runner.needs_repaint.repaint_asap();
// Use web options to tell if the web event should be propagated to parent elements based on the egui event.
if !should_propagate {
if should_stop_propagation {
event.stop_propagation();
}
event.prevent_default();
})
if should_prevent_default {
// Prevents a simulated ctrl-scroll event for zoom
event.prevent_default();
}
}
}
fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
@@ -863,7 +994,10 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
}
}
Err(err) => {
log::error!("Failed to read file: {:?}", err);
log::error!(
"Failed to read file: {}",
string_from_js_value(&err)
);
}
}
};
@@ -932,7 +1066,7 @@ impl ResizeObserverContext {
// we rely on the resize observer to trigger the first `request_animation_frame`:
if let Err(err) = runner_ref.request_animation_frame() {
log::error!("{}", super::string_from_js_value(&err));
};
}
} else {
log::warn!("ResizeObserverContext callback: failed to lock runner");
}

View File

@@ -1,4 +1,4 @@
use super::{canvas_content_rect, AppRunner};
use super::{AppRunner, canvas_content_rect};
pub fn pos_from_mouse_event(
canvas: &web_sys::HtmlCanvasElement,

View File

@@ -1,6 +1,7 @@
//! [`egui`] bindings for web apps (compiling to WASM).
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
#![expect(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
#![expect(clippy::unwrap_used)] // TODO(emilk): remove unwraps
mod app_runner;
mod backend;
@@ -23,23 +24,20 @@ pub use panic_handler::{PanicHandler, PanicSummary};
pub use web_logger::WebLogger;
pub use web_runner::WebRunner;
#[cfg(not(any(feature = "glow", feature = "wgpu")))]
#[cfg(not(any(feature = "glow", feature = "wgpu_no_default_features")))]
compile_error!("You must enable either the 'glow' or 'wgpu' feature");
mod web_painter;
#[cfg(feature = "glow")]
mod web_painter_glow;
#[cfg(feature = "glow")]
pub(crate) type ActiveWebPainter = web_painter_glow::WebPainterGlow;
#[cfg(feature = "wgpu")]
#[cfg(feature = "wgpu_no_default_features")]
mod web_painter_wgpu;
#[cfg(all(feature = "wgpu", not(feature = "glow")))]
pub(crate) type ActiveWebPainter = web_painter_wgpu::WebPainterWgpu;
pub use backend::*;
use egui::Theme;
use wasm_bindgen::prelude::*;
use web_sys::{Document, MediaQueryList, Node};
@@ -113,24 +111,31 @@ pub fn native_pixels_per_point() -> f32 {
///
/// `None` means unknown.
pub fn system_theme() -> Option<egui::Theme> {
let dark_mode = prefers_color_scheme_dark(&web_sys::window()?)
.ok()??
.matches();
Some(theme_from_dark_mode(dark_mode))
}
fn prefers_color_scheme_dark(window: &web_sys::Window) -> Result<Option<MediaQueryList>, JsValue> {
window.match_media("(prefers-color-scheme: dark)")
}
fn theme_from_dark_mode(dark_mode: bool) -> egui::Theme {
if dark_mode {
egui::Theme::Dark
let window = web_sys::window()?;
if does_prefer_color_scheme(&window, Theme::Dark) == Some(true) {
Some(Theme::Dark)
} else if does_prefer_color_scheme(&window, Theme::Light) == Some(true) {
Some(Theme::Light)
} else {
egui::Theme::Light
None
}
}
fn does_prefer_color_scheme(window: &web_sys::Window, theme: Theme) -> Option<bool> {
Some(prefers_color_scheme(window, theme).ok()??.matches())
}
fn prefers_color_scheme(
window: &web_sys::Window,
theme: Theme,
) -> Result<Option<MediaQueryList>, JsValue> {
let theme = match theme {
Theme::Dark => "dark",
Theme::Light => "light",
};
window.match_media(format!("(prefers-color-scheme: {theme})").as_str())
}
/// Returns the canvas in client coordinates.
fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
let bounding_rect = canvas.get_bounding_client_rect();
@@ -141,18 +146,18 @@ fn canvas_content_rect(canvas: &web_sys::HtmlCanvasElement) -> egui::Rect {
);
// We need to subtract padding and border:
if let Some(window) = web_sys::window() {
if let Ok(Some(style)) = window.get_computed_style(canvas) {
let get_property = |name: &str| -> Option<f32> {
let property = style.get_property_value(name).ok()?;
property.trim_end_matches("px").parse::<f32>().ok()
};
if let Some(window) = web_sys::window()
&& let Ok(Some(style)) = window.get_computed_style(canvas)
{
let get_property = |name: &str| -> Option<f32> {
let property = style.get_property_value(name).ok()?;
property.trim_end_matches("px").parse::<f32>().ok()
};
rect.min.x += get_property("padding-left").unwrap_or_default();
rect.min.y += get_property("padding-top").unwrap_or_default();
rect.max.x -= get_property("padding-right").unwrap_or_default();
rect.max.y -= get_property("padding-bottom").unwrap_or_default();
}
rect.min.x += get_property("padding-left").unwrap_or_default();
rect.min.y += get_property("padding-top").unwrap_or_default();
rect.max.x -= get_property("padding-right").unwrap_or_default();
rect.max.y -= get_property("padding-bottom").unwrap_or_default();
}
rect
@@ -280,8 +285,8 @@ fn create_clipboard_item(mime: &str, bytes: &[u8]) -> Result<web_sys::ClipboardI
let items = js_sys::Object::new();
#[expect(unsafe_code, unused_unsafe)] // Weird false positive
// SAFETY: I hope so
#[allow(unsafe_code, unused_unsafe)] // Weird false positive
unsafe {
js_sys::Reflect::set(&items, &JsValue::from_str(mime), &blob)?
};

View File

@@ -179,7 +179,7 @@ impl TextAgent {
if let Err(err) = self.input.focus() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
};
}
}
pub fn blur(&self) {
@@ -191,7 +191,7 @@ impl TextAgent {
if let Err(err) = self.input.blur() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
};
}
}
}

View File

@@ -38,7 +38,7 @@ impl log::Log for WebLogger {
}
fn log(&self, record: &log::Record<'_>) {
#![allow(clippy::match_same_arms)]
#![expect(clippy::match_same_arms)]
if !self.enabled(record.metadata()) {
return;
@@ -110,7 +110,7 @@ mod console {
/// * `tokio-1.24.1/src/runtime/runtime.rs`
/// * `rerun/src/main.rs`
/// * `core/src/ops/function.rs`
#[allow(dead_code)] // only used on web and in tests
#[allow(clippy::allow_attributes, dead_code)] // only used on web and in tests
fn shorten_file_path(file_path: &str) -> &str {
if let Some(i) = file_path.rfind("/src/") {
if let Some(prev_slash) = file_path[..i].rfind('/') {
@@ -126,12 +126,17 @@ fn shorten_file_path(file_path: &str) -> &str {
#[test]
fn test_shorten_file_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
assert_eq!(shorten_file_path(before), after);
}
}

View File

@@ -1,7 +1,7 @@
use egui::{Event, UserData, ViewportId};
use egui_glow::glow;
use std::sync::Arc;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsCast as _;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
@@ -20,14 +20,15 @@ impl WebPainterGlow {
self.painter.gl()
}
pub async fn new(
pub fn new(
_ctx: egui::Context,
canvas: HtmlCanvasElement,
options: &WebOptions,
) -> Result<Self, String> {
let (gl, shader_prefix) =
init_glow_context_from_canvas(&canvas, options.webgl_context_option)?;
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
let gl = std::sync::Arc::new(gl);
let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering)
@@ -90,7 +91,7 @@ impl WebPainter for WebPainterGlow {
for data in data {
events.push(Event::Screenshot {
viewport_id: ViewportId::default(),
image: image.clone(),
image: Arc::clone(&image),
user_data: data,
});
}
@@ -191,17 +192,13 @@ fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
.get_extension("WEBGL_debug_renderer_info")
.unwrap()
.is_some()
{
if let Ok(renderer) =
&& let Ok(renderer) =
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
{
if let Some(renderer) = renderer.as_string() {
if renderer.contains("Apple") {
return true;
}
}
}
&& let Some(renderer) = renderer.as_string()
&& renderer.contains("Apple")
{
true
} else {
false
}
false
}

View File

@@ -1,20 +1,22 @@
use std::sync::Arc;
use super::web_painter::WebPainter;
use crate::WebOptions;
use egui::{Event, UserData, ViewportId};
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use egui_wgpu::{RenderState, SurfaceErrorAction};
use egui_wgpu::{
RenderState, SurfaceErrorAction,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
};
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
depth_format: Option<wgpu::TextureFormat>,
depth_stencil_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
screen_capture_state: Option<CaptureState>,
capture_tx: CaptureSender,
@@ -23,7 +25,6 @@ pub(crate) struct WebPainterWgpu {
}
impl WebPainterWgpu {
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub fn render_state(&self) -> Option<RenderState> {
self.render_state.clone()
}
@@ -35,7 +36,7 @@ impl WebPainterWgpu {
height_in_pixels: u32,
) -> Option<wgpu::TextureView> {
let device = &render_state.device;
self.depth_format.map(|depth_format| {
self.depth_stencil_format.map(|depth_stencil_format| {
device
.create_texture(&wgpu::TextureDescriptor {
label: Some("egui_depth_texture"),
@@ -47,19 +48,18 @@ impl WebPainterWgpu {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: depth_format,
format: depth_stencil_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[depth_format],
view_formats: &[depth_stencil_format],
})
.create_view(&wgpu::TextureViewDescriptor::default())
})
}
#[allow(unused)] // only used if `wgpu` is the only active feature.
pub async fn new(
ctx: egui::Context,
canvas: web_sys::HtmlCanvasElement,
options: &WebOptions,
options: &crate::WebOptions,
) -> Result<Self, String> {
log::debug!("Creating wgpu painter");
@@ -68,15 +68,17 @@ impl WebPainterWgpu {
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
let depth_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let render_state = RenderState::create(
&options.wgpu_options,
&instance,
Some(&surface),
depth_format,
1,
options.dithering,
egui_wgpu::RendererOptions {
dithering: options.dithering,
depth_stencil_format,
..Default::default()
},
)
.await
.map_err(|err| err.to_string())?;
@@ -101,9 +103,9 @@ impl WebPainterWgpu {
render_state: Some(render_state),
surface,
surface_configuration,
depth_format,
depth_stencil_format,
depth_texture_view: None,
on_surface_error: options.wgpu_options.on_surface_error.clone(),
on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _,
screen_capture_state: None,
capture_tx,
capture_rx,
@@ -236,17 +238,31 @@ impl WebPainter for WebPainterWgpu {
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.as_ref().map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
// It is very unlikely that the depth buffer is needed after egui finished rendering
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
store: wgpu::StoreOp::Discard,
}),
stencil_ops: None,
depth_ops: self
.depth_stencil_format
.is_some_and(|depth_stencil_format| {
depth_stencil_format.has_depth_aspect()
})
.then_some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
// It is very unlikely that the depth buffer is needed after egui finished rendering
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
store: wgpu::StoreOp::Discard,
}),
stencil_ops: self
.depth_stencil_format
.is_some_and(|depth_stencil_format| {
depth_stencil_format.has_stencil_aspect()
})
.then_some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0),
store: wgpu::StoreOp::Discard,
}),
}
}),
label: Some("egui_render"),
@@ -266,47 +282,48 @@ impl WebPainter for WebPainterWgpu {
let mut capture_buffer = None;
if capture {
if let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
};
if capture && let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
Some((output_frame, capture_buffer))
};
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
}
}
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
.submit(user_cmd_bufs.into_iter().chain([encoder.finish()]));
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
if let Some(capture_buffer) = capture_buffer {
if let Some(capture_state) = &self.screen_capture_state {
capture_state.read_screen_rgba(
self.ctx.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
ViewportId::ROOT,
);
}
if let Some(capture_buffer) = capture_buffer
&& let Some(capture_state) = &self.screen_capture_state
{
capture_state.read_screen_rgba(
self.ctx.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
ViewportId::ROOT,
);
}
frame.present();
}
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
// 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);
}
}
Ok(())
}
@@ -317,7 +334,7 @@ impl WebPainter for WebPainterWgpu {
events.push(Event::Screenshot {
viewport_id,
user_data: data,
image: screenshot.clone(),
image: Arc::clone(&screenshot),
});
}
}

View File

@@ -2,12 +2,12 @@ use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::prelude::*;
use crate::{epi, App};
use crate::{App, epi};
use super::{
AppRunner, PanicHandler,
events::{self, ResizeObserverContext},
text_agent::TextAgent,
AppRunner, PanicHandler,
};
/// This is how `eframe` runs your web application
@@ -37,7 +37,7 @@ pub struct WebRunner {
impl WebRunner {
/// Will install a panic handler that will catch and log any panics
#[allow(clippy::new_without_default)]
#[expect(clippy::new_without_default)]
pub fn new() -> Self {
let panic_handler = PanicHandler::install();
@@ -280,7 +280,7 @@ struct TargetEvent {
closure: Closure<dyn FnMut(web_sys::Event)>,
}
#[allow(unused)]
#[expect(unused)]
struct IntervalHandle {
handle: i32,
closure: Closure<dyn FnMut()>,
@@ -289,7 +289,7 @@ struct IntervalHandle {
enum EventToUnsubscribe {
TargetEvent(TargetEvent),
#[allow(unused)]
#[expect(unused)]
IntervalHandle(IntervalHandle),
}

View File

@@ -6,6 +6,42 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.3 - 2025-12-11
Nothing new
## 0.33.2 - 2025-11-13
* Fix jittering during window resize on MacOS for WGPU/Metal [#7641](https://github.com/emilk/egui/pull/7641) by [@aspcartman](https://github.com/aspcartman)
## 0.33.0 - 2025-10-09
### 🔧 Changed
* Update wgpu to 26 and wasm-bindgen to 0.2.100 [#7540](https://github.com/emilk/egui/pull/7540) by [@Kumpelinus](https://github.com/Kumpelinus)
* Warn if `DYLD_LIBRARY_PATH` is set and we find no wgpu adapter [#7572](https://github.com/emilk/egui/pull/7572) by [@emilk](https://github.com/emilk)
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
* Update wgpu to 27.0.0 [#7580](https://github.com/emilk/egui/pull/7580) by [@Wumpf](https://github.com/Wumpf)
* Create `egui_wgpu::RendererOptions` [#7601](https://github.com/emilk/egui/pull/7601) by [@emilk](https://github.com/emilk)
* Use software texture filtering in kittest [#7602](https://github.com/emilk/egui/pull/7602) by [@emilk](https://github.com/emilk)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
* Enable wgpu default features in eframe / egui_wgpu default features [#7344](https://github.com/emilk/egui/pull/7344) by [@lucasmerlin](https://github.com/lucasmerlin)
## 0.32.0 - 2025-07-10
* Update to wgpu 25 [#6744](https://github.com/emilk/egui/pull/6744) by [@torokati44](https://github.com/torokati44)
* Free textures after submitting queue instead of before with wgpu renderer on Web [#7291](https://github.com/emilk/egui/pull/7291) by [@Wumpf](https://github.com/Wumpf)
* Improve texture filtering by doing it in gamma space [#7311](https://github.com/emilk/egui/pull/7311) by [@emilk](https://github.com/emilk)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -9,19 +9,13 @@ authors = [
]
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"**/*.wgsl",
"Cargo.toml",
]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
[lints]
workspace = true
@@ -31,10 +25,13 @@ all-features = true
rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["fragile-send-sync-non-atomic-wasm"]
default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
## Enables the `capture` module for capturing screenshots.
capture = ["dep:egui"]
## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11`
winit = ["dep:winit", "winit/rwh_06"]
winit = ["dep:winit", "winit/rwh_06", "dep:egui", "capture"]
## Enables Wayland support for winit.
wayland = ["winit?/wayland"]
@@ -43,14 +40,16 @@ wayland = ["winit?/wayland"]
x11 = ["winit?/x11"]
## Make the renderer `Sync` on wasm, exploiting that by default wasm isn't multithreaded.
## It may make code easier, expecially when targeting both native and web.
## It may make code easier, especially when targeting both native and web.
## On native most wgpu objects are send and sync, on the web they are not (by nature of the WebGPU specification).
## This is not supported in [multithreaded WASM](https://gpuweb.github.io/gpuweb/explainer/#multithreading-transfer).
## Thus that usage is guarded against with compiler errors in wgpu.
fragile-send-sync-non-atomic-wasm = ["wgpu/fragile-send-sync-non-atomic-wasm"]
## Enables `present_with_transaction` surface flag temporary during window resize on MacOS.
macos-window-resize-jitter-fix = ["wgpu/metal"]
[dependencies]
egui = { workspace = true, default-features = false }
epaint = { workspace = true, default-features = false, features = ["bytemuck"] }
ahash.workspace = true
@@ -65,4 +64,5 @@ wgpu = { workspace = true, features = ["wgsl"] }
# Optional dependencies:
egui = { workspace = true, optional = true, default-features = false }
winit = { workspace = true, optional = true, default-features = false }

View File

@@ -1,9 +1,10 @@
use egui::{UserData, ViewportId};
use epaint::ColorImage;
use std::sync::{mpsc, Arc};
use std::sync::{Arc, mpsc};
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
/// A texture and a buffer for reading the rendered frame back to the cpu.
///
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
/// flag for the surface texture on all platforms. This means that anytime we want to
/// capture the frame, we first render it to this texture, and then we can copy it to
@@ -125,7 +126,7 @@ impl CaptureState {
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
// it might make sense to revisit this and implement a more efficient solution.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("egui_screen_capture_buffer"),
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
@@ -159,6 +160,7 @@ impl CaptureState {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
@@ -184,9 +186,9 @@ impl CaptureState {
tx: CaptureSender,
viewport_id: ViewportId,
) {
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
let buffer = Arc::new(buffer);
let buffer_clone = buffer.clone();
let buffer_clone = Arc::clone(&buffer);
let buffer_slice = buffer_clone.slice(..);
let format = self.texture.format();
let tex_extent = self.texture.size();
@@ -195,13 +197,15 @@ impl CaptureState {
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
_ => {
log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format);
log::error!(
"Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}"
);
return;
}
};
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
if let Err(err) = result {
log::error!("Failed to map buffer for reading: {:?}", err);
log::error!("Failed to map buffer for reading: {err}");
return;
}
let buffer_slice = buffer.slice(..);
@@ -226,10 +230,10 @@ impl CaptureState {
tx.send((
viewport_id,
data,
ColorImage {
size: [tex_extent.width as usize, tex_extent.height as usize],
ColorImage::new(
[tex_extent.width as usize, tex_extent.height as usize],
pixels,
},
),
))
.ok();
ctx.request_repaint();

View File

@@ -8,10 +8,13 @@ struct VertexOutput {
struct Locals {
screen_size: vec2<f32>,
dithering: u32, // 1 if dithering is enabled, 0 otherwise
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: u32,
/// 1 if dithering is enabled, 0 otherwise
dithering: u32,
/// 1 to do manual filtering for more predictable kittest snapshot images.
/// See also https://github.com/emilk/egui/issues/5295
predictable_texture_filtering: u32,
};
@group(0) @binding(0) var<uniform> r_locals: Locals;
@@ -95,11 +98,42 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@group(1) @binding(1) var r_tex_sampler: sampler;
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 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);
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
// Load at pixel centers
let tl = textureLoad(r_tex_color, p00, 0);
let tr = textureLoad(r_tex_color, p10, 0);
let bl = textureLoad(r_tex_color, p01, 0);
let br = textureLoad(r_tex_color, p11, 0);
// Manual bilinear interpolation
let top = mix(tl, tr, pixel_fract.x);
let bottom = mix(bl, br, pixel_fract.x);
return mix(top, bottom, pixel_fract.y);
}
}
@fragment
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = sample_texture(in);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.
@@ -115,9 +149,8 @@ fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
@fragment
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
// We always have an sRGB aware texture at the moment.
let tex_linear = textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
let tex_gamma = gamma_from_linear_rgba(tex_linear);
// We expect "normal" textures that are NOT sRGB-aware.
let tex_gamma = sample_texture(in);
var out_color_gamma = in.color * tex_gamma;
// Dither the float color down to eight bits to reduce banding.
// This step is optional for egui backends.

View File

@@ -16,8 +16,6 @@
#![doc = document_features::document_features!()]
//!
#![allow(unsafe_code)]
pub use wgpu;
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
@@ -29,6 +27,7 @@ pub use renderer::*;
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
/// Helpers for capturing screenshots of the UI.
#[cfg(feature = "capture")]
pub mod capture;
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
@@ -42,8 +41,11 @@ use epaint::mutex::RwLock;
/// An error produced by egui-wgpu.
#[derive(thiserror::Error, Debug)]
pub enum WgpuError {
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
NoSuitableAdapterFound(String),
#[error(transparent)]
RequestAdapterError(#[from] wgpu::RequestAdapterError),
#[error("Adapter selection failed: {0}")]
CustomNativeAdapterSelectionError(String),
#[error("There was no valid format for the surface at all.")]
NoSurfaceFormatsAvailable,
@@ -89,7 +91,7 @@ async fn request_adapter(
instance: &wgpu::Instance,
power_preference: wgpu::PowerPreference,
compatible_surface: Option<&wgpu::Surface<'_>>,
_available_adapters: &[wgpu::Adapter],
available_adapters: &[wgpu::Adapter],
) -> Result<wgpu::Adapter, WgpuError> {
profiling::function_scope!();
@@ -104,48 +106,58 @@ async fn request_adapter(
force_fallback_adapter: false,
})
.await
.ok_or_else(|| {
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.is_empty() {
log::info!("No wgpu adapters found");
} else if _available_adapters.len() == 1 {
.inspect_err(|_err| {
if cfg!(target_arch = "wasm32") {
// Nothing to add here
} else if available_adapters.is_empty() {
if std::env::var("DYLD_LIBRARY_PATH").is_ok() {
// DYLD_LIBRARY_PATH can sometimes lead to loading dylibs that cause
// us to find zero adapters. Very strange.
// I don't want to debug this again.
// See https://github.com/rerun-io/rerun/issues/11351 for more
log::warn!(
"No wgpu adapter found. This could be because DYLD_LIBRARY_PATH causes dylibs to be loaded that interfere with Metal device creation. Try restarting with DYLD_LIBRARY_PATH=''"
);
} else {
log::info!("No wgpu adapter found");
}
} else if available_adapters.len() == 1 {
log::info!(
"The only available wgpu adapter was not suitable: {}",
adapter_info_summary(&_available_adapters[0].get_info())
adapter_info_summary(&available_adapters[0].get_info())
);
} else {
log::info!(
"No suitable wgpu adapter found out of the {} available ones: {}",
_available_adapters.len(),
describe_adapters(_available_adapters)
available_adapters.len(),
describe_adapters(available_adapters)
);
}
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
})?;
#[cfg(target_arch = "wasm32")]
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
_available_adapters.len(),
describe_adapters(_available_adapters)
);
if cfg!(target_arch = "wasm32") {
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
// native:
if available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
available_adapters.len(),
describe_adapters(available_adapters)
);
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
}
}
Ok(adapter)
@@ -160,9 +172,7 @@ impl RenderState {
config: &WgpuConfiguration,
instance: &wgpu::Instance,
compatible_surface: Option<&wgpu::Surface<'static>>,
depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
options: RendererOptions,
) -> Result<Self, WgpuError> {
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
@@ -184,7 +194,6 @@ impl RenderState {
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
trace_path,
}) => {
let adapter = {
#[cfg(target_arch = "wasm32")]
@@ -194,7 +203,7 @@ impl RenderState {
#[cfg(not(target_arch = "wasm32"))]
if let Some(native_adapter_selector) = _native_adapter_selector {
native_adapter_selector(&available_adapters, compatible_surface)
.map_err(WgpuError::NoSuitableAdapterFound)
.map_err(WgpuError::CustomNativeAdapterSelectionError)
} else {
request_adapter(
instance,
@@ -209,7 +218,7 @@ impl RenderState {
let (device, queue) = {
profiling::scope!("request_device");
adapter
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
.request_device(&(*device_descriptor)(&adapter))
.await?
};
@@ -232,17 +241,11 @@ impl RenderState {
};
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
let renderer = Renderer::new(
&device,
target_format,
depth_format,
msaa_samples,
dithering,
);
let renderer = Renderer::new(&device, target_format, options);
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)]
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
Ok(Self {
adapter,
#[cfg(not(target_arch = "wasm32"))]
@@ -255,7 +258,6 @@ impl RenderState {
}
}
#[cfg(not(target_arch = "wasm32"))]
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
if adapters.is_empty() {
"(none)".to_owned()

View File

@@ -1,9 +1,10 @@
#![allow(unsafe_code)]
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
use bytemuck::Zeroable as _;
use epaint::{PaintCallbackInfo, Primitive, Vertex, emath::NumExt as _};
use wgpu::util::DeviceExt as _;
@@ -84,7 +85,7 @@ impl Callback {
///
/// # Example
///
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/main/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
pub trait CallbackTrait: Send + Sync {
fn prepare(
&self,
@@ -140,21 +141,16 @@ impl ScreenDescriptor {
}
/// Uniform buffer used when rendering.
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct UniformBuffer {
screen_size_in_points: [f32; 2],
dithering: u32,
// Uniform buffers need to be at least 16 bytes in WebGL.
// See https://github.com/gfx-rs/wgpu/issues/2072
_padding: u32,
}
impl PartialEq for UniformBuffer {
fn eq(&self, other: &Self) -> bool {
self.screen_size_in_points == other.screen_size_in_points
&& self.dithering == other.dithering
}
/// 1 to do manual filtering for more predictable kittest snapshot images.
///
/// See also <https://github.com/emilk/egui/issues/5295>.
predictable_texture_filtering: u32,
}
struct SlicedBuffer {
@@ -175,6 +171,69 @@ pub struct Texture {
pub options: Option<epaint::textures::TextureOptions>,
}
/// Ways to configure [`Renderer`] during creation.
#[derive(Clone, Copy, Debug)]
pub struct RendererOptions {
/// Set the level of the multisampling anti-aliasing (MSAA).
///
/// Must be a power-of-two. Higher = more smooth 3D.
///
/// A value of `0` or `1` turns it off (default).
///
/// `egui` already performs anti-aliasing via "feathering"
/// (controlled by [`egui::epaint::TessellationOptions`]),
/// but if you are embedding 3D in egui you may want to turn on multisampling.
pub msaa_samples: u32,
/// What format to use for the depth and stencil buffers,
/// e.g. [`wgpu::TextureFormat::Depth32FloatStencil8`].
///
/// egui doesn't need depth/stencil, so the default value is `None` (no depth or stancil buffers).
pub depth_stencil_format: Option<wgpu::TextureFormat>,
/// Controls whether to apply dithering to minimize banding artifacts.
///
/// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
/// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
/// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
///
/// Defaults to true.
pub dithering: bool,
/// Perform texture filtering in software?
///
/// This is useful when you want predictable rendering across
/// different hardware, e.g. for kittest snapshots.
///
/// Default is `false`.
///
/// See also <https://github.com/emilk/egui/issues/5295>.
pub predictable_texture_filtering: bool,
}
impl RendererOptions {
/// Set options that produce the most predicatable output.
///
/// Useful for image snapshot tests.
pub const PREDICTABLE: Self = Self {
msaa_samples: 1,
depth_stencil_format: None,
dithering: false,
predictable_texture_filtering: true,
};
}
impl Default for RendererOptions {
fn default() -> Self {
Self {
msaa_samples: 0,
depth_stencil_format: None,
dithering: true,
predictable_texture_filtering: false,
}
}
}
/// Renderer for a egui based GUI.
pub struct Renderer {
pipeline: wgpu::RenderPipeline,
@@ -194,7 +253,7 @@ pub struct Renderer {
next_user_texture_id: u64,
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
dithering: bool,
options: RendererOptions,
/// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
///
@@ -210,9 +269,7 @@ impl Renderer {
pub fn new(
device: &wgpu::Device,
output_color_format: wgpu::TextureFormat,
output_depth_format: Option<wgpu::TextureFormat>,
msaa_samples: u32,
dithering: bool,
options: RendererOptions,
) -> Self {
profiling::function_scope!();
@@ -229,8 +286,8 @@ impl Renderer {
label: Some("egui_uniform_buffer"),
contents: bytemuck::cast_slice(&[UniformBuffer {
screen_size_in_points: [0.0, 0.0],
dithering: u32::from(dithering),
_padding: Default::default(),
dithering: u32::from(options.dithering),
predictable_texture_filtering: u32::from(options.predictable_texture_filtering),
}]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
@@ -299,13 +356,15 @@ impl Renderer {
push_constant_ranges: &[],
});
let depth_stencil = output_depth_format.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
let depth_stencil = options
.depth_stencil_format
.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
let pipeline = {
profiling::scope!("create_render_pipeline");
@@ -337,14 +396,14 @@ impl Renderer {
depth_stencil,
multisample: wgpu::MultisampleState {
alpha_to_coverage_enabled: false,
count: msaa_samples,
count: options.msaa_samples.max(1),
mask: !0,
},
fragment: Some(wgpu::FragmentState {
module: &module,
entry_point: Some(if output_color_format.is_srgb() {
log::warn!("Detected a linear (sRGBA aware) framebuffer {:?}. egui prefers Rgba8Unorm or Bgra8Unorm", output_color_format);
log::warn!("Detected a linear (sRGBA aware) framebuffer {output_color_format:?}. egui prefers Rgba8Unorm or Bgra8Unorm");
"fs_main_linear_framebuffer"
} else {
"fs_main_gamma_framebuffer" // this is what we prefer
@@ -392,17 +451,13 @@ impl Renderer {
},
uniform_buffer,
// Buffers on wgpu are zero initialized, so this is indeed its current state!
previous_uniform_buffer_content: UniformBuffer {
screen_size_in_points: [0.0, 0.0],
dithering: 0,
_padding: 0,
},
previous_uniform_buffer_content: UniformBuffer::zeroed(),
uniform_bind_group,
texture_bind_group_layout,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
dithering,
options,
callback_resources: CallbackResources::default(),
}
}
@@ -564,15 +619,6 @@ impl Renderer {
);
Cow::Borrowed(&image.pixels)
}
epaint::ImageData::Font(image) => {
assert_eq!(
width as usize * height as usize,
image.pixels.len(),
"Mismatch between texture size and texel count"
);
profiling::scope!("font -> sRGBA");
Cow::Owned(image.srgba_pixels(None).collect::<Vec<epaint::Color32>>())
}
};
let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice());
@@ -638,9 +684,9 @@ impl Renderer {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb, // Minspec for wgpu WebGL emulation is WebGL2, so this should always be supported.
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[wgpu::TextureFormat::Rgba8UnormSrgb],
view_formats: &[wgpu::TextureFormat::Rgba8Unorm],
})
};
let origin = wgpu::Origin3d::ZERO;
@@ -699,7 +745,7 @@ impl Renderer {
///
/// This enables the application to reference the texture inside an image ui element.
/// This effectively enables off-screen rendering inside the egui UI. Texture must have
/// the texture format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// the texture format [`wgpu::TextureFormat::Rgba8Unorm`].
pub fn register_native_texture(
&mut self,
device: &wgpu::Device,
@@ -747,9 +793,9 @@ impl Renderer {
/// This allows applications to specify individual minification/magnification filters as well as
/// custom mipmap and tiling options.
///
/// The texture must have the format [`wgpu::TextureFormat::Rgba8UnormSrgb`].
/// The texture must have the format [`wgpu::TextureFormat::Rgba8Unorm`].
/// Any compare function supplied in the [`wgpu::SamplerDescriptor`] will be ignored.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn register_native_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
@@ -796,7 +842,7 @@ impl Renderer {
/// [`wgpu::SamplerDescriptor`] options.
///
/// This allows applications to reuse [`epaint::TextureId`]s created with custom sampler options.
#[allow(clippy::needless_pass_by_value)] // false positive
#[expect(clippy::needless_pass_by_value)] // false positive
pub fn update_egui_texture_from_wgpu_texture_with_sampler_options(
&mut self,
device: &wgpu::Device,
@@ -855,8 +901,8 @@ impl Renderer {
let uniform_buffer_content = UniformBuffer {
screen_size_in_points,
dithering: u32::from(self.dithering),
_padding: Default::default(),
dithering: u32::from(self.options.dithering),
predictable_texture_filtering: u32::from(self.options.predictable_texture_filtering),
};
if uniform_buffer_content != self.previous_uniform_buffer_content {
profiling::scope!("update uniforms");
@@ -882,7 +928,7 @@ impl Renderer {
callbacks.push(c.0.as_ref());
} else {
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
};
}
acc
}
}
@@ -909,7 +955,11 @@ impl Renderer {
);
let Some(mut index_buffer_staging) = index_buffer_staging else {
panic!("Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)", self.index_buffer.buffer.size(), self.index_buffer.capacity);
panic!(
"Failed to create staging buffer for index data. Index count: {index_count}. Required index buffer size: {required_index_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.index_buffer.buffer.size(),
self.index_buffer.capacity
);
};
let mut index_offset = 0;
@@ -948,7 +998,11 @@ impl Renderer {
);
let Some(mut vertex_buffer_staging) = vertex_buffer_staging else {
panic!("Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)", self.vertex_buffer.buffer.size(), self.vertex_buffer.capacity);
panic!(
"Failed to create staging buffer for vertex data. Vertex count: {vertex_count}. Required vertex buffer size: {required_vertex_buffer_size}. Actual size {} and capacity: {} (bytes)",
self.vertex_buffer.buffer.size(),
self.vertex_buffer.capacity
);
};
let mut vertex_offset = 0;

View File

@@ -48,7 +48,7 @@ impl WgpuSetup {
pub async fn new_instance(&self) -> wgpu::Instance {
match self {
Self::CreateNew(create_new) => {
#[allow(unused_mut)]
#[allow(clippy::allow_attributes, unused_mut)]
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
@@ -64,7 +64,7 @@ impl WgpuSetup {
}
}
log::debug!("Creating wgpu instance with backends {:?}", backends);
log::debug!("Creating wgpu instance with backends {backends:?}");
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
.await
}
@@ -126,13 +126,6 @@ pub struct WgpuSetupCreateNew {
/// Configuration passed on device request, given an adapter
pub device_descriptor:
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
/// Option path to output a wgpu trace file.
///
/// This only works if this feature is enabled in `wgpu-core`.
/// Does not work when running with WebGPU.
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
pub trace_path: Option<std::path::PathBuf>,
}
impl Clone for WgpuSetupCreateNew {
@@ -141,8 +134,7 @@ impl Clone for WgpuSetupCreateNew {
instance_descriptor: self.instance_descriptor.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: self.device_descriptor.clone(),
trace_path: self.trace_path.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
}
}
}
@@ -156,7 +148,6 @@ impl std::fmt::Debug for WgpuSetupCreateNew {
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.field("trace_path", &self.trace_path)
.finish()
}
}
@@ -171,6 +162,7 @@ impl Default for WgpuSetupCreateNew {
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
},
power_preference: wgpu::PowerPreference::from_env()
@@ -187,20 +179,15 @@ impl Default for WgpuSetupCreateNew {
wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
required_features: wgpu::Features::default(),
required_limits: wgpu::Limits {
// When using a depth buffer, we have to be able to create a texture
// large enough for the entire surface, and we want to support 4k+ displays.
max_texture_dimension_2d: 8192,
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
..Default::default()
}
}),
trace_path: std::env::var("WGPU_TRACE")
.ok()
.map(std::path::PathBuf::from),
}
}
}

View File

@@ -1,8 +1,13 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::missing_errors_doc)]
#![expect(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
#![expect(unsafe_code)]
use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration};
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
};
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
@@ -11,6 +16,7 @@ struct SurfaceState {
alpha_mode: wgpu::CompositeAlphaMode,
width: u32,
height: u32,
resizing: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -21,10 +27,8 @@ struct SurfaceState {
pub struct Painter {
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
options: RendererOptions,
support_transparent_backbuffer: bool,
dithering: bool,
depth_format: Option<wgpu::TextureFormat>,
screen_capture_state: Option<CaptureState>,
instance: wgpu::Instance,
@@ -54,10 +58,8 @@ impl Painter {
pub async fn new(
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
depth_format: Option<wgpu::TextureFormat>,
support_transparent_backbuffer: bool,
dithering: bool,
options: RendererOptions,
) -> Self {
let (capture_tx, capture_rx) = capture_channel();
let instance = configuration.wgpu_setup.new_instance().await;
@@ -65,10 +67,8 @@ impl Painter {
Self {
context,
configuration,
msaa_samples,
options,
support_transparent_backbuffer,
dithering,
depth_format,
screen_capture_state: None,
instance,
@@ -204,9 +204,7 @@ impl Painter {
&self.configuration,
&self.instance,
Some(&surface),
self.depth_format,
self.msaa_samples,
self.dithering,
self.options,
)
.await?;
self.render_state.get_or_insert(render_state)
@@ -220,7 +218,9 @@ impl Painter {
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
wgpu::CompositeAlphaMode::PostMultiplied
} else {
log::warn!("Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency.");
log::warn!(
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
@@ -233,6 +233,7 @@ impl Painter {
width: size.width,
height: size.height,
alpha_mode,
resizing: false,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
@@ -277,7 +278,7 @@ impl Painter {
Self::configure_surface(surface_state, render_state, &self.configuration);
if let Some(depth_format) = self.depth_format {
if let Some(depth_format) = self.options.depth_stencil_format {
self.depth_texture_view.insert(
viewport_id,
render_state
@@ -290,7 +291,7 @@ impl Painter {
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.msaa_samples,
sample_count: self.options.msaa_samples.max(1),
dimension: wgpu::TextureDimension::D2,
format: depth_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
@@ -301,7 +302,7 @@ impl Painter {
);
}
if let Some(render_state) = (self.msaa_samples > 1)
if let Some(render_state) = (self.options.msaa_samples > 1)
.then_some(self.render_state.as_ref())
.flatten()
{
@@ -318,7 +319,7 @@ impl Painter {
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: self.msaa_samples,
sample_count: self.options.msaa_samples.max(1),
dimension: wgpu::TextureDimension::D2,
format: texture_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
@@ -326,7 +327,60 @@ impl Painter {
})
.create_view(&wgpu::TextureViewDescriptor::default()),
);
}
}
/// Handles changes of the resizing state.
///
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
profiling::function_scope!();
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
return;
};
if state.resizing == resizing {
if resizing {
log::debug!(
"Painter::on_window_resize_state_change() redundant call while resizing"
);
} else {
log::debug!(
"Painter::on_window_resize_state_change() redundant call after resizing"
);
}
return;
}
// Resizing is a bit tricky on macOS.
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
// flag to avoid jittering during the resize. Even though resize jittering on macOS
// is common across rendering backends, the solution for wgpu/metal is known.
//
// See https://github.com/emilk/egui/issues/903
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
{
// SAFETY: The cast is checked with if condition. If the used backend is not metal
// it gracefully fails. The pointer casts are valid as it's 1-to-1 type mapping.
// This is how wgpu currently exposes this backend-specific flag.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
let raw =
std::ptr::from_ref::<wgpu::hal::metal::Surface>(&*hal_surface).cast_mut();
(*raw).present_with_transaction = resizing;
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.configuration,
);
}
}
}
state.resizing = resizing;
}
pub fn on_window_resized(
@@ -344,7 +398,9 @@ impl Painter {
height_in_pixels,
);
} else {
log::warn!("Ignoring window resize notification with no surface created via Painter::set_window()");
log::warn!(
"Ignoring window resize notification with no surface created via Painter::set_window()"
);
}
}
@@ -446,7 +502,7 @@ impl Painter {
};
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
let (view, resolve_target) = (self.msaa_samples > 1)
let (view, resolve_target) = (self.options.msaa_samples > 1)
.then_some(self.msaa_texture_view.get(&viewport_id))
.flatten()
.map_or((&target_view, None), |texture_view| {
@@ -467,17 +523,33 @@ impl Painter {
}),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
// It is very unlikely that the depth buffer is needed after egui finished rendering
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
store: wgpu::StoreOp::Discard,
}),
stencil_ops: None,
depth_ops: self
.options
.depth_stencil_format
.is_some_and(|depth_stencil_format| {
depth_stencil_format.has_depth_aspect()
})
.then_some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
// It is very unlikely that the depth buffer is needed after egui finished rendering
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
store: wgpu::StoreOp::Discard,
}),
stencil_ops: self
.options
.depth_stencil_format
.is_some_and(|depth_stencil_format| {
depth_stencil_format.has_stencil_aspect()
})
.then_some(wgpu::Operations {
load: wgpu::LoadOp::Clear(0),
store: wgpu::StoreOp::Discard,
}),
}
}),
timestamp_writes: None,
@@ -493,14 +565,12 @@ impl Painter {
&screen_descriptor,
);
if capture {
if let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
if capture && let Some(capture_state) = &mut self.screen_capture_state {
capture_buffer = Some(capture_state.copy_textures(
&render_state.device,
&output_frame,
&mut encoder,
));
}
}
@@ -530,16 +600,16 @@ impl Painter {
}
}
if let Some(capture_buffer) = capture_buffer {
if let Some(screen_capture_state) = &mut self.screen_capture_state {
screen_capture_state.read_screen_rgba(
self.context.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
viewport_id,
);
}
if let Some(capture_buffer) = capture_buffer
&& let Some(screen_capture_state) = &mut self.screen_capture_state
{
screen_capture_state.read_screen_rgba(
self.context.clone(),
capture_buffer,
capture_data,
self.capture_tx.clone(),
viewport_id,
);
}
{
@@ -561,7 +631,7 @@ impl Painter {
events.push(Event::Screenshot {
viewport_id,
user_data: data,
image: screenshot.clone(),
image: Arc::clone(&screenshot),
});
}
}
@@ -575,7 +645,7 @@ impl Painter {
.retain(|id, _| active_viewports.contains(id));
}
#[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
pub fn destroy(&mut self) {
// TODO(emilk): something here?
}

View File

@@ -5,6 +5,49 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.33.3 - 2025-12-11
Nothing new
## 0.33.2 - 2025-11-13
* Don't enable `arboard` on iOS [#7663](https://github.com/emilk/egui/pull/7663) by [@irh](https://github.com/irh)
## 0.33.0 - 2025-10-09
### ⭐ Added
* Add rotation gesture support for trackpad sources [#7453](https://github.com/emilk/egui/pull/7453) by [@thatcomputerguy0101](https://github.com/thatcomputerguy0101)
* Add support for the safe area on iOS [#7578](https://github.com/emilk/egui/pull/7578) by [@irh](https://github.com/irh)
### 🔧 Changed
* Update MSRV from 1.86 to 1.88 [#7579](https://github.com/emilk/egui/pull/7579) by [@Wumpf](https://github.com/Wumpf)
* Create `egui_wgpu::RendererOptions` [#7601](https://github.com/emilk/egui/pull/7601) by [@emilk](https://github.com/emilk)
### 🐛 Fixed
* Fix build error in egui-winit with profiling enabled [#7557](https://github.com/emilk/egui/pull/7557) by [@torokati44](https://github.com/torokati44)
* Properly end winit event loop [#7565](https://github.com/emilk/egui/pull/7565) by [@tye-exe](https://github.com/tye-exe)
* Fix eframe window not being focused on mac on startup [#7593](https://github.com/emilk/egui/pull/7593) by [@emilk](https://github.com/emilk)
## 0.32.3 - 2025-09-12
Nothing new
## 0.32.2 - 2025-09-04
Nothing new
## 0.32.1 - 2025-08-15
* Update to winit 0.30.12 [#7420](https://github.com/emilk/egui/pull/7420) by [@emilk](https://github.com/emilk)
## 0.32.0 - 2025-07-10
* Mark all keys as released if the app loses focus [#5743](https://github.com/emilk/egui/pull/5743) by [@emilk](https://github.com/emilk)
* Fix text input on Android [#5759](https://github.com/emilk/egui/pull/5759) by [@StratusFearMe21](https://github.com/StratusFearMe21)
* Add macOS-specific `has_shadow` and `with_has_shadow` to ViewportBuilder [#6850](https://github.com/emilk/egui/pull/6850) by [@gaelanmcmillan](https://github.com/gaelanmcmillan)
* Support for back-button on Android [#7073](https://github.com/emilk/egui/pull/7073) by [@ardocrat](https://github.com/ardocrat)
* Fix incorrect window sizes for non-resizable windows on Wayland [#7103](https://github.com/emilk/egui/pull/7103) by [@GoldsteinE](https://github.com/GoldsteinE)
## 0.31.1 - 2025-03-05
Nothing new

View File

@@ -5,10 +5,10 @@ authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for using egui with winit"
edition.workspace = true
rust-version.workspace = true
homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
homepage = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
license.workspace = true
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
@@ -24,7 +24,7 @@ rustdoc-args = ["--generate-link-to-definition"]
default = ["clipboard", "links", "wayland", "winit/default", "x11"]
## Enable platform accessibility API implementations through [AccessKit](https://accesskit.dev/).
accesskit = ["dep:accesskit_winit", "egui/accesskit"]
accesskit = ["dep:accesskit_winit"]
# Allow crates to choose an android-activity backend via Winit
# - It's important that most applications should not have to depend on android-activity directly, and can
@@ -55,9 +55,8 @@ wayland = ["winit/wayland", "bytemuck"]
x11 = ["winit/x11", "bytemuck"]
[dependencies]
egui = { workspace = true, default-features = false, features = ["log"] }
egui = { workspace = true, default-features = false }
ahash.workspace = true
log.workspace = true
profiling.workspace = true
raw-window-handle.workspace = true
@@ -75,17 +74,30 @@ bytemuck = { workspace = true, optional = true }
document-features = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
webbrowser = { version = "1.0.0", optional = true }
webbrowser = { workspace = true, optional = true }
[target.'cfg(target_os = "ios")'.dependencies]
objc2.workspace = true
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
objc2-ui-kit = { workspace = true, features = [
"std",
"UIApplication",
"UIGeometry",
"UIResponder",
"UIScene",
"UISceneDefinitions",
"UIView",
"UIWindow",
"UIWindowScene",
] }
[target.'cfg(any(target_os="linux", target_os="dragonfly", target_os="freebsd", target_os="netbsd", target_os="openbsd"))'.dependencies]
smithay-clipboard = { version = "0.7.2", optional = true }
smithay-clipboard = { workspace = true, optional = true }
# The wayland-cursor normally selected doesn't properly enable all the features it uses
# and thus doesn't compile as it is used in egui-winit. This is fixed upstream, so force
# a slightly newer version. Remove this when winit upgrades past this version.
wayland-cursor = { version = "0.31.1", default-features = false, optional = true }
wayland-cursor = { workspace = true, optional = true }
[target.'cfg(not(target_os = "android"))'.dependencies]
arboard = { version = "3.3", optional = true, default-features = false, features = [
"image-data",
] }
arboard = { workspace = true, optional = true, features = ["image-data"] }

View File

@@ -5,7 +5,10 @@ use raw_window_handle::RawDisplayHandle;
/// If the "clipboard" feature is off, or we cannot connect to the OS clipboard,
/// then a fallback clipboard that just works within the same app is used instead.
pub struct Clipboard {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
arboard: Option<arboard::Clipboard>,
#[cfg(all(
@@ -28,7 +31,10 @@ impl Clipboard {
/// Construct a new instance
pub fn new(_raw_display_handle: Option<RawDisplayHandle>) -> Self {
Self {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
arboard: init_arboard(),
#[cfg(all(
@@ -68,7 +74,10 @@ impl Clipboard {
};
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
return match clipboard.get_text() {
Ok(text) => Some(text),
@@ -98,7 +107,10 @@ impl Clipboard {
return;
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_text(text) {
log::error!("arboard copy/cut error: {err}");
@@ -110,7 +122,10 @@ impl Clipboard {
}
pub fn set_image(&mut self, image: &egui::ColorImage) {
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
if let Some(clipboard) = &mut self.arboard {
if let Err(err) = clipboard.set_image(arboard::ImageData {
width: image.width(),
@@ -123,12 +138,17 @@ impl Clipboard {
return;
}
log::error!("Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it.");
log::error!(
"Copying images is not supported. Enable the 'clipboard' feature of `egui-winit` to enable it."
);
_ = image;
}
}
#[cfg(all(feature = "arboard", not(target_os = "android")))]
#[cfg(all(
not(any(target_os = "android", target_os = "ios")),
feature = "arboard",
))]
fn init_arboard() -> Option<arboard::Clipboard> {
profiling::function_scope!();
@@ -155,13 +175,13 @@ fn init_arboard() -> Option<arboard::Clipboard> {
fn init_smithay_clipboard(
raw_display_handle: Option<RawDisplayHandle>,
) -> Option<smithay_clipboard::Clipboard> {
#![allow(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::undocumented_unsafe_blocks)]
profiling::function_scope!();
if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle {
log::trace!("Initializing smithay clipboard…");
#[allow(unsafe_code)]
#[expect(unsafe_code)]
Some(unsafe { smithay_clipboard::Clipboard::new(display.display.as_ptr()) })
} else {
#[cfg(feature = "wayland")]

View File

@@ -7,7 +7,7 @@
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
#![allow(clippy::manual_range_contains)]
#![expect(clippy::manual_range_contains)]
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
@@ -18,11 +18,11 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
pub use winit;
pub mod clipboard;
mod safe_area;
mod window_settings;
pub use window_settings::WindowSettings;
use ahash::HashSet;
use raw_window_handle::HasDisplayHandle;
use winit::{
@@ -275,6 +275,21 @@ impl State {
}
use winit::event::WindowEvent;
#[cfg(target_os = "ios")]
match &event {
WindowEvent::Resized(_)
| WindowEvent::ScaleFactorChanged { .. }
| WindowEvent::Focused(true)
| WindowEvent::Occluded(false) => {
// Once winit v0.31 has been released this can be reworked to get the safe area from
// `Window::safe_area`, and updated from a new event which is being discussed in
// https://github.com/rust-windowing/winit/issues/3911.
self.egui_input_mut().safe_area_insets = Some(safe_area::get_safe_area_insets());
}
_ => {}
}
match event {
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let native_pixels_per_point = *scale_factor as f32;
@@ -294,21 +309,21 @@ impl State {
self.on_mouse_button_input(*state, *button);
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_pointer_input(),
consumed: self.egui_ctx.egui_wants_pointer_input(),
}
}
WindowEvent::MouseWheel { delta, .. } => {
self.on_mouse_wheel(window, *delta);
WindowEvent::MouseWheel { delta, phase, .. } => {
self.on_mouse_wheel(window, *delta, *phase);
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_pointer_input(),
consumed: self.egui_ctx.egui_wants_pointer_input(),
}
}
WindowEvent::CursorMoved { position, .. } => {
self.on_cursor_moved(window, *position);
EventResponse {
repaint: true,
consumed: self.egui_ctx.is_using_pointer(),
consumed: self.egui_ctx.egui_is_using_pointer(),
}
}
WindowEvent::CursorLeft { .. } => {
@@ -325,8 +340,10 @@ impl State {
let consumed = match touch.phase {
winit::event::TouchPhase::Started
| winit::event::TouchPhase::Ended
| winit::event::TouchPhase::Cancelled => self.egui_ctx.wants_pointer_input(),
winit::event::TouchPhase::Moved => self.egui_ctx.is_using_pointer(),
| winit::event::TouchPhase::Cancelled => {
self.egui_ctx.egui_wants_pointer_input()
}
winit::event::TouchPhase::Moved => self.egui_ctx.egui_is_using_pointer(),
};
EventResponse {
repaint: true,
@@ -335,49 +352,11 @@ impl State {
}
WindowEvent::Ime(ime) => {
// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
// So no need to check is_mac_cmd.
//
// How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS
// and Windows.
//
// - On Windows, before and after each Commit will produce an Enable/Disabled
// event.
// - On MacOS, only when user explicit enable/disable ime. No Disabled
// after Commit.
//
// We use input_method_editor_started to manually insert CompositionStart
// between Commits.
match ime {
winit::event::Ime::Enabled => {
if cfg!(target_os = "linux") {
// This event means different things in X11 and Wayland, but we can just
// ignore it and enable IME on the preedit event.
// See <https://github.com/rust-windowing/winit/issues/2498>
} else {
self.ime_event_enable();
}
}
winit::event::Ime::Preedit(text, Some(_cursor)) => {
self.ime_event_enable();
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
}
winit::event::Ime::Commit(text) => {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
self.ime_event_disable();
}
winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => {
self.ime_event_disable();
}
};
self.on_ime(ime);
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_keyboard_input(),
consumed: self.egui_ctx.egui_wants_keyboard_input(),
}
}
WindowEvent::KeyboardInput {
@@ -398,7 +377,7 @@ impl State {
self.on_keyboard_input(event);
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = self.egui_ctx.wants_keyboard_input()
let consumed = self.egui_ctx.egui_wants_keyboard_input()
|| event.logical_key
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
EventResponse {
@@ -408,10 +387,19 @@ impl State {
}
}
WindowEvent::Focused(focused) => {
self.egui_input.focused = *focused;
let focused = if cfg!(target_os = "macos") {
// TODO(emilk): remove this work-around once we update winit
// https://github.com/rust-windowing/winit/issues/4371
// https://github.com/emilk/egui/issues/7588
window.has_focus()
} else {
*focused
};
self.egui_input.focused = focused;
self.egui_input
.events
.push(egui::Event::WindowFocused(*focused));
.push(egui::Event::WindowFocused(focused));
EventResponse {
repaint: true,
consumed: false,
@@ -492,9 +480,7 @@ impl State {
// Things we completely ignore:
WindowEvent::ActivationTokenDone { .. }
| WindowEvent::AxisMotion { .. }
| WindowEvent::DoubleTapGesture { .. }
| WindowEvent::RotationGesture { .. }
| WindowEvent::PanGesture { .. } => EventResponse {
| WindowEvent::DoubleTapGesture { .. } => EventResponse {
repaint: false,
consumed: false,
},
@@ -506,9 +492,135 @@ impl State {
self.egui_input.events.push(egui::Event::Zoom(zoom_factor));
EventResponse {
repaint: true,
consumed: self.egui_ctx.wants_pointer_input(),
consumed: self.egui_ctx.egui_wants_pointer_input(),
}
}
WindowEvent::RotationGesture { delta, .. } => {
// Positive delta values indicate counterclockwise rotation
// Negative delta values indicate clockwise rotation
// This is opposite of egui's sign convention for angles
self.egui_input
.events
.push(egui::Event::Rotate(-delta.to_radians()));
EventResponse {
repaint: true,
consumed: self.egui_ctx.egui_wants_pointer_input(),
}
}
WindowEvent::PanGesture { delta, phase, .. } => {
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
self.egui_input.events.push(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
phase: to_egui_touch_phase(*phase),
modifiers: self.egui_input.modifiers,
});
EventResponse {
repaint: true,
consumed: self.egui_ctx.egui_wants_pointer_input(),
}
}
}
}
/// ## NOTE
///
/// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
/// So no need to check `is_mac_cmd`.
///
/// ### How events are emitted by [`winit`] across different setups in various situations
///
/// This is done by uncommenting the code block at the top of this method
/// and checking console outputs.
///
/// winit version: 0.30.12.
///
/// #### Setups
///
/// - `a-macos15-apple_shuangpin`: macOS 15.7.3 `aarch64`, IME: builtin Chinese Shuangpin - Simplified. (Demo app shows: renderer: `wgpu`, backend: `Metal`.)
/// - `b-debian13_gnome48_wayland-fcitx5_shuangpin`: Debian 13 `aarch64`, Gnome 48, Wayland, IME: Fcitx5 with fcitx5-chinese-addons's Shuangpin. (Demo app shows: renderer: `wgpu`, backend: `Gl`.)
/// - `c-windows11-ms_pinyin`: Windows11 23H2 `x86_64`, IME: builtin Microsoft Pinyin. (Demo app shows: renderer: `wgpu`, backend: `Vulkan` & `Dx12`, others: `Dx12` & `Gl`.)
///
/// #### Situation: pressed space to select the first candidate "测试"
///
/// | Setup | Events in Order |
/// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
/// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("测试")` |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("测试")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | `Predict("测试", Some(…))` -> `Predict("", None)` -> `Commit("测试")` -> `Disabled` |
///
/// #### Situation: pressed backspace to delete the last character in the prediction
///
/// | Setup | Events in Order |
/// | a-macos15-apple_shuangpin | `Predict("", None)` |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` |
///
/// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce"
///
/// | Setup | Events in Order |
/// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
/// | a-macos15-apple_shuangpin | nothing emitted |
/// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) |
/// | c-windows11-ms_pinyin | nothing emitted |
fn on_ime(&mut self, ime: &winit::event::Ime) {
// // code for inspecting ime events emitted by winit:
// {
// static LAST_IME: std::sync::Mutex<Option<winit::event::Ime>> =
// std::sync::Mutex::new(None);
// static IS_LAST_DUPLICATE: std::sync::atomic::AtomicBool =
// std::sync::atomic::AtomicBool::new(false);
// let mut last_ime_guard = LAST_IME.lock().unwrap();
// if { last_ime_guard.as_ref().cloned() }.as_ref() != Some(ime) {
// println!("IME={ime:?}");
// *last_ime_guard = Some(ime.clone());
// IS_LAST_DUPLICATE.store(false, std::sync::atomic::Ordering::Relaxed);
// } else if !IS_LAST_DUPLICATE.load(std::sync::atomic::Ordering::Relaxed) {
// println!("IME=(duplicate)");
// IS_LAST_DUPLICATE.store(true, std::sync::atomic::Ordering::Relaxed);
// }
// }
match ime {
winit::event::Ime::Enabled => {
if cfg!(target_os = "linux") {
// This event means different things in X11 and Wayland, but we can just
// ignore it and enable IME on the preedit event.
// See <https://github.com/rust-windowing/winit/issues/2498>
} else {
self.ime_event_enable();
}
}
winit::event::Ime::Preedit(text, Some(_cursor)) => {
self.ime_event_enable();
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
}
winit::event::Ime::Commit(text) => {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone())));
self.ime_event_disable();
}
winit::event::Ime::Disabled => {
self.ime_event_disable();
}
winit::event::Ime::Preedit(_, None) => {
// we need to emit this on macOS, since winit doesn't emit
// `Predict("", Some(0, 0))` before this event on macOS when the
// user deletes the last character in the prediction with the
// backspace key. Without this, only `egui::ImeEvent::Disabled`
// is emitted here, leading to the last character being left in
// TextEdit in such situation.
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
self.ime_event_disable();
}
}
}
@@ -550,41 +662,41 @@ impl State {
state: winit::event::ElementState,
button: winit::event::MouseButton,
) {
if let Some(pos) = self.pointer_pos_in_points {
if let Some(button) = translate_mouse_button(button) {
let pressed = state == winit::event::ElementState::Pressed;
if let Some(pos) = self.pointer_pos_in_points
&& let Some(button) = translate_mouse_button(button)
{
let pressed = state == winit::event::ElementState::Pressed;
self.egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed,
modifiers: self.egui_input.modifiers,
});
self.egui_input.events.push(egui::Event::PointerButton {
pos,
button,
pressed,
modifiers: self.egui_input.modifiers,
});
if self.simulate_touch_screen {
if pressed {
self.any_pointer_button_down = true;
if self.simulate_touch_screen {
if pressed {
self.any_pointer_button_down = true;
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::Start,
pos,
force: None,
});
} else {
self.any_pointer_button_down = false;
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::Start,
pos,
force: None,
});
} else {
self.any_pointer_button_down = false;
self.egui_input.events.push(egui::Event::PointerGone);
self.egui_input.events.push(egui::Event::PointerGone);
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::End,
pos,
force: None,
});
};
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId(0),
phase: egui::TouchPhase::End,
pos,
force: None,
});
}
}
}
@@ -631,12 +743,7 @@ impl State {
self.egui_input.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
id: egui::TouchId::from(touch.id),
phase: match touch.phase {
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
},
phase: to_egui_touch_phase(touch.phase),
pos: egui::pos2(
touch.location.x as f32 / pixels_per_point,
touch.location.y as f32 / pixels_per_point,
@@ -689,7 +796,12 @@ impl State {
}
}
fn on_mouse_wheel(&mut self, window: &Window, delta: winit::event::MouseScrollDelta) {
fn on_mouse_wheel(
&mut self,
window: &Window,
delta: winit::event::MouseScrollDelta,
phase: winit::event::TouchPhase,
) {
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
{
@@ -705,10 +817,12 @@ impl State {
egui::vec2(x as f32, y as f32) / pixels_per_point,
),
};
let phase = to_egui_touch_phase(phase);
let modifiers = self.egui_input.modifiers;
self.egui_input.events.push(egui::Event::MouseWheel {
unit,
delta,
phase,
modifiers,
});
}
@@ -828,18 +942,14 @@ impl State {
window: &Window,
platform_output: egui::PlatformOutput,
) {
#![allow(deprecated)]
profiling::function_scope!();
let egui::PlatformOutput {
commands,
cursor_icon,
open_url,
copied_text,
events: _, // handled elsewhere
mutable_text_under_cursor: _, // only used in eframe web
ime,
#[cfg(feature = "accesskit")]
accesskit_update,
num_completed_passes: _, // `egui::Context::run` handles this
request_discard_reasons: _, // `egui::Context::run` handles this
@@ -856,22 +966,11 @@ impl State {
egui::OutputCommand::OpenUrl(open_url) => {
open_url_in_browser(&open_url.url);
}
egui::OutputCommand::SetPointerPosition(egui::Pos2 { x, y }) => {
let _ = window.set_cursor_position(winit::dpi::LogicalPosition { x, y });
}
}
}
self.set_cursor_icon(window, cursor_icon);
if let Some(open_url) = open_url {
open_url_in_browser(&open_url.url);
}
if !copied_text.is_empty() {
self.clipboard.set_text(copied_text);
}
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
self.allow_ime = allow_ime;
@@ -903,12 +1002,15 @@ impl State {
}
#[cfg(feature = "accesskit")]
if let Some(accesskit) = self.accesskit.as_mut() {
if let Some(update) = accesskit_update {
profiling::scope!("accesskit");
accesskit.update_if_active(|| update);
}
if let Some(accesskit) = self.accesskit.as_mut()
&& let Some(update) = accesskit_update
{
profiling::scope!("accesskit");
accesskit.update_if_active(|| update);
}
#[cfg(not(feature = "accesskit"))]
let _ = accesskit_update;
}
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
@@ -935,6 +1037,15 @@ impl State {
}
}
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
match phase {
winit::event::TouchPhase::Started => egui::TouchPhase::Start,
winit::event::TouchPhase::Moved => egui::TouchPhase::Move,
winit::event::TouchPhase::Ended => egui::TouchPhase::End,
winit::event::TouchPhase::Cancelled => egui::TouchPhase::Cancel,
}
}
fn to_egui_theme(theme: winit::window::Theme) -> Theme {
match theme {
winit::window::Theme::Dark => Theme::Dark,
@@ -1029,7 +1140,7 @@ pub fn update_viewport_info(
fn open_url_in_browser(_url: &str) {
#[cfg(feature = "webbrowser")]
if let Err(err) = webbrowser::open(_url) {
log::warn!("Failed to open url: {}", err);
log::warn!("Failed to open url: {err}");
}
#[cfg(not(feature = "webbrowser"))]
@@ -1147,6 +1258,8 @@ fn key_from_named_key(named_key: winit::keyboard::NamedKey) -> Option<egui::Key>
NamedKey::F33 => Key::F33,
NamedKey::F34 => Key::F34,
NamedKey::F35 => Key::F35,
NamedKey::BrowserBack => Key::BrowserBack,
_ => {
log::trace!("Unknown key: {named_key:?}");
return None;
@@ -1335,7 +1448,7 @@ pub fn process_viewport_commands(
info: &mut ViewportInfo,
commands: impl IntoIterator<Item = ViewportCommand>,
window: &Window,
actions_requested: &mut HashSet<ActionRequested>,
actions_requested: &mut Vec<ActionRequested>,
) {
for command in commands {
process_viewport_command(egui_ctx, window, command, info, actions_requested);
@@ -1347,9 +1460,9 @@ fn process_viewport_command(
window: &Window,
command: ViewportCommand,
info: &mut ViewportInfo,
actions_requested: &mut HashSet<ActionRequested>,
actions_requested: &mut Vec<ActionRequested>,
) {
profiling::function_scope!();
profiling::function_scope!(&format!("{command:?}"));
use winit::window::ResizeDirection;
@@ -1366,10 +1479,10 @@ fn process_viewport_command(
}
ViewportCommand::StartDrag => {
// If `.has_focus()` is not checked on x11 the input will be permanently taken until the app is killed!
if window.has_focus() {
if let Err(err) = window.drag_window() {
log::warn!("{command:?}: {err}");
}
if window.has_focus()
&& let Err(err) = window.drag_window()
{
log::warn!("{command:?}: {err}");
}
}
ViewportCommand::InnerSize(size) => {
@@ -1540,16 +1653,16 @@ fn process_viewport_command(
}
}
ViewportCommand::Screenshot(user_data) => {
actions_requested.insert(ActionRequested::Screenshot(user_data));
actions_requested.push(ActionRequested::Screenshot(user_data));
}
ViewportCommand::RequestCut => {
actions_requested.insert(ActionRequested::Cut);
actions_requested.push(ActionRequested::Cut);
}
ViewportCommand::RequestCopy => {
actions_requested.insert(ActionRequested::Copy);
actions_requested.push(ActionRequested::Copy);
}
ViewportCommand::RequestPaste => {
actions_requested.insert(ActionRequested::Paste);
actions_requested.push(ActionRequested::Paste);
}
}
}
@@ -1567,8 +1680,7 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let window_attributes =
create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone());
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
@@ -1576,28 +1688,10 @@ pub fn create_window(
pub fn create_winit_window_attributes(
egui_ctx: &egui::Context,
event_loop: &ActiveEventLoop,
viewport_builder: ViewportBuilder,
) -> winit::window::WindowAttributes {
profiling::function_scope!();
// We set sizes and positions in egui:s own ui points, which depends on the egui
// zoom_factor and the native pixels per point, so we need to know that here.
// We don't know what monitor the window will appear on though, but
// we'll try to fix that after the window is created in the call to `apply_viewport_builder_to_window`.
let native_pixels_per_point = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
.map_or_else(
|| {
log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0");
1.0
},
|m| m.scale_factor() as f32,
);
let zoom_factor = egui_ctx.zoom_factor();
let pixels_per_point = zoom_factor * native_pixels_per_point;
let ViewportBuilder {
title,
position,
@@ -1623,6 +1717,7 @@ pub fn create_winit_window_attributes(
title_shown: _title_shown,
titlebar_buttons_shown: _titlebar_buttons_shown,
titlebar_shown: _titlebar_shown,
has_shadow: _has_shadow,
// Windows:
drag_and_drop: _drag_and_drop,
@@ -1633,6 +1728,7 @@ pub fn create_winit_window_attributes(
// x11
window_type: _window_type,
override_redirect: _override_redirect,
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs`
@@ -1672,40 +1768,46 @@ pub fn create_winit_window_attributes(
})
.with_active(active.unwrap_or(true));
// Here and below: we create `LogicalSize` / `LogicalPosition` taking
// zoom factor into account. We don't have a good way to get physical size here,
// and trying to do it anyway leads to weird bugs on Wayland, see:
// https://github.com/emilk/egui/issues/7095#issuecomment-2920545377
// https://github.com/rust-windowing/winit/issues/4266
#[expect(
clippy::disallowed_types,
reason = "zoom factor is manually accounted for"
)]
#[cfg(not(target_os = "ios"))]
if let Some(size) = inner_size {
window_attributes = window_attributes.with_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
{
use winit::dpi::{LogicalPosition, LogicalSize};
let zoom_factor = egui_ctx.zoom_factor();
#[cfg(not(target_os = "ios"))]
if let Some(size) = min_inner_size {
window_attributes = window_attributes.with_min_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = inner_size {
window_attributes = window_attributes
.with_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(size) = max_inner_size {
window_attributes = window_attributes.with_max_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
));
}
if let Some(size) = min_inner_size {
window_attributes = window_attributes
.with_min_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
#[cfg(not(target_os = "ios"))]
if let Some(pos) = position {
window_attributes = window_attributes.with_position(PhysicalPosition::new(
pixels_per_point * pos.x,
pixels_per_point * pos.y,
));
if let Some(size) = max_inner_size {
window_attributes = window_attributes
.with_max_inner_size(LogicalSize::new(zoom_factor * size.x, zoom_factor * size.y));
}
if let Some(pos) = position {
window_attributes = window_attributes.with_position(LogicalPosition::new(
zoom_factor * pos.x,
zoom_factor * pos.y,
));
}
}
#[cfg(target_os = "ios")]
{
// Unused:
_ = egui_ctx;
_ = pixels_per_point;
_ = position;
_ = inner_size;
@@ -1726,8 +1828,8 @@ pub fn create_winit_window_attributes(
#[cfg(all(feature = "x11", target_os = "linux"))]
{
use winit::platform::x11::WindowAttributesExtX11 as _;
if let Some(window_type) = _window_type {
use winit::platform::x11::WindowAttributesExtX11 as _;
use winit::platform::x11::WindowType;
window_attributes = window_attributes.with_x11_window_type(vec![match window_type {
egui::X11WindowType::Normal => WindowType::Normal,
@@ -1746,6 +1848,9 @@ pub fn create_winit_window_attributes(
egui::X11WindowType::Dnd => WindowType::Dnd,
}]);
}
if let Some(override_redirect) = _override_redirect {
window_attributes = window_attributes.with_override_redirect(override_redirect);
}
}
#[cfg(target_os = "windows")]
@@ -1767,7 +1872,8 @@ pub fn create_winit_window_attributes(
.with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true))
.with_titlebar_transparent(!_titlebar_shown.unwrap_or(true))
.with_fullsize_content_view(_fullsize_content_view.unwrap_or(false))
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false));
.with_movable_by_window_background(_movable_by_window_background.unwrap_or(false))
.with_has_shadow(_has_shadow.unwrap_or(true));
}
window_attributes
@@ -1794,10 +1900,10 @@ pub fn apply_viewport_builder_to_window(
window: &Window,
builder: &ViewportBuilder,
) {
if let Some(mouse_passthrough) = builder.mouse_passthrough {
if let Err(err) = window.set_cursor_hittest(!mouse_passthrough) {
log::warn!("set_cursor_hittest failed: {err}");
}
if let Some(mouse_passthrough) = builder.mouse_passthrough
&& let Err(err) = window.set_cursor_hittest(!mouse_passthrough)
{
log::warn!("set_cursor_hittest failed: {err}");
}
{
@@ -1808,16 +1914,15 @@ pub fn apply_viewport_builder_to_window(
let pixels_per_point = pixels_per_point(egui_ctx, window);
if let Some(size) = builder.inner_size {
if window
if let Some(size) = builder.inner_size
&& window
.request_inner_size(PhysicalSize::new(
pixels_per_point * size.x,
pixels_per_point * size.y,
))
.is_some()
{
log::debug!("Failed to set window size");
}
{
log::debug!("Failed to set window size");
}
if let Some(size) = builder.min_inner_size {
window.set_min_inner_size(Some(PhysicalSize::new(
@@ -1849,8 +1954,8 @@ pub fn short_device_event_description(event: &winit::event::DeviceEvent) -> &'st
use winit::event::DeviceEvent;
match event {
DeviceEvent::Added { .. } => "DeviceEvent::Added",
DeviceEvent::Removed { .. } => "DeviceEvent::Removed",
DeviceEvent::Added => "DeviceEvent::Added",
DeviceEvent::Removed => "DeviceEvent::Removed",
DeviceEvent::MouseMotion { .. } => "DeviceEvent::MouseMotion",
DeviceEvent::MouseWheel { .. } => "DeviceEvent::MouseWheel",
DeviceEvent::Motion { .. } => "DeviceEvent::Motion",
@@ -1868,11 +1973,11 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::ActivationTokenDone { .. } => "WindowEvent::ActivationTokenDone",
WindowEvent::Resized { .. } => "WindowEvent::Resized",
WindowEvent::Moved { .. } => "WindowEvent::Moved",
WindowEvent::CloseRequested { .. } => "WindowEvent::CloseRequested",
WindowEvent::Destroyed { .. } => "WindowEvent::Destroyed",
WindowEvent::CloseRequested => "WindowEvent::CloseRequested",
WindowEvent::Destroyed => "WindowEvent::Destroyed",
WindowEvent::DroppedFile { .. } => "WindowEvent::DroppedFile",
WindowEvent::HoveredFile { .. } => "WindowEvent::HoveredFile",
WindowEvent::HoveredFileCancelled { .. } => "WindowEvent::HoveredFileCancelled",
WindowEvent::HoveredFileCancelled => "WindowEvent::HoveredFileCancelled",
WindowEvent::Focused { .. } => "WindowEvent::Focused",
WindowEvent::KeyboardInput { .. } => "WindowEvent::KeyboardInput",
WindowEvent::ModifiersChanged { .. } => "WindowEvent::ModifiersChanged",
@@ -1883,7 +1988,7 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st
WindowEvent::MouseWheel { .. } => "WindowEvent::MouseWheel",
WindowEvent::MouseInput { .. } => "WindowEvent::MouseInput",
WindowEvent::PinchGesture { .. } => "WindowEvent::PinchGesture",
WindowEvent::RedrawRequested { .. } => "WindowEvent::RedrawRequested",
WindowEvent::RedrawRequested => "WindowEvent::RedrawRequested",
WindowEvent::DoubleTapGesture { .. } => "WindowEvent::DoubleTapGesture",
WindowEvent::RotationGesture { .. } => "WindowEvent::RotationGesture",
WindowEvent::TouchpadPressure { .. } => "WindowEvent::TouchpadPressure",

View File

@@ -0,0 +1,56 @@
#[cfg(target_os = "ios")]
pub use ios::get_safe_area_insets;
#[cfg(target_os = "ios")]
mod ios {
use egui::{SafeAreaInsets, epaint::MarginF32};
use objc2::{ClassType, rc::Retained};
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
use objc2_ui_kit::{UIApplication, UISceneActivationState, UIWindowScene};
/// Gets the ios safe area insets.
///
/// A safe area defines the area within a view that isnt covered by a navigation bar, tab bar,
/// toolbar, or other views a window might provide. Safe areas are essential for avoiding a
/// devices interactive and display features, like Dynamic Island on iPhone or the camera
/// housing on some Mac models.
///
/// Once winit v0.31 has been released this can be removed in favor of
/// `winit::Window::safe_area`.
pub fn get_safe_area_insets() -> SafeAreaInsets {
let Some(main_thread_marker) = MainThreadMarker::new() else {
log::error!("Getting safe area insets needs to be performed on the main thread");
return SafeAreaInsets::default();
};
let app = UIApplication::sharedApplication(main_thread_marker);
#[expect(unsafe_code)]
unsafe {
// Look for the first window scene that's in the foreground
for scene in app.connectedScenes() {
if scene.isKindOfClass(UIWindowScene::class())
&& matches!(
scene.activationState(),
UISceneActivationState::ForegroundActive
| UISceneActivationState::ForegroundInactive
)
{
// Safe to cast, the class kind was checked above
let window_scene = Retained::cast::<UIWindowScene>(scene.clone());
if let Some(window) = window_scene.keyWindow() {
let insets = window.safeAreaInsets();
return SafeAreaInsets(MarginF32 {
top: insets.top as f32,
left: insets.left as f32,
right: insets.right as f32,
bottom: insets.bottom as f32,
});
}
}
}
}
SafeAreaInsets::default()
}
}

View File

@@ -26,10 +26,6 @@ rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["default_fonts"]
## Exposes detailed accessibility implementation required by platform
## accessibility APIs. Also requires support in the egui integration.
accesskit = ["dep:accesskit"]
## [`bytemuck`](https://docs.rs/bytemuck) enables you to cast [`epaint::Vertex`], [`emath::Vec2`] etc to `&[u8]`.
bytemuck = ["epaint/bytemuck"]
@@ -44,18 +40,10 @@ cint = ["epaint/cint"]
## Enable the [`hex_color`] macro.
color-hex = ["epaint/color-hex"]
## This will automatically detect deadlocks due to double-locking on the same thread.
## If your app freezes, you may want to enable this!
## Only affects [`epaint::mutex::RwLock`] (which egui uses a lot).
deadlock_detection = ["epaint/deadlock_detection"]
## If set, egui will use `include_bytes!` to bundle some fonts.
## If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["epaint/default_fonts"]
## Turn on the `log` feature, that makes egui log some errors using the [`log`](https://docs.rs/log) crate.
log = ["dep:log", "epaint/log"]
## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra).
mint = ["epaint/mint"]
@@ -69,7 +57,7 @@ persistence = ["serde", "epaint/serde", "ron"]
rayon = ["epaint/rayon"]
## Allow serialization using [`serde`](https://docs.rs/serde).
serde = ["dep:serde", "epaint/serde", "accesskit?/serde"]
serde = ["dep:serde", "epaint/serde", "accesskit/serde"]
## Change Vertex layout to be compatible with unity
unity = ["epaint/unity"]
@@ -83,19 +71,21 @@ _override_unity = ["epaint/_override_unity"]
emath = { workspace = true, default-features = false }
epaint = { workspace = true, default-features = false }
accesskit.workspace = true
ahash.workspace = true
bitflags.workspace = true
log.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true
smallvec.workspace = true
unicode-segmentation.workspace = true
#! ### Optional dependencies
accesskit = { workspace = true, optional = true }
backtrace = { workspace = true, optional = true }
## Enable this when generating docs.
document-features = { workspace = true, optional = true }
log = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive", "rc"] }

View File

@@ -1,7 +1,7 @@
There are no stand-alone egui examples, because egui is not stand-alone!
See the top-level [examples](https://github.com/emilk/egui/tree/master/examples/) folder instead.
See the top-level [examples](https://github.com/emilk/egui/tree/main/examples/) folder instead.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/master/crates/egui_demo_lib>.
There are also plenty of examples in [the online demo](https://www.egui.rs/#demo). You can find the source code for it at <https://github.com/emilk/egui/tree/main/crates/egui_demo_lib>.
To learn how to set up `eframe` for web and native, go to <https://github.com/emilk/eframe_template/> and follow the instructions there!

View File

@@ -1,6 +1,6 @@
use crate::{
emath::{remap_clamp, NumExt as _},
Id, IdMap, InputState,
emath::{NumExt as _, remap_clamp},
};
#[derive(Clone, Default)]

View File

@@ -0,0 +1,112 @@
use crate::{AtomKind, FontSelection, Id, SizedAtom, Ui};
use emath::{NumExt as _, Vec2};
use epaint::text::TextWrapMode;
/// A low-level ui building block.
///
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
/// ```
/// # use egui::{Image, emath::Vec2};
/// use egui::AtomExt as _;
/// let string_atom = "Hello".atom_grow(true);
/// let image_atom = Image::new("some_image_url").atom_size(Vec2::splat(20.0));
/// ```
#[derive(Clone, Debug)]
pub struct Atom<'a> {
/// See [`crate::AtomExt::atom_size`]
pub size: Option<Vec2>,
/// See [`crate::AtomExt::atom_max_size`]
pub max_size: Vec2,
/// See [`crate::AtomExt::atom_grow`]
pub grow: bool,
/// See [`crate::AtomExt::atom_shrink`]
pub shrink: bool,
/// The atom type
pub kind: AtomKind<'a>,
}
impl Default for Atom<'_> {
fn default() -> Self {
Atom {
size: None,
max_size: Vec2::INFINITY,
grow: false,
shrink: false,
kind: AtomKind::Empty,
}
}
}
impl<'a> Atom<'a> {
/// Create an empty [`Atom`] marked as `grow`.
///
/// This will expand in size, allowing all preceding atoms to be left-aligned,
/// and all following atoms to be right-aligned
pub fn grow() -> Self {
Atom {
grow: true,
..Default::default()
}
}
/// Create a [`AtomKind::Custom`] with a specific size.
pub fn custom(id: Id, size: impl Into<Vec2>) -> Self {
Atom {
size: Some(size.into()),
kind: AtomKind::Custom(id),
..Default::default()
}
}
/// Turn this into a [`SizedAtom`].
pub fn into_sized(
self,
ui: &Ui,
mut available_size: Vec2,
mut wrap_mode: Option<TextWrapMode>,
fallback_font: FontSelection,
) -> SizedAtom<'a> {
if !self.shrink && self.max_size.x.is_infinite() {
wrap_mode = Some(TextWrapMode::Extend);
}
available_size = available_size.at_most(self.max_size);
if let Some(size) = self.size {
available_size = available_size.at_most(size);
}
if self.max_size.x.is_finite() {
wrap_mode = Some(TextWrapMode::Truncate);
}
let (intrinsic, kind) = self
.kind
.into_sized(ui, available_size, wrap_mode, fallback_font);
let size = self
.size
.map_or_else(|| kind.size(), |s| s.at_most(self.max_size));
SizedAtom {
size,
intrinsic_size: intrinsic.at_least(self.size.unwrap_or_default()),
grow: self.grow,
kind,
}
}
}
impl<'a, T> From<T> for Atom<'a>
where
T: Into<AtomKind<'a>>,
{
fn from(value: T) -> Self {
Atom {
kind: value.into(),
..Default::default()
}
}
}

View File

@@ -0,0 +1,107 @@
use crate::{Atom, FontSelection, Ui};
use emath::Vec2;
/// A trait for conveniently building [`Atom`]s.
///
/// The functions are prefixed with `atom_` to avoid conflicts with e.g. [`crate::RichText::size`].
pub trait AtomExt<'a> {
/// Set the atom to a fixed size.
///
/// If [`Atom::grow`] is `true`, this will be the minimum width.
/// If [`Atom::shrink`] is `true`, this will be the maximum width.
/// If both are true, the width will have no effect.
///
/// [`Self::atom_max_size`] will limit size.
///
/// See [`crate::AtomKind`] docs to see how the size affects the different types.
fn atom_size(self, size: Vec2) -> Atom<'a>;
/// Grow this atom to the available space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
///
/// You can also combine this with [`Self::atom_shrink`] to make it always take exactly the
/// remaining space.
fn atom_grow(self, grow: bool) -> Atom<'a>;
/// Shrink this atom if there isn't enough space.
///
/// This will affect the size of the [`Atom`] in the main direction. Since
/// [`crate::AtomLayout`] today only supports horizontal layout, it will affect the width.
///
/// NOTE: Only a single [`Atom`] may shrink for each widget.
///
/// If no atom was set to shrink and `wrap_mode != TextWrapMode::Extend`, the first
/// `AtomKind::Text` is set to shrink.
fn atom_shrink(self, shrink: bool) -> Atom<'a>;
/// Set the maximum size of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_size(self, max_size: Vec2) -> Atom<'a>;
/// Set the maximum width of this atom.
///
/// Will not affect the space taken by `grow` (All atoms marked as grow will always grow
/// equally to fill the available space).
fn atom_max_width(self, max_width: f32) -> Atom<'a>;
/// Set the maximum height of this atom.
fn atom_max_height(self, max_height: f32) -> Atom<'a>;
/// Set the max height of this atom to match the font size.
///
/// This is useful for e.g. limiting the height of icons in buttons.
fn atom_max_height_font_size(self, ui: &Ui) -> Atom<'a>
where
Self: Sized,
{
let font_selection = FontSelection::default();
let font_id = font_selection.resolve(ui.style());
let height = ui.fonts_mut(|f| f.row_height(&font_id));
self.atom_max_height(height)
}
}
impl<'a, T> AtomExt<'a> for T
where
T: Into<Atom<'a>> + Sized,
{
fn atom_size(self, size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.size = Some(size);
atom
}
fn atom_grow(self, grow: bool) -> Atom<'a> {
let mut atom = self.into();
atom.grow = grow;
atom
}
fn atom_shrink(self, shrink: bool) -> Atom<'a> {
let mut atom = self.into();
atom.shrink = shrink;
atom
}
fn atom_max_size(self, max_size: Vec2) -> Atom<'a> {
let mut atom = self.into();
atom.max_size = max_size;
atom
}
fn atom_max_width(self, max_width: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.x = max_width;
atom
}
fn atom_max_height(self, max_height: f32) -> Atom<'a> {
let mut atom = self.into();
atom.max_size.y = max_height;
atom
}
}

View File

@@ -0,0 +1,119 @@
use crate::{FontSelection, Id, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use emath::Vec2;
use epaint::text::TextWrapMode;
/// The different kinds of [`crate::Atom`]s.
#[derive(Clone, Default, Debug)]
pub enum AtomKind<'a> {
/// Empty, that can be used with [`crate::AtomExt::atom_grow`] to reserve space.
#[default]
Empty,
/// Text atom.
///
/// Truncation within [`crate::AtomLayout`] works like this:
/// -
/// - if `wrap_mode` is not Extend
/// - if no atom is `shrink`
/// - the first text atom is selected and will be marked as `shrink`
/// - the atom marked as `shrink` will shrink / wrap based on the selected wrap mode
/// - any other text atoms will have `wrap_mode` extend
/// - if `wrap_mode` is extend, Text will extend as expected.
///
/// Unless [`crate::AtomExt::atom_max_width`] is set, `wrap_mode` should only be set via [`crate::Style`] or
/// [`crate::AtomLayout::wrap_mode`], as setting a wrap mode on a [`WidgetText`] atom
/// that is not `shrink` will have unexpected results.
///
/// The size is determined by converting the [`WidgetText`] into a galley and using the galleys
/// size. You can use [`crate::AtomExt::atom_size`] to override this, and [`crate::AtomExt::atom_max_width`]
/// to limit the width (Causing the text to wrap or truncate, depending on the `wrap_mode`.
/// [`crate::AtomExt::atom_max_height`] has no effect on text.
Text(WidgetText),
/// Image atom.
///
/// By default the size is determined via [`Image::calc_size`].
/// You can use [`crate::AtomExt::atom_max_size`] or [`crate::AtomExt::atom_size`] to customize the size.
/// There is also a helper [`crate::AtomExt::atom_max_height_font_size`] to set the max height to the
/// default font height, which is convenient for icons.
Image(Image<'a>),
/// For custom rendering.
///
/// You can get the [`crate::Rect`] with the [`Id`] from [`crate::AtomLayoutResponse`] and use a
/// [`crate::Painter`] or [`Ui::place`] to add/draw some custom content.
///
/// Example:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Button, Id, __run_test_ui};
/// # use emath::Vec2;
/// # __run_test_ui(|ui| {
/// let id = Id::new("my_button");
/// let response = Button::new(("Hi!", Atom::custom(id, Vec2::splat(18.0)))).atom_ui(ui);
///
/// let rect = response.rect(id);
/// if let Some(rect) = rect {
/// ui.place(rect, Button::new("⏵"));
/// }
/// # });
/// ```
Custom(Id),
}
impl<'a> AtomKind<'a> {
pub fn text(text: impl Into<WidgetText>) -> Self {
AtomKind::Text(text.into())
}
pub fn image(image: impl Into<Image<'a>>) -> Self {
AtomKind::Image(image.into())
}
/// Turn this [`AtomKind`] into a [`SizedAtomKind`].
///
/// This converts [`WidgetText`] into [`crate::Galley`] and tries to load and size [`Image`].
/// The first returned argument is the preferred size.
pub fn into_sized(
self,
ui: &Ui,
available_size: Vec2,
wrap_mode: Option<TextWrapMode>,
fallback_font: FontSelection,
) -> (Vec2, SizedAtomKind<'a>) {
match self {
AtomKind::Text(text) => {
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
let galley = text.into_galley(ui, Some(wrap_mode), available_size.x, fallback_font);
(galley.intrinsic_size(), SizedAtomKind::Text(galley))
}
AtomKind::Image(image) => {
let size = image.load_and_calc_size(ui, available_size);
let size = size.unwrap_or(Vec2::ZERO);
(size, SizedAtomKind::Image(image, size))
}
AtomKind::Custom(id) => (Vec2::ZERO, SizedAtomKind::Custom(id)),
AtomKind::Empty => (Vec2::ZERO, SizedAtomKind::Empty),
}
}
}
impl<'a> From<ImageSource<'a>> for AtomKind<'a> {
fn from(value: ImageSource<'a>) -> Self {
AtomKind::Image(value.into())
}
}
impl<'a> From<Image<'a>> for AtomKind<'a> {
fn from(value: Image<'a>) -> Self {
AtomKind::Image(value)
}
}
impl<T> From<T> for AtomKind<'_>
where
T: Into<WidgetText>,
{
fn from(value: T) -> Self {
AtomKind::Text(value.into())
}
}

View File

@@ -0,0 +1,515 @@
use crate::atomics::ATOMS_SMALL_VEC_SIZE;
use crate::{
AtomKind, Atoms, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom,
SizedAtomKind, Ui, Widget,
};
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;
/// Intra-widget layout utility.
///
/// Used to lay out and paint [`crate::Atom`]s.
/// This is used internally by widgets like [`crate::Button`] and [`crate::Checkbox`].
/// You can use it to make your own widgets.
///
/// Painting the atoms can be split in two phases:
/// - [`AtomLayout::allocate`]
/// - calculates sizes
/// - converts texts to [`Galley`]s
/// - allocates a [`Response`]
/// - returns a [`AllocatedAtomLayout`]
/// - [`AllocatedAtomLayout::paint`]
/// - paints the [`Frame`]
/// - calculates individual [`crate::Atom`] positions
/// - paints each single atom
///
/// You can use this to first allocate a response and then modify, e.g., the [`Frame`] on the
/// [`AllocatedAtomLayout`] for interaction styling.
pub struct AtomLayout<'a> {
id: Option<Id>,
pub atoms: Atoms<'a>,
gap: Option<f32>,
pub(crate) frame: Frame,
pub(crate) sense: Sense,
fallback_text_color: Option<Color32>,
fallback_font: Option<FontSelection>,
min_size: Vec2,
wrap_mode: Option<TextWrapMode>,
align2: Option<Align2>,
}
impl Default for AtomLayout<'_> {
fn default() -> Self {
Self::new(())
}
}
impl<'a> AtomLayout<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self {
id: None,
atoms: atoms.into_atoms(),
gap: None,
frame: Frame::default(),
sense: Sense::hover(),
fallback_text_color: None,
fallback_font: None,
min_size: Vec2::ZERO,
wrap_mode: None,
align2: None,
}
}
/// Set the gap between atoms.
///
/// Default: `Spacing::icon_spacing`
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.gap = Some(gap);
self
}
/// Set the [`Frame`].
#[inline]
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = frame;
self
}
/// Set the [`Sense`] used when allocating the [`Response`].
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the fallback (default) text color.
///
/// Default: [`crate::Visuals::text_color`]
#[inline]
pub fn fallback_text_color(mut self, color: Color32) -> Self {
self.fallback_text_color = Some(color);
self
}
/// Set the fallback (default) font.
#[inline]
pub fn fallback_font(mut self, font: impl Into<FontSelection>) -> Self {
self.fallback_font = Some(font.into());
self
}
/// Set the minimum size of the Widget.
///
/// This will find and expand atoms with `grow: true`.
/// If there are no growable atoms then everything will be left-aligned.
#[inline]
pub fn min_size(mut self, size: Vec2) -> Self {
self.min_size = size;
self
}
/// Set the [`Id`] used to allocate a [`Response`].
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
/// Set the [`TextWrapMode`] for the [`crate::Atom`] marked as `shrink`.
///
/// Only a single [`crate::Atom`] may shrink. If this (or `ui.wrap_mode()`) is not
/// [`TextWrapMode::Extend`] and no item is set to shrink, the first (left-most)
/// [`AtomKind::Text`] will be set to shrink.
#[inline]
pub fn wrap_mode(mut self, wrap_mode: TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
/// Set the [`Align2`].
///
/// This will align the [`crate::Atom`]s within the [`Rect`] returned by [`Ui::allocate_space`].
///
/// The default is chosen based on the [`Ui`]s [`crate::Layout`]. See
/// [this snapshot](https://github.com/emilk/egui/blob/master/tests/egui_tests/tests/snapshots/layout/button.png)
/// for info on how the [`crate::Layout`] affects the alignment.
#[inline]
pub fn align2(mut self, align2: Align2) -> Self {
self.align2 = Some(align2);
self
}
/// [`AtomLayout::allocate`] and [`AllocatedAtomLayout::paint`] in one go.
pub fn show(self, ui: &mut Ui) -> AtomLayoutResponse {
self.allocate(ui).paint(ui)
}
/// Calculate sizes, create [`Galley`]s and allocate a [`Response`].
///
/// Use the returned [`AllocatedAtomLayout`] for painting.
pub fn allocate(self, ui: &mut Ui) -> AllocatedAtomLayout<'a> {
let Self {
id,
mut atoms,
gap,
frame,
sense,
fallback_text_color,
min_size,
wrap_mode,
align2,
fallback_font,
} = self;
let fallback_font = fallback_font.unwrap_or_default();
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
// If none is found, mark the first text item as `shrink`.
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.iter().any(|a| a.shrink);
if !any_shrink {
let first_text = atoms
.iter_mut()
.find(|a| matches!(a.kind, AtomKind::Text(..)));
if let Some(atom) = first_text {
atom.shrink = true; // Will make the text truncate or shrink depending on wrap_mode
}
}
}
let id = id.unwrap_or_else(|| ui.next_auto_id());
let fallback_text_color =
fallback_text_color.unwrap_or_else(|| ui.style().visuals.text_color());
let gap = gap.unwrap_or_else(|| ui.spacing().icon_spacing);
// The size available for the content
let available_inner_size = ui.available_size() - frame.total_margin().sum();
let mut desired_width = 0.0;
// intrinsic width / height is the ideal size of the widget, e.g. the size where the
// text is not wrapped. Used to set Response::intrinsic_size.
let mut intrinsic_width = 0.0;
let mut intrinsic_height = 0.0;
let mut height: f32 = 0.0;
let mut sized_items = SmallVec::new();
let mut grow_count = 0;
let mut shrink_item = None;
let align2 = align2.unwrap_or_else(|| {
Align2([ui.layout().horizontal_align(), ui.layout().vertical_align()])
});
if atoms.len() > 1 {
let gap_space = gap * (atoms.len() as f32 - 1.0);
desired_width += gap_space;
intrinsic_width += gap_space;
}
for (idx, item) in atoms.into_iter().enumerate() {
if item.grow {
grow_count += 1;
}
if item.shrink {
debug_assert!(
shrink_item.is_none(),
"Only one atomic may be marked as shrink. {item:?}"
);
if shrink_item.is_none() {
shrink_item = Some((idx, item));
continue;
}
}
let sized = item.into_sized(
ui,
available_inner_size,
Some(wrap_mode),
fallback_font.clone(),
);
let size = sized.size;
desired_width += size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
sized_items.push(sized);
}
if let Some((index, item)) = shrink_item {
// The `shrink` item gets the remaining space
let available_size_for_shrink_item = Vec2::new(
available_inner_size.x - desired_width,
available_inner_size.y,
);
let sized = item.into_sized(
ui,
available_size_for_shrink_item,
Some(wrap_mode),
fallback_font,
);
let size = sized.size;
desired_width += size.x;
intrinsic_width += sized.intrinsic_size.x;
height = height.at_least(size.y);
intrinsic_height = intrinsic_height.at_least(sized.intrinsic_size.y);
sized_items.insert(index, sized);
}
let margin = frame.total_margin();
let desired_size = Vec2::new(desired_width, height);
let frame_size = (desired_size + margin.sum()).at_least(min_size);
let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);
response.intrinsic_size =
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
AllocatedAtomLayout {
sized_atoms: sized_items,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
}
}
}
/// Instructions for painting an [`AtomLayout`].
#[derive(Clone, Debug)]
pub struct AllocatedAtomLayout<'a> {
pub sized_atoms: SmallVec<[SizedAtom<'a>; ATOMS_SMALL_VEC_SIZE]>,
pub frame: Frame,
pub fallback_text_color: Color32,
pub response: Response,
grow_count: usize,
// The size of the inner content, before any growing.
desired_size: Vec2,
align2: Align2,
gap: f32,
}
impl<'atom> AllocatedAtomLayout<'atom> {
pub fn iter_kinds(&self) -> impl Iterator<Item = &SizedAtomKind<'atom>> {
self.sized_atoms.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut SizedAtomKind<'atom>> {
self.sized_atoms.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'atom>> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'atom>> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Image(image, _) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &Arc<Galley>> + use<'atom, '_> {
self.iter_kinds().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut Arc<Galley>> + use<'atom, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let SizedAtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'atom>) -> Image<'atom>,
{
self.map_kind(|kind| {
if let SizedAtomKind::Image(image, size) = kind {
SizedAtomKind::Image(f(image), size)
} else {
kind
}
});
}
/// Paint the [`Frame`] and individual [`crate::Atom`]s.
pub fn paint(self, ui: &Ui) -> AtomLayoutResponse {
let Self {
sized_atoms,
frame,
fallback_text_color,
response,
grow_count,
desired_size,
align2,
gap,
} = self;
let inner_rect = response.rect - self.frame.total_margin();
ui.painter().add(frame.paint(inner_rect));
let width_to_fill = inner_rect.width();
let extra_space = f32::max(width_to_fill - desired_size.x, 0.0);
let grow_width = f32::max(extra_space / grow_count as f32, 0.0).floor_ui();
let aligned_rect = if grow_count > 0 {
align2.align_size_within_rect(Vec2::new(width_to_fill, desired_size.y), inner_rect)
} else {
align2.align_size_within_rect(desired_size, inner_rect)
};
let mut cursor = aligned_rect.left();
let mut response = AtomLayoutResponse::empty(response);
for sized in sized_atoms {
let size = sized.size;
// TODO(lucasmerlin): This is not ideal, since this might lead to accumulated rounding errors
// https://github.com/emilk/egui/pull/5830#discussion_r2079627864
let growth = if sized.is_grow() { grow_width } else { 0.0 };
let frame = aligned_rect
.with_min_x(cursor)
.with_max_x(cursor + size.x + growth);
cursor = frame.right() + gap;
let align = Align2::CENTER_CENTER;
let rect = align.align_size_within_rect(size, frame);
match sized.kind {
SizedAtomKind::Text(galley) => {
ui.painter().galley(rect.min, galley, fallback_text_color);
}
SizedAtomKind::Image(image, _) => {
image.paint_at(ui, rect);
}
SizedAtomKind::Custom(id) => {
debug_assert!(
!response.custom_rects.iter().any(|(i, _)| *i == id),
"Duplicate custom id"
);
response.custom_rects.push((id, rect));
}
SizedAtomKind::Empty => {}
}
}
response
}
}
/// Response from a [`AtomLayout::show`] or [`AllocatedAtomLayout::paint`].
///
/// Use [`AtomLayoutResponse::rect`] to get the response rects from [`AtomKind::Custom`].
#[derive(Clone, Debug)]
pub struct AtomLayoutResponse {
pub response: Response,
// There should rarely be more than one custom rect.
custom_rects: SmallVec<[(Id, Rect); 1]>,
}
impl AtomLayoutResponse {
pub fn empty(response: Response) -> Self {
Self {
response,
custom_rects: Default::default(),
}
}
pub fn custom_rects(&self) -> impl Iterator<Item = (Id, Rect)> + '_ {
self.custom_rects.iter().copied()
}
/// Use this together with [`AtomKind::Custom`] to add custom painting / child widgets.
///
/// NOTE: Don't `unwrap` rects, they might be empty when the widget is not visible.
pub fn rect(&self, id: Id) -> Option<Rect> {
self.custom_rects
.iter()
.find_map(|(i, r)| if *i == id { Some(*r) } else { None })
}
}
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
}
}
impl<'a> Deref for AtomLayout<'a> {
type Target = Atoms<'a>;
fn deref(&self) -> &Self::Target {
&self.atoms
}
}
impl DerefMut for AtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.atoms
}
}
impl<'a> Deref for AllocatedAtomLayout<'a> {
type Target = [SizedAtom<'a>];
fn deref(&self) -> &Self::Target {
&self.sized_atoms
}
}
impl DerefMut for AllocatedAtomLayout<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sized_atoms
}
}

View File

@@ -0,0 +1,258 @@
use crate::{Atom, AtomKind, Image, WidgetText};
use smallvec::SmallVec;
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
// Rarely there should be more than 2 atoms in one Widget.
// I guess it could happen in a menu button with Image and right text...
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// A list of [`Atom`]s.
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
impl<'a> Atoms<'a> {
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
atoms.into_atoms()
}
/// Insert a new [`Atom`] at the end of the list (right side).
pub fn push_right(&mut self, atom: impl Into<Atom<'a>>) {
self.0.push(atom.into());
}
/// Insert a new [`Atom`] at the beginning of the list (left side).
pub fn push_left(&mut self, atom: impl Into<Atom<'a>>) {
self.0.insert(0, atom.into());
}
/// Concatenate and return the text contents.
// TODO(lucasmerlin): It might not always make sense to return the concatenated text, e.g.
// in a submenu button there is a right text '⏵' which is now passed to the screen reader.
pub fn text(&self) -> Option<Cow<'_, str>> {
let mut string: Option<Cow<'_, str>> = None;
for atom in &self.0 {
if let AtomKind::Text(text) = &atom.kind {
if let Some(string) = &mut string {
let string = string.to_mut();
string.push(' ');
string.push_str(text.text());
} else {
string = Some(Cow::Borrowed(text.text()));
}
}
}
// If there is no text, try to find an image with alt text.
if string.is_none() {
string = self.iter().find_map(|a| match &a.kind {
AtomKind::Image(image) => image.alt_text.as_deref().map(Cow::Borrowed),
_ => None,
});
}
string
}
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
self.0.iter().map(|atom| &atom.kind)
}
pub fn iter_kinds_mut(&mut self) -> impl Iterator<Item = &mut AtomKind<'a>> {
self.0.iter_mut().map(|atom| &mut atom.kind)
}
pub fn iter_images(&self) -> impl Iterator<Item = &Image<'a>> {
self.iter_kinds().filter_map(|kind| {
if let AtomKind::Image(image) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_images_mut(&mut self) -> impl Iterator<Item = &mut Image<'a>> {
self.iter_kinds_mut().filter_map(|kind| {
if let AtomKind::Image(image) = kind {
Some(image)
} else {
None
}
})
}
pub fn iter_texts(&self) -> impl Iterator<Item = &WidgetText> + use<'_, 'a> {
self.iter_kinds().filter_map(|kind| {
if let AtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
pub fn iter_texts_mut(&mut self) -> impl Iterator<Item = &mut WidgetText> + use<'a, '_> {
self.iter_kinds_mut().filter_map(|kind| {
if let AtomKind::Text(text) = kind {
Some(text)
} else {
None
}
})
}
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)));
}
pub fn map_kind<F>(&mut self, mut f: F)
where
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
}
}
pub fn map_images<F>(&mut self, mut f: F)
where
F: FnMut(Image<'a>) -> Image<'a>,
{
self.map_kind(|kind| {
if let AtomKind::Image(image) = kind {
AtomKind::Image(f(image))
} else {
kind
}
});
}
pub fn map_texts<F>(&mut self, mut f: F)
where
F: FnMut(WidgetText) -> WidgetText,
{
self.map_kind(|kind| {
if let AtomKind::Text(text) = kind {
AtomKind::Text(f(text))
} else {
kind
}
});
}
}
impl<'a> IntoIterator for Atoms<'a> {
type Item = Atom<'a>;
type IntoIter = smallvec::IntoIter<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
/// Helper trait to convert a tuple of atoms into [`Atoms`].
///
/// ```
/// use egui::{Atoms, Image, IntoAtoms, RichText};
/// let atoms: Atoms = (
/// "Some text",
/// RichText::new("Some RichText"),
/// Image::new("some_image_url"),
/// ).into_atoms();
/// ```
impl<'a, T> IntoAtoms<'a> for T
where
T: Into<Atom<'a>>,
{
fn collect(self, atoms: &mut Atoms<'a>) {
atoms.push_right(self);
}
}
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
pub trait IntoAtoms<'a> {
fn collect(self, atoms: &mut Atoms<'a>);
fn into_atoms(self) -> Atoms<'a>
where
Self: Sized,
{
let mut atoms = Atoms::default();
self.collect(&mut atoms);
atoms
}
}
impl<'a> IntoAtoms<'a> for Atoms<'a> {
fn collect(self, atoms: &mut Self) {
atoms.0.extend(self.0);
}
}
macro_rules! all_the_atoms {
($($T:ident),*) => {
impl<'a, $($T),*> IntoAtoms<'a> for ($($T),*)
where
$($T: IntoAtoms<'a>),*
{
fn collect(self, _atoms: &mut Atoms<'a>) {
#[allow(clippy::allow_attributes, non_snake_case)]
let ($($T),*) = self;
$($T.collect(_atoms);)*
}
}
};
}
all_the_atoms!();
all_the_atoms!(T0, T1);
all_the_atoms!(T0, T1, T2);
all_the_atoms!(T0, T1, T2, T3);
all_the_atoms!(T0, T1, T2, T3, T4);
all_the_atoms!(T0, T1, T2, T3, T4, T5);
impl<'a> Deref for Atoms<'a> {
type Target = [Atom<'a>];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Atoms<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a, T: Into<Atom<'a>>> From<Vec<T>> for Atoms<'a> {
fn from(vec: Vec<T>) -> Self {
Atoms(vec.into_iter().map(Into::into).collect())
}
}
impl<'a, T: Into<Atom<'a>> + Clone> From<&[T]> for Atoms<'a> {
fn from(slice: &[T]) -> Self {
Atoms(slice.iter().cloned().map(Into::into).collect())
}
}
impl<'a, Item: Into<Atom<'a>>> FromIterator<Item> for Atoms<'a> {
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
Atoms(iter.into_iter().map(Into::into).collect())
}
}
#[cfg(test)]
mod tests {
use crate::Atoms;
#[test]
fn collect_atoms() {
let _: Atoms<'_> = ["Hello", "World"].into_iter().collect();
let _ = Atoms::from(vec!["Hi"]);
let _ = Atoms::from(["Hi"].as_slice());
}
}

View File

@@ -0,0 +1,15 @@
mod atom;
mod atom_ext;
mod atom_kind;
mod atom_layout;
mod atoms;
mod sized_atom;
mod sized_atom_kind;
pub use atom::*;
pub use atom_ext::*;
pub use atom_kind::*;
pub use atom_layout::*;
pub use atoms::*;
pub use sized_atom::*;
pub use sized_atom_kind::*;

View File

@@ -0,0 +1,26 @@
use crate::SizedAtomKind;
use emath::Vec2;
/// A [`crate::Atom`] which has been sized.
#[derive(Clone, Debug)]
pub struct SizedAtom<'a> {
pub(crate) grow: bool,
/// The size of the atom.
///
/// Used for placing this atom in [`crate::AtomLayout`], the cursor will advance by
/// size.x + gap.
pub size: Vec2,
/// Intrinsic size of the atom. This is used to calculate `Response::intrinsic_size`.
pub intrinsic_size: Vec2,
pub kind: SizedAtomKind<'a>,
}
impl SizedAtom<'_> {
/// Was this [`crate::Atom`] marked as `grow`?
pub fn is_grow(&self) -> bool {
self.grow
}
}

View File

@@ -0,0 +1,25 @@
use crate::{Id, Image};
use emath::Vec2;
use epaint::Galley;
use std::sync::Arc;
/// A sized [`crate::AtomKind`].
#[derive(Clone, Default, Debug)]
pub enum SizedAtomKind<'a> {
#[default]
Empty,
Text(Arc<Galley>),
Image(Image<'a>, Vec2),
Custom(Id),
}
impl SizedAtomKind<'_> {
/// Get the calculated size.
pub fn size(&self) -> Vec2 {
match self {
SizedAtomKind::Text(galley) => galley.size(),
SizedAtomKind::Image(_, size) => *size,
SizedAtomKind::Empty | SizedAtomKind::Custom(_) => Vec2::ZERO,
}
}
}

View File

@@ -19,7 +19,7 @@ use super::CacheTrait;
///
/// # let mut cache_storage = CacheStorage::default();
/// let mut cache = cache_storage.cache::<CharCountCache<'_>>();
/// assert_eq!(cache.get("hello"), 5);
/// assert_eq!(*cache.get("hello"), 5);
/// ```
#[derive(Default)]
pub struct CacheStorage {
@@ -28,10 +28,13 @@ pub struct CacheStorage {
impl CacheStorage {
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
self.caches
let cache = self
.caches
.entry(std::any::TypeId::of::<Cache>())
.or_insert_with(|| Box::<Cache>::default())
.as_any_mut()
.or_insert_with(|| Box::<Cache>::default());
#[expect(clippy::unwrap_used)]
(cache.as_mut() as &mut dyn std::any::Any)
.downcast_mut::<Cache>()
.unwrap()
}

View File

@@ -1,11 +1,9 @@
/// A cache, storing some value for some length of time.
#[allow(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync {
#[expect(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync + std::any::Any {
/// Call once per frame to evict cache.
fn update(&mut self);
/// Number of values currently in the cache.
fn len(&self) -> usize;
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}

View File

@@ -46,10 +46,9 @@ impl<Value, Computer> FrameCache<Value, Computer> {
impl<Value, Computer> FrameCache<Value, Computer> {
/// Get from cache (if the same key was used last frame)
/// or recompute and store in the cache.
pub fn get<Key>(&mut self, key: Key) -> Value
pub fn get<Key>(&mut self, key: Key) -> &Value
where
Key: Copy + std::hash::Hash,
Value: Clone,
Computer: ComputerMut<Key, Value>,
{
let hash = crate::util::hash(key);
@@ -58,12 +57,12 @@ impl<Value, Computer> FrameCache<Value, Computer> {
std::collections::hash_map::Entry::Occupied(entry) => {
let cached = entry.into_mut();
cached.0 = self.generation;
cached.1.clone()
&cached.1
}
std::collections::hash_map::Entry::Vacant(entry) => {
let value = self.computer.compute(key);
entry.insert((self.generation, value.clone()));
value
let inserted = entry.insert((self.generation, value));
&inserted.1
}
}
}
@@ -79,8 +78,4 @@ impl<Value: 'static + Send + Sync, Computer: 'static + Send + Sync> CacheTrait
fn len(&self) -> usize {
self.cache.len()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

View File

@@ -54,8 +54,4 @@ where
fn len(&self) -> usize {
self.cache.len()
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}

View File

@@ -20,10 +20,10 @@ pub fn capture() -> String {
backtrace::resolve_frame(frame, |symbol| {
let mut file_and_line = symbol.filename().map(shorten_source_file_path);
if let Some(file_and_line) = &mut file_and_line {
if let Some(line_nr) = symbol.lineno() {
file_and_line.push_str(&format!(":{line_nr}"));
}
if let Some(file_and_line) = &mut file_and_line
&& let Some(line_nr) = symbol.lineno()
{
file_and_line.push_str(&format!(":{line_nr}"));
}
let file_and_line = file_and_line.unwrap_or_default();
@@ -204,12 +204,17 @@ fn shorten_source_file_path(path: &std::path::Path) -> String {
#[test]
fn test_shorten_path() {
for (before, after) in [
("/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs", "tokio-1.24.1/src/runtime/runtime.rs"),
(
"/Users/emilk/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-1.24.1/src/runtime/runtime.rs",
"tokio-1.24.1/src/runtime/runtime.rs",
),
("crates/rerun/src/main.rs", "rerun/src/main.rs"),
("/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs", "core/src/ops/function.rs"),
(
"/rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/ops/function.rs",
"core/src/ops/function.rs",
),
("/weird/path/file.rs", "/weird/path/file.rs"),
]
{
] {
use std::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);

View File

@@ -5,8 +5,8 @@
use emath::GuiRounding as _;
use crate::{
emath, pos2, Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt, Order, Pos2, Rect,
Response, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState,
Align2, Context, Id, InnerResponse, LayerId, Layout, NumExt as _, Order, Pos2, Rect, Response,
Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetRect, WidgetWithState, emath, pos2,
};
/// State of an [`Area`] that is persisted between frames.
@@ -121,6 +121,7 @@ pub struct Area {
new_pos: Option<Pos2>,
fade_in: bool,
layout: Layout,
sizing_pass: bool,
}
impl WidgetWithState for Area {
@@ -147,6 +148,7 @@ impl Area {
anchor: None,
fade_in: true,
layout: Layout::default(),
sizing_pass: false,
}
}
@@ -170,7 +172,7 @@ impl Area {
/// Set the [`UiStackInfo`] of the area's [`Ui`].
///
/// Default to [`UiStackInfo::new(UiKind::GenericArea)`].
/// Default to [`UiStackInfo`] with kind [`UiKind::GenericArea`].
#[inline]
pub fn info(mut self, info: UiStackInfo) -> Self {
self.info = info;
@@ -357,6 +359,27 @@ impl Area {
self.layout = layout;
self
}
/// While true, a sizing pass will be done. This means the area will be invisible
/// and the contents will be laid out to estimate the proper containing size of the area.
/// If false, there will be no change to the default area behavior. This is useful if the
/// area contents area dynamic and you need to need to make sure the area adjusts its size
/// accordingly.
///
/// This should only be set to true during the specific frames you want force a sizing pass.
/// Do NOT hard-code this as `.sizing_pass(true)`, as it will cause the area to never be
/// visible.
///
/// # Arguments
/// - resize: If true, the area will be resized to fit its contents. False will keep the
/// default area resizing behavior.
///
/// Default: `false`.
#[inline]
pub fn sizing_pass(mut self, resize: bool) -> Self {
self.sizing_pass = resize;
self
}
}
pub(crate) struct Prepared {
@@ -410,9 +433,10 @@ impl Area {
constrain_rect,
fade_in,
layout,
sizing_pass: force_sizing_pass,
} = self;
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.screen_rect());
let constrain_rect = constrain_rect.unwrap_or_else(|| ctx.content_rect());
let layer_id = LayerId::new(order, id);
@@ -425,13 +449,17 @@ impl Area {
interactable,
last_became_visible_at: None,
});
if force_sizing_pass {
sizing_pass = true;
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, layer_id))
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
});
state.interactable = interactable;
@@ -441,7 +469,7 @@ impl Area {
// during the sizing pass we will use this as the max size
let mut size = default_size;
let default_area_size = ctx.style().spacing.default_area_size;
let default_area_size = ctx.global_style().spacing.default_area_size;
if size.x.is_nan() {
size.x = default_area_size.x;
}
@@ -477,9 +505,9 @@ impl Area {
let interact_id = layer_id.id.with("move");
let sense = sense.unwrap_or_else(|| {
if movable {
Sense::drag()
Sense::DRAG
} else if interactable {
Sense::click() // allow clicks to bring to front
Sense::CLICK // allow clicks to bring to front
} else {
Sense::hover()
}
@@ -495,12 +523,24 @@ impl Area {
enabled,
},
true,
Default::default(),
);
if movable && move_response.dragged() {
if let Some(pivot_pos) = &mut state.pivot_pos {
*pivot_pos += move_response.drag_delta();
}
// Used to prevent drift
let pivot_at_start_of_drag_id = id.with("pivot_at_drag_start");
if movable
&& move_response.dragged()
&& let Some(pivot_pos) = &mut state.pivot_pos
{
let pivot_at_start_of_drag = ctx.data_mut(|data| {
*data.get_temp_mut_or::<Pos2>(pivot_at_start_of_drag_id, *pivot_pos)
});
*pivot_pos =
pivot_at_start_of_drag + move_response.total_drag_delta().unwrap_or_default();
} else {
ctx.data_mut(|data| data.remove::<Pos2>(pivot_at_start_of_drag_id));
}
if (move_response.dragged() || move_response.clicked())
@@ -514,13 +554,14 @@ impl Area {
move_response
};
if constrain {
state.set_left_top_pos(
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(state.left_top_pos());
state.set_left_top_pos(round_area_position(
ctx,
if constrain {
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min
} else {
state.left_top_pos()
},
));
// Update response with possibly moved/constrained rect:
move_response.rect = state.rect();
@@ -541,6 +582,16 @@ impl Area {
}
}
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
// We round a lot of rendering to pixels, so we round the whole
// area positions to pixels too, so avoid widgets appearing to float
// around independently of each other when the area is dragged.
// But just in case pixels_per_point is irrational,
// we then also round to ui coordinates:
pos.round_to_pixels(ctx.pixels_per_point()).round_ui()
}
impl Prepared {
pub(crate) fn state(&self) -> &AreaState {
&self.state
@@ -566,6 +617,7 @@ impl Prepared {
.layer_id(self.layer_id)
.max_rect(max_rect)
.layout(self.layout)
.accessibility_parent(self.move_response.id)
.closable();
if !self.enabled {
@@ -578,16 +630,17 @@ impl Prepared {
let mut ui = Ui::new(ctx.clone(), self.layer_id.id, ui_builder);
ui.set_clip_rect(self.constrain_rect); // Don't paint outside our bounds
if self.fade_in {
if let Some(last_became_visible_at) = self.state.last_became_visible_at {
let age =
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
let opacity = crate::remap_clamp(age, 0.0..=ctx.style().animation_time, 0.0..=1.0);
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
ui.multiply_opacity(opacity);
if opacity < 1.0 {
ctx.request_repaint();
}
if self.fade_in
&& let Some(last_became_visible_at) = self.state.last_became_visible_at
{
let age =
ctx.input(|i| (i.time - last_became_visible_at) as f32 + i.predicted_dt / 2.0);
let opacity =
crate::remap_clamp(age, 0.0..=ctx.global_style().animation_time, 0.0..=1.0);
let opacity = emath::easing::quadratic_out(opacity); // slow fade-out = quick fade-in
ui.multiply_opacity(opacity);
if opacity < 1.0 {
ctx.request_repaint();
}
}
@@ -602,7 +655,7 @@ impl Prepared {
self.move_response.id
}
#[allow(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
#[expect(clippy::needless_pass_by_value)] // intentional to swallow up `content_ui`.
pub(crate) fn end(self, ctx: &Context, content_ui: Ui) -> Response {
let Self {
info: _,
@@ -647,7 +700,7 @@ fn pointer_pressed_on_area(ctx: &Context, layer_id: LayerId) -> bool {
}
}
fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
fn automatic_area_position(ctx: &Context, constrain_rect: Rect, layer_id: LayerId) -> Pos2 {
let mut existing: Vec<Rect> = ctx.memory(|mem| {
mem.areas()
.visible_windows()
@@ -658,13 +711,9 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
});
existing.sort_by_key(|r| r.left().round() as i32);
// NOTE: for the benefit of the egui demo, we position the windows so they don't
// cover the side panels, which means we use `available_rect` here instead of `constrain_rect` or `screen_rect`.
let available_rect = ctx.available_rect();
let spacing = 16.0;
let left = available_rect.left() + spacing;
let top = available_rect.top() + spacing;
let left = constrain_rect.left() + spacing;
let top = constrain_rect.top() + spacing;
if existing.is_empty() {
return pos2(left, top);
@@ -674,10 +723,11 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
let mut column_bbs = vec![existing[0]];
for &rect in &existing {
#[expect(clippy::unwrap_used)]
let current_column_bb = column_bbs.last_mut().unwrap();
if rect.left() < current_column_bb.right() {
// same column
*current_column_bb = current_column_bb.union(rect);
*current_column_bb |= rect;
} else {
// new column
column_bbs.push(rect);
@@ -698,14 +748,15 @@ fn automatic_area_position(ctx: &Context, layer_id: LayerId) -> Pos2 {
// Find first column with some available space at the bottom of it:
for col_bb in &column_bbs {
if col_bb.bottom() < available_rect.center().y {
if col_bb.bottom() < constrain_rect.center().y {
return pos2(col_bb.left(), col_bb.bottom() + spacing);
}
}
// Maybe we can fit a new column?
#[expect(clippy::unwrap_used)]
let rightmost = column_bbs.last().unwrap().right();
if rightmost + 200.0 < available_rect.right() {
if rightmost + 200.0 < constrain_rect.right() {
return pos2(rightmost + spacing, top);
}

View File

@@ -1,5 +1,13 @@
#[expect(unused_imports)]
use crate::{Ui, UiBuilder};
use std::sync::atomic::AtomicBool;
/// A tag to mark a container as closable.
///
/// Usually set via [`UiBuilder::closable`].
///
/// [`Ui::close`] will find the closest parent [`ClosableTag`] and set its `close` field to `true`.
/// Use [`Ui::should_close`] to check if close has been called.
#[derive(Debug, Default)]
pub struct ClosableTag {
pub close: AtomicBool,

View File

@@ -2,9 +2,9 @@ use std::fmt::Debug;
use std::hash::Hash;
use crate::{
emath, epaint, pos2, remap, remap_clamp, vec2, Context, Id, InnerResponse, NumExt, Rect,
Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2,
WidgetInfo, WidgetText, WidgetType,
Context, Id, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke, TextStyle,
TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetText, WidgetType,
emath, epaint, pos2, remap, remap_clamp, vec2,
};
use emath::GuiRounding as _;
use epaint::{Shape, StrokeKind};
@@ -70,7 +70,7 @@ impl CollapsingState {
pub fn toggle(&mut self, ui: &Ui) {
self.state.open = !self.state.open;
ui.ctx().request_repaint();
ui.request_repaint();
}
/// 0 for closed, 1 for open, with tweening

View File

@@ -2,12 +2,12 @@ use epaint::Shape;
use std::fmt::Debug;
use crate::{
epaint, style::StyleModifier, style::WidgetVisuals, vec2, Align2, Context, Id, InnerResponse,
NumExt, Painter, Popup, PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType,
Align2, Context, Id, InnerResponse, NumExt as _, Painter, Popup, PopupCloseBehavior, Rect,
Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui, UiBuilder, Vec2, WidgetInfo,
WidgetText, WidgetType, epaint, style::StyleModifier, style::WidgetVisuals, vec2,
};
#[allow(unused_imports)] // Documentation
#[expect(unused_imports)] // Documentation
use crate::style::Spacing;
/// A function that paints the [`ComboBox`] icon
@@ -17,9 +17,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[derive(Debug, PartialEq)]
/// # #[derive(Debug, PartialEq, Copy, Clone)]
/// # enum Enum { First, Second, Third }
/// # let mut selected = Enum::First;
/// let before = selected;
/// egui::ComboBox::from_label("Select one!")
/// .selected_text(format!("{:?}", selected))
/// .show_ui(ui, |ui| {
@@ -28,6 +29,10 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// ui.selectable_value(&mut selected, Enum::Third, "Third");
/// }
/// );
///
/// if selected != before {
/// // Handle selection change
/// }
/// # });
/// ```
#[must_use = "You should call .show*"]
@@ -40,6 +45,7 @@ pub struct ComboBox {
icon: Option<IconPainter>,
wrap_mode: Option<TextWrapMode>,
close_behavior: Option<PopupCloseBehavior>,
popup_style: StyleModifier,
}
impl ComboBox {
@@ -54,6 +60,7 @@ impl ComboBox {
icon: None,
wrap_mode: None,
close_behavior: None,
popup_style: StyleModifier::default(),
}
}
@@ -69,6 +76,7 @@ impl ComboBox {
icon: None,
wrap_mode: None,
close_behavior: None,
popup_style: StyleModifier::default(),
}
}
@@ -83,11 +91,12 @@ impl ComboBox {
icon: None,
wrap_mode: None,
close_behavior: None,
popup_style: StyleModifier::default(),
}
}
/// Without label.
#[deprecated = "Renamed id_salt"]
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash + Debug) -> Self {
Self::from_id_salt(id_salt)
}
@@ -187,6 +196,16 @@ impl ComboBox {
self
}
/// Set the style of the popup menu.
///
/// Could for example be used with [`crate::containers::menu::menu_style`] to get the frame-less
/// menu button style.
#[inline]
pub fn popup_style(mut self, popup_style: StyleModifier) -> Self {
self.popup_style = popup_style;
self
}
/// Show the combo box, with the given ui code for the menu contents.
///
/// Returns `InnerResponse { inner: None }` if the combo box is closed.
@@ -212,6 +231,7 @@ impl ComboBox {
icon,
wrap_mode,
close_behavior,
popup_style,
} = self;
let button_id = ui.make_persistent_id(id_salt);
@@ -220,21 +240,24 @@ impl ComboBox {
let mut ir = combo_box_dyn(
ui,
button_id,
selected_text,
selected_text.clone(),
menu_contents,
icon,
wrap_mode,
close_behavior,
popup_style,
(width, height),
);
ir.response.widget_info(|| {
let mut info = WidgetInfo::new(WidgetType::ComboBox);
info.enabled = ui.is_enabled();
info.current_text_value = Some(selected_text.text().to_owned());
info
});
if let Some(label) = label {
ir.response.widget_info(|| {
WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), label.text())
});
ir.response |= ui.label(label);
} else {
ir.response
.widget_info(|| WidgetInfo::labeled(WidgetType::ComboBox, ui.is_enabled(), ""));
let label_response = ui.label(label);
ir.response = ir.response.labelled_by(label_response.id);
ir.response |= label_response;
}
ir
})
@@ -289,7 +312,7 @@ impl ComboBox {
/// Check if the [`ComboBox`] with the given id has its popup menu currently opened.
pub fn is_open(ctx: &Context, id: Id) -> bool {
ctx.memory(|m| m.is_popup_open(Self::widget_to_popup_id(id)))
Popup::is_id_open(ctx, Self::widget_to_popup_id(id))
}
/// Convert a [`ComboBox`] id to the id used to store it's popup state.
@@ -298,7 +321,7 @@ impl ComboBox {
}
}
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn combo_box_dyn<'c, R>(
ui: &mut Ui,
button_id: Id,
@@ -307,11 +330,12 @@ fn combo_box_dyn<'c, R>(
icon: Option<IconPainter>,
wrap_mode: Option<TextWrapMode>,
close_behavior: Option<PopupCloseBehavior>,
popup_style: StyleModifier,
(width, height): (Option<f32>, Option<f32>),
) -> InnerResponse<Option<R>> {
let popup_id = ComboBox::widget_to_popup_id(button_id);
let is_popup_open = ui.memory(|m| m.is_popup_open(popup_id));
let is_popup_open = Popup::is_id_open(ui.ctx(), popup_id);
let wrap_mode = wrap_mode.unwrap_or_else(|| ui.wrap_mode());
@@ -375,9 +399,9 @@ fn combo_box_dyn<'c, R>(
let inner = Popup::menu(&button_response)
.id(popup_id)
.style(StyleModifier::default())
.width(button_response.rect.width())
.close_behavior(close_behavior)
.style(popup_style)
.show(|ui| {
ui.set_min_width(ui.available_width());

View File

@@ -1,8 +1,8 @@
//! Frame container
use crate::{
epaint, layers::ShapeIdx, InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind,
UiStackInfo,
InnerResponse, Response, Sense, Style, Ui, UiBuilder, UiKind, UiStackInfo, epaint,
layers::ShapeIdx,
};
use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stroke};
@@ -46,7 +46,7 @@ use epaint::{Color32, CornerRadius, Margin, MarginF32, Rect, Shadow, Shape, Stro
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::Frame::none()
/// egui::Frame::NONE
/// .fill(egui::Color32::RED)
/// .show(ui, |ui| {
/// ui.label("Label with red background");
@@ -143,7 +143,8 @@ pub struct Frame {
#[test]
fn frame_size() {
assert_eq!(
std::mem::size_of::<Frame>(), 32,
std::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!(
@@ -335,7 +336,7 @@ impl Frame {
impl Frame {
/// How much extra space the frame uses up compared to the content.
///
/// [`Self::inner_margin`] + [`Self.stroke`]`.width` + [`Self::outer_margin`].
/// [`Self::inner_margin`] + [`Self::stroke`]`.width` + [`Self::outer_margin`].
#[inline]
pub fn total_margin(&self) -> MarginF32 {
MarginF32::from(self.inner_margin)

View File

@@ -1,9 +1,19 @@
//! Popup menus, context menus and menu bars.
//!
//! Show menus via
//! - [`Popup::menu`] and [`Popup::context_menu`]
//! - [`Ui::menu_button`], [`MenuButton`] and [`SubMenuButton`]
//! - [`MenuBar`]
//! - [`Response::context_menu`]
//!
//! See [`MenuBar`] for an example.
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, Layout, Popup, PopupCloseBehavior,
Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget, WidgetText,
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{vec2, Align, RectAlign, Vec2};
use emath::{Align, RectAlign, Vec2, vec2};
use epaint::Stroke;
/// Apply a menu style to the [`Style`].
@@ -50,6 +60,7 @@ pub fn is_in_menu(ui: &Ui) -> bool {
false
}
/// Configuration and style for menus.
#[derive(Clone, Debug)]
pub struct MenuConfig {
/// Is this a menu bar?
@@ -120,8 +131,10 @@ impl MenuConfig {
}
}
/// Holds the state of the menu.
#[derive(Clone)]
pub struct MenuState {
/// The currently open sub menu in this menu.
pub open_item: Option<Id>,
last_visible_pass: u64,
}
@@ -139,7 +152,8 @@ impl MenuState {
pub fn from_id<R>(ctx: &Context, id: Id, f: impl FnOnce(&mut Self) -> R) -> R {
let pass_nr = ctx.cumulative_pass_nr();
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_insert_with(id.with(Self::ID), || Self {
let state_id = id.with(Self::ID);
let mut state = data.get_temp(state_id).unwrap_or(Self {
open_item: None,
last_visible_pass: pass_nr,
});
@@ -147,28 +161,68 @@ impl MenuState {
if state.last_visible_pass + 1 < pass_nr {
state.open_item = None;
}
state.last_visible_pass = pass_nr;
f(state)
if let Some(item) = state.open_item
&& data
.get_temp(item.with(Self::ID))
.is_none_or(|item: Self| item.last_visible_pass + 1 < pass_nr)
{
// If the open item wasn't shown for at least a frame, reset the open item
state.open_item = None;
}
let r = f(&mut state);
data.insert_temp(state_id, state);
r
})
}
pub fn mark_shown(ctx: &Context, id: Id) {
let pass_nr = ctx.cumulative_pass_nr();
Self::from_id(ctx, id, |state| {
state.last_visible_pass = pass_nr;
});
}
/// Is the menu with this id the deepest sub menu? (-> no child sub menu is open)
pub fn is_deepest_sub_menu(ctx: &Context, id: Id) -> bool {
Self::from_id(ctx, id, |state| state.open_item.is_none())
///
/// Note: This only returns correct results if called after the menu contents were shown.
pub fn is_deepest_open_sub_menu(ctx: &Context, id: Id) -> bool {
let pass_nr = ctx.cumulative_pass_nr();
let open_item = Self::from_id(ctx, id, |state| state.open_item);
// If we have some open item, check if that was actually shown this frame
open_item.is_none_or(|submenu_id| {
Self::from_id(ctx, submenu_id, |state| state.last_visible_pass != pass_nr)
})
}
}
/// Horizontal menu bar where you can add [`MenuButton`]s.
///
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
///
/// ### Example:
/// ```
/// # egui::__run_test_ui(|ui| {
/// egui::MenuBar::new().ui(ui, |ui| {
/// ui.menu_button("File", |ui| {
/// if ui.button("Quit").clicked() {
/// ui.send_viewport_cmd(egui::ViewportCommand::Close);
/// }
/// });
/// });
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct Bar {
pub struct MenuBar {
config: MenuConfig,
style: StyleModifier,
}
impl Default for Bar {
#[deprecated = "Renamed to `egui::MenuBar`"]
pub type Bar = MenuBar;
impl Default for MenuBar {
fn default() -> Self {
Self {
config: MenuConfig::default(),
@@ -177,7 +231,7 @@ impl Default for Bar {
}
}
impl Bar {
impl MenuBar {
pub fn new() -> Self {
Self::default()
}
@@ -233,8 +287,8 @@ impl Bar {
/// A thin wrapper around a [`Button`] that shows a [`Popup::menu`] when clicked.
///
/// The only thing this does is search for the current menu config (if set via [`Bar`]).
/// If your menu button is not in a [`Bar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
/// The only thing this does is search for the current menu config (if set via [`MenuBar`]).
/// If your menu button is not in a [`MenuBar`] it's fine to use [`Ui::button`] and [`Popup::menu`]
/// directly.
pub struct MenuButton<'a> {
pub button: Button<'a>,
@@ -242,8 +296,8 @@ pub struct MenuButton<'a> {
}
impl<'a> MenuButton<'a> {
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::from_button(Button::new(text))
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()))
}
/// Set the config for the menu.
@@ -292,8 +346,8 @@ impl<'a> SubMenuButton<'a> {
/// The default right arrow symbol: `"⏵"`
pub const RIGHT_ARROW: &'static str = "";
pub fn new(text: impl Into<WidgetText>) -> Self {
Self::from_button(Button::new(text).right_text(""))
pub fn new(atoms: impl IntoAtoms<'a>) -> Self {
Self::from_button(Button::new(atoms.into_atoms()).right_text(""))
}
/// Create a new submenu button from a [`Button`].
@@ -340,6 +394,10 @@ impl<'a> SubMenuButton<'a> {
}
}
/// Show a submenu in a menu.
///
/// Useful if you want to make custom menu buttons.
/// Usually, just use [`MenuButton`] or [`SubMenuButton`] instead.
#[derive(Clone, Debug, Default)]
pub struct SubMenu {
config: Option<MenuConfig>,
@@ -365,6 +423,9 @@ impl SubMenu {
}
/// Show the submenu.
///
/// This does some heuristics to check if the `button_response` was the last thing in the
/// menu that was hovered/clicked, and if so, shows the submenu.
pub fn show<R>(
self,
ui: &Ui,
@@ -375,6 +436,7 @@ impl SubMenu {
let id = Self::id_from_widget_id(button_response.id);
// Get the state from the parent menu
let (open_item, menu_id, parent_config) = MenuState::from_ui(ui, |state, stack| {
(state.open_item, stack.id, MenuConfig::from_stack(stack))
});
@@ -382,11 +444,8 @@ impl SubMenu {
let mut menu_config = self.config.unwrap_or_else(|| parent_config.clone());
menu_config.bar = false;
let menu_root_response = ui
.ctx()
.read_response(menu_id)
// Since we are a child of that ui, this should always exist
.unwrap();
#[expect(clippy::unwrap_used)] // Since we are a child of that ui, this should always exist
let menu_root_response = ui.ctx().read_response(menu_id).unwrap();
let hover_pos = ui.ctx().pointer_hover_pos();
@@ -412,11 +471,13 @@ impl SubMenu {
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
// The clicked handler is there for accessibility (keyboard navigation)
if (!is_any_open && is_hovered) || button_response.clicked() {
let should_open =
ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
if should_open {
set_open = Some(true);
is_open = true;
// Ensure that all other sub menus are closed when we open the menu
MenuState::from_id(ui.ctx(), id, |state| {
MenuState::from_id(ui.ctx(), menu_id, |state| {
state.open_item = None;
});
}
@@ -452,7 +513,7 @@ impl SubMenu {
if let Some(popup_response) = &popup_response {
// If no child sub menu is open means we must be the deepest child sub menu.
let is_deepest_submenu = MenuState::is_deepest_sub_menu(ui.ctx(), id);
let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
// If the user clicks and the cursor is not hovering over our menu rect, it's
// safe to assume they clicked outside the menu, so we close everything.
@@ -492,7 +553,7 @@ impl SubMenu {
if is_moving_towards_rect {
// We need to repaint while this is true, so we can detect when
// the pointer is no longer moving towards the rect
ui.ctx().request_repaint();
ui.request_repaint();
}
let hovering_other_menu_entry = is_open
&& !is_hovered

View File

@@ -1,9 +1,9 @@
//! Containers are pieces of the UI which wraps other pieces of UI. Examples: [`Window`], [`ScrollArea`], [`Resize`], [`SidePanel`], etc.
//! Containers are pieces of the UI which wraps other pieces of UI. Examples: [`Window`], [`ScrollArea`], [`Resize`], [`Panel`], etc.
//!
//! For instance, a [`Frame`] adds a frame and background to some contained UI.
pub(crate) mod area;
pub mod close_tag;
mod close_tag;
pub mod collapsing_header;
mod combo_box;
pub mod frame;
@@ -21,15 +21,16 @@ pub(crate) mod window;
pub use {
area::{Area, AreaState},
close_tag::ClosableTag,
collapsing_header::{CollapsingHeader, CollapsingResponse},
combo_box::*,
frame::Frame,
modal::{Modal, ModalResponse},
old_popup::*,
panel::{CentralPanel, SidePanel, TopBottomPanel},
panel::*,
popup::*,
resize::Resize,
scene::Scene,
scene::{DragPanButtons, Scene},
scroll_area::ScrollArea,
sides::Sides,
tooltip::*,

View File

@@ -1,7 +1,8 @@
use emath::{Align2, Vec2};
use crate::{
Area, Color32, Context, Frame, Id, InnerResponse, Order, Response, Sense, Ui, UiBuilder, UiKind,
};
use emath::{Align2, Vec2};
/// A modal dialog.
///
@@ -10,7 +11,7 @@ use emath::{Align2, Vec2};
///
/// You can show multiple modals on top of each other. The topmost modal will always be
/// the most recently shown one.
/// If multiple modals are newly shown in the same frame, the order of the modals not undefined
/// If multiple modals are newly shown in the same frame, the order of the modals is undefined
/// (either first or second could be top).
pub struct Modal {
pub area: Area,
@@ -80,18 +81,16 @@ impl Modal {
frame,
} = self;
let (is_top_modal, any_popup_open) = ctx.memory_mut(|mem| {
let is_top_modal = ctx.memory_mut(|mem| {
mem.set_modal_layer(area.layer());
(
mem.top_modal_layer() == Some(area.layer()),
mem.any_popup_open(),
)
mem.top_modal_layer() == Some(area.layer())
});
let any_popup_open = crate::Popup::is_any_open(ctx);
let InnerResponse {
inner: (inner, backdrop_response),
response,
} = area.show(ctx, |ui| {
let bg_rect = ui.ctx().screen_rect();
let bg_rect = ui.ctx().content_rect();
let bg_sense = Sense::CLICK | Sense::DRAG;
let mut backdrop = ui.new_child(UiBuilder::new().sense(bg_sense).max_rect(bg_rect));
backdrop.set_min_size(bg_rect.size());

View File

@@ -1,10 +1,10 @@
//! Old and deprecated API for popups. Use [`Popup`] instead.
#![allow(deprecated)]
#![expect(deprecated)]
use crate::containers::tooltip::Tooltip;
use crate::{
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
Response, Ui, Widget, WidgetText,
Response, Ui, Widget as _, WidgetText,
};
use emath::RectAlign;
// ----------------------------------------------------------------------------
@@ -19,7 +19,7 @@ use emath::RectAlign;
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[allow(deprecated)]
/// # #[expect(deprecated)]
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
@@ -61,7 +61,7 @@ pub fn show_tooltip_at_pointer<R>(
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(widget_id, ctx.clone(), PopupAnchor::Pointer, parent_layer)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
.gap(12.0)
.show(add_contents)
.map(|response| response.inner)
@@ -78,7 +78,7 @@ pub fn show_tooltip_for<R>(
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(widget_id, ctx.clone(), *widget_rect, parent_layer)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect)
.show(add_contents)
.map(|response| response.inner)
}
@@ -94,7 +94,7 @@ pub fn show_tooltip_at<R>(
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::new(widget_id, ctx.clone(), suggested_position, parent_layer)
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}
@@ -177,7 +177,7 @@ pub fn popup_below_widget<R>(
/// }
/// let below = egui::AboveOrBelow::Below;
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
/// # #[allow(deprecated)]
/// # #[expect(deprecated)]
/// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| {
/// ui.set_min_width(200.0); // if you want to control the size
/// ui.label("Some more info, or things you can select:");

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,15 @@
use crate::containers::menu::{menu_style, MenuConfig, MenuState};
use crate::style::StyleModifier;
#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API.
use std::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
use crate::{
Area, AreaState, Context, Frame, Id, InnerResponse, Key, LayerId, Layout, Order, Response,
Sense, Ui, UiKind, UiStackInfo,
containers::menu::{MenuConfig, MenuState, menu_style},
style::StyleModifier,
};
use emath::{vec2, Align, Pos2, Rect, RectAlign, Vec2};
use std::iter::once;
/// What should we anchor the popup to?
///
@@ -64,9 +68,7 @@ impl PopupAnchor {
match self {
Self::ParentRect(rect) => Some(rect),
Self::Pointer => ctx.pointer_hover_pos().map(Rect::from_pos),
Self::PointerFixed => ctx
.memory(|mem| mem.popup_position(popup_id))
.map(Rect::from_pos),
Self::PointerFixed => Popup::position_of_id(ctx, popup_id).map(Rect::from_pos),
Self::Position(pos) => Some(Rect::from_pos(pos)),
}
}
@@ -122,12 +124,12 @@ enum OpenKind<'a> {
impl OpenKind<'_> {
/// Returns `true` if the popup should be open
fn is_open(&self, id: Id, ctx: &Context) -> bool {
fn is_open(&self, popup_id: Id, ctx: &Context) -> bool {
match self {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => ctx.memory(|mem| mem.is_popup_open(id)),
OpenKind::Memory { .. } => Popup::is_id_open(ctx, popup_id),
}
}
}
@@ -160,6 +162,8 @@ impl From<PopupKind> for UiKind {
}
}
/// A popup container.
#[must_use = "Call `.show()` to actually display the popup"]
pub struct Popup<'a> {
id: Id,
ctx: Context,
@@ -175,9 +179,6 @@ pub struct Popup<'a> {
/// Gap between the anchor and the popup
gap: f32,
/// Used later depending on close behavior
widget_clicked_elsewhere: bool,
/// Default width passed to the Area
width: Option<f32>,
sense: Sense,
@@ -201,7 +202,6 @@ impl<'a> Popup<'a> {
rect_align: RectAlign::BOTTOM_START,
alternative_aligns: None,
gap: 0.0,
widget_clicked_elsewhere: false,
width: None,
sense: Sense::click(),
layout: Layout::default(),
@@ -210,6 +210,55 @@ impl<'a> Popup<'a> {
}
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
Self::new(
Self::default_response_id(response),
response.ctx.clone(),
response,
response.layer_id,
)
}
/// Show a popup relative to some widget,
/// toggling the open state based on the widget's click state.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_toggle_button_response(button_response: &Response) -> Self {
Self::from_response(button_response)
.open_memory(button_response.clicked().then_some(SetOpenCommand::Toggle))
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(button_response: &Response) -> Self {
Self::from_toggle_button_response(button_response)
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Set the kind of the popup. Used for [`Area::kind`] and [`Area::order`].
#[inline]
pub fn kind(mut self, kind: PopupKind) -> Self {
@@ -242,49 +291,6 @@ impl<'a> Popup<'a> {
self
}
/// Show a popup relative to some widget.
/// The popup will be always open.
///
/// See [`Self::menu`] and [`Self::context_menu`] for common use cases.
pub fn from_response(response: &Response) -> Self {
let mut popup = Self::new(
response.id.with("popup"),
response.ctx.clone(),
response,
response.layer_id,
);
popup.widget_clicked_elsewhere = response.clicked_elsewhere();
popup
}
/// Show a popup when the widget was clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
pub fn menu(response: &Response) -> Self {
Self::from_response(response)
.open_memory(response.clicked().then_some(SetOpenCommand::Toggle))
.kind(PopupKind::Menu)
.layout(Layout::top_down_justified(Align::Min))
.style(menu_style)
.gap(0.0)
}
/// Show a context menu when the widget was secondary clicked.
/// Sets the layout to `Layout::top_down_justified(Align::Min)`.
/// In contrast to [`Self::menu`], this will open at the pointer position.
pub fn context_menu(response: &Response) -> Self {
Self::menu(response)
.open_memory(if response.secondary_clicked() {
Some(SetOpenCommand::Bool(true))
} else if response.clicked() {
// Explicitly close the menu if the widget was clicked
// Without this, the context menu would stay open if the user clicks the widget
Some(SetOpenCommand::Bool(false))
} else {
None
})
.at_pointer_fixed()
}
/// Force the popup to be open or closed.
#[inline]
pub fn open(mut self, open: bool) -> Self {
@@ -446,27 +452,28 @@ impl<'a> Popup<'a> {
OpenKind::Open => true,
OpenKind::Closed => false,
OpenKind::Bool(open) => **open,
OpenKind::Memory { .. } => self.ctx.memory(|mem| mem.is_popup_open(self.id)),
OpenKind::Memory { .. } => Self::is_id_open(&self.ctx, self.id),
}
}
/// Get the expected size of the popup.
pub fn get_expected_size(&self) -> Option<Vec2> {
AreaState::load(&self.ctx, self.id).and_then(|area| area.size)
AreaState::load(&self.ctx, self.id)?.size
}
/// Calculate the best alignment for the popup, based on the last size and screen rect.
pub fn get_best_align(&self) -> RectAlign {
let expected_popup_size = self
.get_expected_size()
.unwrap_or(vec2(self.width.unwrap_or(0.0), 0.0));
.unwrap_or_else(|| vec2(self.width.unwrap_or(0.0), 0.0));
let Some(anchor_rect) = self.anchor.rect(self.id, &self.ctx) else {
return self.rect_align;
};
RectAlign::find_best_align(
#[allow(clippy::iter_on_empty_collections)]
#[expect(clippy::iter_on_empty_collections)]
#[expect(clippy::or_fun_call)]
once(self.rect_align).chain(
self.alternative_aligns
// Need the empty slice so the iters have the same type so we can unwrap_or
@@ -479,17 +486,54 @@ impl<'a> Popup<'a> {
.chain(RectAlign::MENU_ALIGNS.iter().copied()),
),
),
self.ctx.screen_rect(),
self.ctx.content_rect(),
anchor_rect,
self.gap,
expected_popup_size,
)
.unwrap_or_default()
}
/// Show the popup.
///
/// Returns `None` if the popup is not open or anchor is `PopupAnchor::Pointer` and there is
/// no pointer.
pub fn show<R>(self, content: impl FnOnce(&mut Ui) -> R) -> Option<InnerResponse<R>> {
let id = self.id;
// When the popup was just opened with a click we don't want to immediately close it based
// on the `PopupCloseBehavior`, so we need to remember if the popup was already open on
// last frame. A convenient way to check this is to see if we have a response for the `Area`
// from last frame:
let was_open_last_frame = self.ctx.read_response(id).is_some();
let hover_pos = self.ctx.pointer_hover_pos();
if let OpenKind::Memory { set } = self.open_kind {
match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
self.ctx.memory_mut(|mem| mem.open_popup_at(id, hover_pos));
}
_ => Popup::open_id(&self.ctx, id),
}
} else {
Self::close_id(&self.ctx, id);
}
}
Some(SetOpenCommand::Toggle) => {
Self::toggle_id(&self.ctx, id);
}
None => {
self.ctx.memory_mut(|mem| mem.keep_popup_open(id));
}
}
}
if !self.open_kind.is_open(self.id, &self.ctx) {
return None;
}
let best_align = self.get_best_align();
let Popup {
@@ -504,7 +548,6 @@ impl<'a> Popup<'a> {
rect_align: _,
alternative_aligns: _,
gap,
widget_clicked_elsewhere,
width,
sense,
layout,
@@ -512,34 +555,6 @@ impl<'a> Popup<'a> {
style,
} = self;
let hover_pos = ctx.pointer_hover_pos();
if let OpenKind::Memory { set, .. } = open_kind {
ctx.memory_mut(|mem| match set {
Some(SetOpenCommand::Bool(open)) => {
if open {
match self.anchor {
PopupAnchor::PointerFixed => {
mem.open_popup_at(id, hover_pos);
}
_ => mem.open_popup(id),
}
} else {
mem.close_popup(id);
}
}
Some(SetOpenCommand::Toggle) => {
mem.toggle_popup(id);
}
None => {
mem.keep_popup_open(id);
}
});
}
if !open_kind.is_open(id, &ctx) {
return None;
}
if kind != PopupKind::Tooltip {
ctx.pass_state_mut(|fs| {
fs.layers
@@ -573,23 +588,28 @@ impl<'a> Popup<'a> {
area = area.default_width(width);
}
let frame = frame.unwrap_or_else(|| Frame::popup(&ctx.style()));
let mut response = area.show(&ctx, |ui| {
style.apply(ui.style_mut());
let frame = frame.unwrap_or_else(|| Frame::popup(ui.style()));
frame.show(ui, content).inner
});
// If the popup was just opened with a click, we don't want to immediately close it again.
let close_click = was_open_last_frame && ctx.input(|i| i.pointer.any_click());
let closed_by_click = match close_behavior {
PopupCloseBehavior::CloseOnClick => widget_clicked_elsewhere,
PopupCloseBehavior::CloseOnClick => close_click,
PopupCloseBehavior::CloseOnClickOutside => {
widget_clicked_elsewhere && response.response.clicked_elsewhere()
close_click && response.response.clicked_elsewhere()
}
PopupCloseBehavior::IgnoreClicks => false,
};
// Mark the menu as shown, so the sub menu open state is not reset
MenuState::mark_shown(&ctx, id);
// If a submenu is open, the CloseBehavior is handled there
let is_any_submenu_open = !MenuState::is_deepest_sub_menu(&response.response.ctx, id);
let is_any_submenu_open = !MenuState::is_deepest_open_sub_menu(&response.response.ctx, id);
let should_close = (!is_any_submenu_open && closed_by_click)
|| ctx.input(|i| i.key_pressed(Key::Escape))
@@ -616,3 +636,65 @@ impl<'a> Popup<'a> {
Some(response)
}
}
/// ## Static methods
impl Popup<'_> {
/// The default ID when constructing a popup from the [`Response`] of e.g. a button.
pub fn default_response_id(response: &Response) -> Id {
response.id.with("popup")
}
/// Is the given popup open?
///
/// This assumes the use of either:
/// * [`Self::open_memory`]
/// * [`Self::from_toggle_button_response`]
/// * [`Self::menu`]
/// * [`Self::context_menu`]
///
/// The popup id should be the same as either you set with [`Self::id`] or the
/// default one from [`Self::default_response_id`].
pub fn is_id_open(ctx: &Context, popup_id: Id) -> bool {
ctx.memory(|mem| mem.is_popup_open(popup_id))
}
/// Is any popup open?
///
/// This assumes the egui memory is being used to track the open state of popups.
pub fn is_any_open(ctx: &Context) -> bool {
ctx.memory(|mem| mem.any_popup_open())
}
/// Open the given popup and close all others.
///
/// If you are NOT using [`Popup::show`], you must
/// also call [`crate::Memory::keep_popup_open`] as long as
/// you're showing the popup.
pub fn open_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.open_popup(popup_id));
}
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
pub fn toggle_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.toggle_popup(popup_id));
}
/// Close all currently open popups.
pub fn close_all(ctx: &Context) {
ctx.memory_mut(|mem| mem.close_all_popups());
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all`] if you want to close any / all currently open popups.
pub fn close_id(ctx: &Context, popup_id: Id) {
ctx.memory_mut(|mem| mem.close_popup(popup_id));
}
/// Get the position for this popup, if it is open.
pub fn position_of_id(ctx: &Context, popup_id: Id) -> Option<Pos2> {
ctx.memory(|mem| mem.popup_position(popup_id))
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
pos2, vec2, Align2, Color32, Context, CursorIcon, Id, NumExt, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
};
use std::fmt::Debug;
@@ -214,14 +214,14 @@ impl Resize {
});
let mut state = State::load(ui.ctx(), id).unwrap_or_else(|| {
ui.ctx().request_repaint(); // counter frame delay
ui.request_repaint(); // counter frame delay
let default_size = self
.default_size
.at_least(self.min_size)
.at_most(self.max_size)
.at_most(
ui.ctx().screen_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
ui.ctx().content_rect().size() - ui.spacing().window_margin.sum(), // hack for windows
)
.round_ui();
@@ -242,14 +242,12 @@ impl Resize {
let corner_id = self.resizable.any().then(|| id.with("__resize_corner"));
if let Some(corner_id) = corner_id {
if let Some(corner_response) = ui.ctx().read_response(corner_id) {
if let Some(pointer_pos) = corner_response.interact_pointer_pos() {
// Respond to the interaction early to avoid frame delay.
user_requested_size =
Some(pointer_pos - position + 0.5 * corner_response.rect.size());
}
}
if let Some(corner_id) = corner_id
&& let Some(corner_response) = ui.ctx().read_response(corner_id)
&& let Some(pointer_pos) = corner_response.interact_pointer_pos()
{
// Respond to the interaction early to avoid frame delay.
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
}
if let Some(user_requested_size) = user_requested_size {
@@ -365,20 +363,20 @@ impl Resize {
paint_resize_corner(ui, &corner_response);
if corner_response.hovered() || corner_response.dragged() {
ui.ctx().set_cursor_icon(CursorIcon::ResizeNwSe);
ui.set_cursor_icon(CursorIcon::ResizeNwSe);
}
}
state.store(ui.ctx(), id);
#[cfg(debug_assertions)]
if ui.ctx().style().debug.show_resize {
ui.ctx().debug_painter().debug_rect(
if ui.global_style().debug.show_resize {
ui.debug_painter().debug_rect(
Rect::from_min_size(content_ui.min_rect().left_top(), state.desired_size),
Color32::GREEN,
"desired_size",
);
ui.ctx().debug_painter().debug_rect(
ui.debug_painter().debug_rect(
Rect::from_min_size(content_ui.min_rect().left_top(), state.last_content_size),
Color32::LIGHT_BLUE,
"last_content_size",

View File

@@ -1,9 +1,10 @@
use core::f32;
use emath::{GuiRounding, Pos2};
use emath::{GuiRounding as _, Pos2};
use crate::{
emath::TSTransform, InnerResponse, LayerId, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
InnerResponse, LayerId, PointerButton, Rangef, Rect, Response, Sense, Ui, UiBuilder, Vec2,
emath::TSTransform,
};
/// Creates a transformation that fits a given scene rectangle into the available screen size.
@@ -44,14 +45,41 @@ fn fit_to_rect_in_scene(
#[must_use = "You should call .show()"]
pub struct Scene {
zoom_range: Rangef,
sense: Sense,
max_inner_size: Vec2,
drag_pan_buttons: DragPanButtons,
}
/// Specifies which pointer buttons can be used to pan the scene by dragging.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct DragPanButtons(u8);
bitflags::bitflags! {
impl DragPanButtons: u8 {
/// [PointerButton::Primary]
const PRIMARY = 1 << 0;
/// [PointerButton::Secondary]
const SECONDARY = 1 << 1;
/// [PointerButton::Middle]
const MIDDLE = 1 << 2;
/// [PointerButton::Extra1]
const EXTRA_1 = 1 << 3;
/// [PointerButton::Extra2]
const EXTRA_2 = 1 << 4;
}
}
impl Default for Scene {
fn default() -> Self {
Self {
zoom_range: Rangef::new(f32::EPSILON, 1.0),
sense: Sense::click_and_drag(),
max_inner_size: Vec2::splat(1000.0),
drag_pan_buttons: DragPanButtons::all(),
}
}
}
@@ -62,6 +90,17 @@ impl Scene {
Default::default()
}
/// Specify what type of input the scene should respond to.
///
/// The default is `Sense::click_and_drag()`.
///
/// Set this to `Sense::hover()` to disable panning via clicking and dragging.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set the allowed zoom range.
///
/// The default zoom range is `0.0..=1.0`,
@@ -82,6 +121,15 @@ impl Scene {
self
}
/// Specify which pointer buttons can be used to pan by clicking and dragging.
///
/// By default, this is `DragPanButtons::all()`.
#[inline]
pub fn drag_pan_buttons(mut self, flags: DragPanButtons) -> Self {
self.drag_pan_buttons = flags;
self
}
/// `scene_rect` contains the view bounds of the inner [`Ui`].
///
/// `scene_rect` will be mutated by any panning/zooming done by the user.
@@ -149,7 +197,7 @@ impl Scene {
UiBuilder::new()
.layer_id(scene_layer_id)
.max_rect(Rect::from_min_size(Pos2::ZERO, self.max_inner_size))
.sense(Sense::click_and_drag()),
.sense(self.sense),
);
let mut pan_response = local_ui.response();
@@ -160,17 +208,17 @@ impl Scene {
// Set a correct global clip rect:
local_ui.set_clip_rect(to_global.inverse() * outer_rect);
// Tell egui to apply the transform on the layer:
local_ui
.ctx()
.set_transform_layer(scene_layer_id, *to_global);
// Add the actual contents to the area:
let ret = add_contents(&mut local_ui);
// This ensures we catch clicks/drags/pans anywhere on the background.
local_ui.force_set_min_rect((to_global.inverse() * outer_rect).round_ui());
// Tell egui to apply the transform on the layer:
local_ui
.ctx()
.set_transform_layer(scene_layer_id, *to_global);
InnerResponse {
response: pan_response,
inner: ret,
@@ -179,43 +227,51 @@ impl Scene {
/// Helper function to handle pan and zoom interactions on a response.
pub fn register_pan_and_zoom(&self, ui: &Ui, resp: &mut Response, to_global: &mut TSTransform) {
if resp.dragged() {
let dragged = self.drag_pan_buttons.iter().any(|button| match button {
DragPanButtons::PRIMARY => resp.dragged_by(PointerButton::Primary),
DragPanButtons::SECONDARY => resp.dragged_by(PointerButton::Secondary),
DragPanButtons::MIDDLE => resp.dragged_by(PointerButton::Middle),
DragPanButtons::EXTRA_1 => resp.dragged_by(PointerButton::Extra1),
DragPanButtons::EXTRA_2 => resp.dragged_by(PointerButton::Extra2),
_ => false,
});
if dragged {
to_global.translation += to_global.scaling * resp.drag_delta();
resp.mark_changed();
}
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos()) {
if resp.contains_pointer() {
let pointer_in_scene = to_global.inverse() * mouse_pos;
let zoom_delta = ui.ctx().input(|i| i.zoom_delta());
let pan_delta = ui.ctx().input(|i| i.smooth_scroll_delta);
if let Some(mouse_pos) = ui.input(|i| i.pointer.latest_pos())
&& resp.contains_pointer()
{
let pointer_in_scene = to_global.inverse() * mouse_pos;
let zoom_delta = ui.input(|i| i.zoom_delta());
let pan_delta = ui.input(|i| i.smooth_scroll_delta());
// Most of the time we can return early. This is also important to
// avoid `ui_from_scene` to change slightly due to floating point errors.
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
return;
}
if zoom_delta != 1.0 {
// Zoom in on pointer, but only if we are not zoomed in or out too far.
let zoom_delta = zoom_delta.clamp(
self.zoom_range.min / to_global.scaling,
self.zoom_range.max / to_global.scaling,
);
*to_global = *to_global
* TSTransform::from_translation(pointer_in_scene.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
// Clamp to exact zoom range.
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
}
// Pan:
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
resp.mark_changed();
// Most of the time we can return early. This is also important to
// avoid `ui_from_scene` to change slightly due to floating point errors.
if zoom_delta == 1.0 && pan_delta == Vec2::ZERO {
return;
}
if zoom_delta != 1.0 {
// Zoom in on pointer, but only if we are not zoomed in or out too far.
let zoom_delta = zoom_delta.clamp(
self.zoom_range.min / to_global.scaling,
self.zoom_range.max / to_global.scaling,
);
*to_global = *to_global
* TSTransform::from_translation(pointer_in_scene.to_vec2())
* TSTransform::from_scaling(zoom_delta)
* TSTransform::from_translation(-pointer_in_scene.to_vec2());
// Clamp to exact zoom range.
to_global.scaling = self.zoom_range.clamp(to_global.scaling);
}
// Pan:
*to_global = TSTransform::from_translation(pan_delta) * *to_global;
resp.mark_changed();
}
}
}

View File

@@ -1,8 +1,16 @@
#![allow(clippy::needless_range_loop)]
//! See [`ScrollArea`] for docs.
#![expect(clippy::needless_range_loop)]
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use emath::GuiRounding as _;
use epaint::Margin;
use crate::{
emath, epaint, lerp, pass_state, pos2, remap, remap_clamp, Context, Id, NumExt, Pos2, Rangef,
Rect, Sense, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b,
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
UiKind, UiStackInfo, Vec2, Vec2b, WidgetInfo, emath, epaint, lerp, pass_state, pos2, remap,
remap_clamp,
};
#[derive(Clone, Copy, Debug)]
@@ -133,6 +141,113 @@ impl ScrollBarVisibility {
];
}
/// What is the source of scrolling for a [`ScrollArea`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ScrollSource {
/// Scroll the area by dragging a scroll bar.
///
/// By default the scroll bars remain visible to show current position.
/// To hide them use [`ScrollArea::scroll_bar_visibility()`].
pub scroll_bar: bool,
/// Scroll the area by dragging the contents.
pub drag: bool,
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
/// the mouse cursor over the [`ScrollArea`].
pub mouse_wheel: bool,
}
impl Default for ScrollSource {
fn default() -> Self {
Self::ALL
}
}
impl ScrollSource {
pub const NONE: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: false,
};
pub const ALL: Self = Self {
scroll_bar: true,
drag: true,
mouse_wheel: true,
};
pub const SCROLL_BAR: Self = Self {
scroll_bar: true,
drag: false,
mouse_wheel: false,
};
pub const DRAG: Self = Self {
scroll_bar: false,
drag: true,
mouse_wheel: false,
};
pub const MOUSE_WHEEL: Self = Self {
scroll_bar: false,
drag: false,
mouse_wheel: true,
};
/// Is everything disabled?
#[inline]
pub fn is_none(&self) -> bool {
self == &Self::NONE
}
/// Is anything enabled?
#[inline]
pub fn any(&self) -> bool {
self.scroll_bar | self.drag | self.mouse_wheel
}
/// Is everything enabled?
#[inline]
pub fn is_all(&self) -> bool {
self.scroll_bar & self.drag & self.mouse_wheel
}
}
impl BitOr for ScrollSource {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
Self {
scroll_bar: self.scroll_bar | rhs.scroll_bar,
drag: self.drag | rhs.drag,
mouse_wheel: self.mouse_wheel | rhs.mouse_wheel,
}
}
}
#[expect(clippy::suspicious_arithmetic_impl)]
impl Add for ScrollSource {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
self | rhs
}
}
impl BitOrAssign for ScrollSource {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = *self | rhs;
}
}
impl AddAssign for ScrollSource {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}
/// Add vertical and/or horizontal scrolling to a contained [`Ui`].
///
/// By default, scroll bars only show up when needed, i.e. when the contents
@@ -147,7 +262,7 @@ impl ScrollBarVisibility {
/// ### Coordinate system
/// * content: size of contents (generally large; that's why we want scroll bars)
/// * outer: size of scroll area including scroll bar(s)
/// * inner: excluding scroll bar(s). The area we clip the contents to.
/// * inner: excluding scroll bar(s). The area we clip the contents to. Includes `content_margin`.
///
/// If the floating scroll bars settings is turned on then `inner == outer`.
///
@@ -168,7 +283,7 @@ impl ScrollBarVisibility {
#[must_use = "You should call .show()"]
pub struct ScrollArea {
/// Do we have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
auto_shrink: Vec2b,
max_size: Vec2,
@@ -178,10 +293,12 @@ pub struct ScrollArea {
id_salt: Option<Id>,
offset_x: Option<f32>,
offset_y: Option<f32>,
on_hover_cursor: Option<CursorIcon>,
on_drag_cursor: Option<CursorIcon>,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
/// If false, we ignore scroll events.
scrolling_enabled: bool,
drag_to_scroll: bool,
content_margin: Option<Margin>,
/// If true for vertical or horizontal the scroll wheel will stick to the
/// end position until user manually changes position. It will become true
@@ -220,9 +337,9 @@ impl ScrollArea {
/// Create a scroll area where you decide which axis has scrolling enabled.
/// For instance, `ScrollArea::new([true, false])` enables horizontal scrolling.
pub fn new(scroll_enabled: impl Into<Vec2b>) -> Self {
pub fn new(direction_enabled: impl Into<Vec2b>) -> Self {
Self {
scroll_enabled: scroll_enabled.into(),
direction_enabled: direction_enabled.into(),
auto_shrink: Vec2b::TRUE,
max_size: Vec2::INFINITY,
min_scrolled_size: Vec2::splat(64.0),
@@ -231,8 +348,11 @@ impl ScrollArea {
id_salt: None,
offset_x: None,
offset_y: None,
scrolling_enabled: true,
drag_to_scroll: true,
on_hover_cursor: None,
on_drag_cursor: None,
scroll_source: ScrollSource::default(),
wheel_scroll_multiplier: Vec2::splat(1.0),
content_margin: None,
stick_to_end: Vec2b::FALSE,
animated: true,
}
@@ -355,17 +475,41 @@ impl ScrollArea {
self
}
/// Set the cursor used when the mouse pointer is hovering over the [`ScrollArea`].
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_hover_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_hover_cursor = Some(cursor);
self
}
/// Set the cursor used when the [`ScrollArea`] is being dragged.
///
/// Only applies if [`Self::scroll_source()`] has set [`ScrollSource::drag`] to `true`.
///
/// Any changes to the mouse cursor made within the contents of the [`ScrollArea`] will
/// override this setting.
#[inline]
pub fn on_drag_cursor(mut self, cursor: CursorIcon) -> Self {
self.on_drag_cursor = Some(cursor);
self
}
/// Turn on/off scrolling on the horizontal axis.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
self.scroll_enabled[0] = hscroll;
self.direction_enabled[0] = hscroll;
self
}
/// Turn on/off scrolling on the vertical axis.
#[inline]
pub fn vscroll(mut self, vscroll: bool) -> Self {
self.scroll_enabled[1] = vscroll;
self.direction_enabled[1] = vscroll;
self
}
@@ -373,16 +517,8 @@ impl ScrollArea {
///
/// You can pass in `false`, `true`, `[false, true]` etc.
#[inline]
pub fn scroll(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
self
}
/// Turn on/off scrolling on the horizontal/vertical axes.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll_enabled: impl Into<Vec2b>) -> Self {
self.scroll_enabled = scroll_enabled.into();
pub fn scroll(mut self, direction_enabled: impl Into<Vec2b>) -> Self {
self.direction_enabled = direction_enabled.into();
self
}
@@ -395,9 +531,14 @@ impl ScrollArea {
/// is typing text in a [`crate::TextEdit`] widget contained within the scroll area.
///
/// This controls both scrolling directions.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn enable_scrolling(mut self, enable: bool) -> Self {
self.scrolling_enabled = enable;
self.scroll_source = if enable {
ScrollSource::ALL
} else {
ScrollSource::NONE
};
self
}
@@ -408,9 +549,28 @@ impl ScrollArea {
/// If `true`, the [`ScrollArea`] will sense drags.
///
/// Default: `true`.
#[deprecated = "Use `ScrollArea::scroll_source()"]
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.drag_to_scroll = drag_to_scroll;
self.scroll_source.drag = drag_to_scroll;
self
}
/// What sources does the [`ScrollArea`] use for scrolling the contents.
#[inline]
pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self {
self.scroll_source = scroll_source;
self
}
/// The scroll amount caused by a mouse wheel scroll is multiplied by this amount.
///
/// Independent for each scroll direction. Defaults to `Vec2{x: 1.0, y: 1.0}`.
///
/// This can invert or effectively disable mouse scrolling.
#[inline]
pub fn wheel_scroll_multiplier(mut self, multiplier: Vec2) -> Self {
self.wheel_scroll_multiplier = multiplier;
self
}
@@ -437,7 +597,19 @@ impl ScrollArea {
/// Is any scrolling enabled?
pub(crate) fn is_any_scroll_enabled(&self) -> bool {
self.scroll_enabled[0] || self.scroll_enabled[1]
self.direction_enabled[0] || self.direction_enabled[1]
}
/// Extra margin added around the contents.
///
/// The scroll bars will be either on top of this margin, or outside of it,
/// depending on the value of [`crate::style::ScrollStyle::floating`].
///
/// Default: [`crate::style::ScrollStyle::content_margin`].
#[inline]
pub fn content_margin(mut self, margin: impl Into<Margin>) -> Self {
self.content_margin = Some(margin.into());
self
}
/// The scroll handle will stick to the rightmost position even while the content size
@@ -472,7 +644,7 @@ struct Prepared {
auto_shrink: Vec2b,
/// Does this `ScrollArea` have horizontal/vertical scrolling enabled?
scroll_enabled: Vec2b,
direction_enabled: Vec2b,
/// Smoothly interpolated boolean of whether or not to show the scroll bars.
show_bars_factor: Vec2,
@@ -491,7 +663,7 @@ struct Prepared {
scroll_bar_visibility: ScrollBarVisibility,
scroll_bar_rect: Option<Rect>,
/// Where on the screen the content is (excludes scroll bars).
/// Where on the screen the content is (excludes scroll bars; includes `content_margin`).
inner_rect: Rect,
content_ui: Ui,
@@ -500,20 +672,24 @@ struct Prepared {
/// `viewport.min == ZERO` means we scrolled to the top.
viewport: Rect,
scrolling_enabled: bool,
scroll_source: ScrollSource,
wheel_scroll_multiplier: Vec2,
stick_to_end: Vec2b,
/// If there was a scroll target before the [`ScrollArea`] was added this frame, it's
/// not for us to handle so we save it and restore it after this [`ScrollArea`] is done.
saved_scroll_target: [Option<pass_state::ScrollTarget>; 2],
/// The response from dragging the background (if enabled)
background_drag_response: Option<Response>,
animated: bool,
}
impl ScrollArea {
fn begin(self, ui: &mut Ui) -> Prepared {
let Self {
scroll_enabled,
direction_enabled,
auto_shrink,
max_size,
min_scrolled_size,
@@ -522,14 +698,16 @@ impl ScrollArea {
id_salt,
offset_x,
offset_y,
scrolling_enabled,
drag_to_scroll,
on_hover_cursor,
on_drag_cursor,
scroll_source,
wheel_scroll_multiplier,
content_margin: _, // Used elsewhere
stick_to_end,
animated,
} = self;
let ctx = ui.ctx().clone();
let scrolling_enabled = scrolling_enabled && ui.is_enabled();
let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
let id = ui.make_persistent_id(id_salt);
@@ -546,7 +724,7 @@ impl ScrollArea {
let show_bars: Vec2b = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => state.show_scroll,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
let show_bars_factor = Vec2::new(
@@ -568,7 +746,7 @@ impl ScrollArea {
// one shouldn't collapse into nothingness.
// See https://github.com/emilk/egui/issues/1097
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
inner_size[d] = inner_size[d].max(min_scrolled_size[d]);
}
}
@@ -585,13 +763,19 @@ impl ScrollArea {
} else {
// Tell the inner Ui to use as much space as possible, we can scroll to see it!
for d in 0..2 {
if scroll_enabled[d] {
if direction_enabled[d] {
content_max_size[d] = f32::INFINITY;
}
}
}
let content_max_rect = Rect::from_min_size(inner_rect.min - state.offset, content_max_size);
// Round to pixels to avoid widgets appearing to "float" when scrolling fractional amounts:
let content_max_rect = content_max_rect
.round_to_pixels(ui.pixels_per_point())
.round_ui();
let mut content_ui = ui.new_child(
UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::ScrollArea))
@@ -603,7 +787,7 @@ impl ScrollArea {
let clip_rect_margin = ui.visuals().clip_rect_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if scroll_enabled[d] {
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;
} else {
@@ -619,56 +803,72 @@ impl ScrollArea {
let viewport = Rect::from_min_size(Pos2::ZERO + state.offset, inner_size);
let dt = ui.input(|i| i.stable_dt).at_most(0.1);
if (scrolling_enabled && drag_to_scroll)
&& (state.content_is_too_large[0] || state.content_is_too_large[1])
{
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
let content_response_option = state
.interact_rect
.map(|rect| ui.interact(rect, id.with("area"), Sense::drag()));
let background_drag_response =
if scroll_source.drag && ui.is_enabled() && state.content_is_too_large.any() {
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
let content_response_option = state
.interact_rect
.map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG));
if content_response_option
.as_ref()
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if scroll_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option
.as_ref()
.is_some_and(|response| response.drag_stopped())
.is_some_and(|response| response.dragged())
{
state.vel =
scroll_enabled.to_vec2() * ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
for d in 0..2 {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option
.as_ref()
.is_some_and(|response| response.drag_stopped())
{
state.vel = direction_enabled.to_vec2()
* ui.input(|input| input.pointer.velocity());
}
for d in 0..2 {
// Kinetic scrolling
let stop_speed = 20.0; // Pixels per second.
let friction_coeff = 1000.0; // Pixels per second squared.
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
let friction = friction_coeff * dt;
if friction > state.vel[d].abs() || state.vel[d].abs() < stop_speed {
state.vel[d] = 0.0;
} else {
state.vel[d] -= friction * state.vel[d].signum();
// Offset has an inverted coordinate system compared to
// the velocity, so we subtract it instead of adding it
state.offset[d] -= state.vel[d] * dt;
ctx.request_repaint();
}
}
}
}
}
// Set the desired mouse cursors.
if let Some(response) = &content_response_option {
if response.dragged()
&& let Some(cursor) = on_drag_cursor
{
ui.set_cursor_icon(cursor);
} else if response.hovered()
&& let Some(cursor) = on_hover_cursor
{
ui.set_cursor_icon(cursor);
}
}
content_response_option
} else {
None
};
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
// above).
@@ -709,7 +909,7 @@ impl ScrollArea {
id,
state,
auto_shrink,
scroll_enabled,
direction_enabled,
show_bars_factor,
current_bar_use,
scroll_bar_visibility,
@@ -717,9 +917,11 @@ impl ScrollArea {
inner_rect,
content_ui,
viewport,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
background_drag_response,
animated,
}
}
@@ -801,10 +1003,21 @@ impl ScrollArea {
ui: &mut Ui,
add_contents: Box<dyn FnOnce(&mut Ui, Rect) -> R + 'c>,
) -> ScrollAreaOutput<R> {
let margin = self
.content_margin
.unwrap_or_else(|| ui.spacing().scroll.content_margin);
let mut prepared = self.begin(ui);
let id = prepared.id;
let inner_rect = prepared.inner_rect;
let inner = add_contents(&mut prepared.content_ui, prepared.viewport);
let inner = crate::Frame::NONE
.inner_margin(margin)
.show(&mut prepared.content_ui, |ui| {
add_contents(ui, prepared.viewport)
})
.inner;
let (content_size, state) = prepared.end(ui);
ScrollAreaOutput {
inner,
@@ -824,16 +1037,18 @@ impl Prepared {
mut state,
inner_rect,
auto_shrink,
scroll_enabled,
direction_enabled,
mut show_bars_factor,
current_bar_use,
scroll_bar_visibility,
scroll_bar_rect,
content_ui,
viewport: _,
scrolling_enabled,
scroll_source,
wheel_scroll_multiplier,
stick_to_end,
saved_scroll_target,
background_drag_response,
animated,
} = self;
@@ -854,7 +1069,7 @@ impl Prepared {
.ctx()
.pass_state_mut(|state| state.scroll_target[d].take());
if scroll_enabled[d] {
if direction_enabled[d] {
if let Some(target) = scroll_target {
let pass_state::ScrollTarget {
range,
@@ -890,7 +1105,7 @@ impl Prepared {
delta += delta_update;
animation = animation_update;
};
}
if delta != 0.0 {
let target_offset = state.offset[d] + delta;
@@ -911,7 +1126,7 @@ impl Prepared {
target_offset,
});
}
ui.ctx().request_repaint();
ui.request_repaint();
}
}
}
@@ -921,7 +1136,7 @@ impl Prepared {
for d in 0..2 {
if saved_scroll_target[d].is_some() {
state.scroll_target[d] = saved_scroll_target[d].clone();
};
}
}
});
@@ -930,7 +1145,7 @@ impl Prepared {
let mut inner_size = inner_rect.size();
for d in 0..2 {
inner_size[d] = match (scroll_enabled[d], auto_shrink[d]) {
inner_size[d] = match (direction_enabled[d], auto_shrink[d]) {
(true, true) => inner_size[d].min(content_size[d]), // shrink scroll area if content is small
(true, false) => inner_size[d], // let scroll area be larger than content; fill with blank space
(false, true) => content_size[d], // Follow the content (expand/contract to fit it).
@@ -944,25 +1159,35 @@ impl Prepared {
let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use);
let content_is_too_large = Vec2b::new(
scroll_enabled[0] && inner_rect.width() < content_size.x,
scroll_enabled[1] && inner_rect.height() < content_size.y,
direction_enabled[0] && inner_rect.width() < content_size.x,
direction_enabled[1] && inner_rect.height() < content_size.y,
);
let max_offset = content_size - inner_rect.size();
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect);
if scrolling_enabled && is_hovering_outer_rect {
// Drag-to-scroll?
let is_dragging_background = background_drag_response
.as_ref()
.is_some_and(|r| r.dragged());
let is_hovering_outer_rect = ui.rect_contains_pointer(outer_rect)
&& ui.ctx().dragged_id().is_none()
|| is_dragging_background;
if scroll_source.mouse_wheel && ui.is_enabled() && is_hovering_outer_rect {
let always_scroll_enabled_direction = ui.style().always_scroll_the_only_direction
&& scroll_enabled[0] != scroll_enabled[1];
&& direction_enabled[0] != direction_enabled[1];
for d in 0..2 {
if scroll_enabled[d] {
let scroll_delta = ui.ctx().input_mut(|input| {
if direction_enabled[d] {
let scroll_delta = ui.input(|input| {
if always_scroll_enabled_direction {
// no bidirectional scrolling; allow horizontal scrolling without pressing shift
input.smooth_scroll_delta[0] + input.smooth_scroll_delta[1]
input.smooth_scroll_delta()[0] + input.smooth_scroll_delta()[1]
} else {
input.smooth_scroll_delta[d]
input.smooth_scroll_delta()[d]
}
});
let scroll_delta = scroll_delta * wheel_scroll_multiplier[d];
let scrolling_up = state.offset[d] > 0.0 && scroll_delta > 0.0;
let scrolling_down = state.offset[d] < max_offset[d] && scroll_delta < 0.0;
@@ -971,10 +1196,9 @@ impl Prepared {
state.offset[d] -= scroll_delta;
// Clear scroll delta so no parent scroll will use it:
ui.ctx().input_mut(|input| {
ui.input_mut(|input| {
if always_scroll_enabled_direction {
input.smooth_scroll_delta[0] = 0.0;
input.smooth_scroll_delta[1] = 0.0;
input.smooth_scroll_delta = Vec2::ZERO;
} else {
input.smooth_scroll_delta[d] = 0.0;
}
@@ -990,7 +1214,7 @@ impl Prepared {
let show_scroll_this_frame = match scroll_bar_visibility {
ScrollBarVisibility::AlwaysHidden => Vec2b::FALSE,
ScrollBarVisibility::VisibleWhenNeeded => content_is_too_large,
ScrollBarVisibility::AlwaysVisible => scroll_enabled,
ScrollBarVisibility::AlwaysVisible => direction_enabled,
};
// Avoid frame delay; start showing scroll bar right away:
@@ -1017,45 +1241,17 @@ impl Prepared {
continue;
}
let interact_id = id.with(d);
// Margin on either side of the scroll bar:
let inner_margin = show_factor * scroll_style.bar_inner_margin;
let outer_margin = show_factor * scroll_style.bar_outer_margin;
// top/bottom of a horizontal scroll (d==0).
// left/rigth of a vertical scroll (d==1).
let mut cross = if scroll_style.floating {
// The bounding rect of a fully visible bar.
// When we hover this area, we should show the full bar:
let max_bar_rect = if d == 0 {
outer_rect.with_min_y(outer_rect.max.y - outer_margin - scroll_style.bar_width)
} else {
outer_rect.with_min_x(outer_rect.max.x - outer_margin - scroll_style.bar_width)
};
// bottom of a horizontal scroll (d==0).
// right of a vertical scroll (d==1).
let mut max_cross = outer_rect.max[1 - d] - outer_margin;
let is_hovering_bar_area = is_hovering_outer_rect
&& ui.rect_contains_pointer(max_bar_rect)
|| state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui
.ctx()
.animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
let width = show_factor
* lerp(
scroll_style.floating_width..=scroll_style.bar_width,
is_hovering_bar_area_t,
);
let max_cross = outer_rect.max[1 - d] - outer_margin;
let min_cross = max_cross - width;
Rangef::new(min_cross, max_cross)
} else {
let min_cross = inner_rect.max[1 - d] + inner_margin;
let max_cross = outer_rect.max[1 - d] - outer_margin;
Rangef::new(min_cross, max_cross)
};
if ui.clip_rect().max[1 - d] < cross.max + outer_margin {
if ui.clip_rect().max[1 - d] - outer_margin < max_cross {
// Move the scrollbar so it is visible. This is needed in some cases.
// For instance:
// * When we have a vertical-only scroll area in a top level panel,
@@ -1065,21 +1261,59 @@ impl Prepared {
// is outside the clip rectangle.
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
let width = cross.max - cross.min;
cross.max = ui.clip_rect().max[1 - d] - outer_margin;
cross.min = cross.max - width;
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
}
let outer_scroll_bar_rect = if d == 0 {
Rect::from_min_max(
pos2(scroll_bar_rect.left(), cross.min),
pos2(scroll_bar_rect.right(), cross.max),
)
let full_width = scroll_style.bar_width;
// The bounding rect of a fully visible bar.
// When we hover this area, we should show the full bar:
let max_bar_rect = if d == 0 {
outer_rect.with_min_y(max_cross - full_width)
} else {
Rect::from_min_max(
pos2(cross.min, scroll_bar_rect.top()),
pos2(cross.max, scroll_bar_rect.bottom()),
)
outer_rect.with_min_x(max_cross - full_width)
};
let sense = if scroll_source.scroll_bar && ui.is_enabled() {
Sense::CLICK | Sense::DRAG
} else {
Sense::hover()
};
// We always sense interaction with the full width, even if we antimate it growing/shrinking.
// This is to present a more consistent target for our hit test code,
// and to avoid producing jitter in "thin widget" heuristics there.
// Also: it make sense to detect any hover where the scroll bar _will_ be.
let response = ui.interact(max_bar_rect, interact_id, sense);
response.widget_info(|| WidgetInfo::new(crate::WidgetType::ScrollBar));
// top/bottom of a horizontal scroll (d==0).
// left/rigth of a vertical scroll (d==1).
let cross = if scroll_style.floating {
let is_hovering_bar_area = response.hovered() || state.scroll_bar_interaction[d];
let is_hovering_bar_area_t = ui
.ctx()
.animate_bool_responsive(id.with((d, "bar_hover")), is_hovering_bar_area);
let width = show_factor
* lerp(
scroll_style.floating_width..=full_width,
is_hovering_bar_area_t,
);
let min_cross = max_cross - width;
Rangef::new(min_cross, max_cross)
} else {
let min_cross = inner_rect.max[1 - d] + inner_margin;
Rangef::new(min_cross, max_cross)
};
let outer_scroll_bar_rect = if d == 0 {
Rect::from_x_y_ranges(scroll_bar_rect.x_range(), cross)
} else {
Rect::from_x_y_ranges(cross, scroll_bar_rect.y_range())
};
let from_content = |content| {
@@ -1119,14 +1353,6 @@ impl Prepared {
let handle_rect = calculate_handle_rect(d, &state.offset);
let interact_id = id.with(d);
let sense = if self.scrolling_enabled {
Sense::click_and_drag()
} else {
Sense::hover()
};
let response = ui.interact(outer_scroll_bar_rect, interact_id, sense);
state.scroll_bar_interaction[d] = response.hovered() || response.dragged();
if let Some(pointer_pos) = response.interact_pointer_pos() {
@@ -1145,11 +1371,13 @@ impl Prepared {
});
let new_handle_top = pointer_pos[d] - *scroll_start_offset_from_top_left;
state.offset[d] = remap(
new_handle_top,
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]),
0.0..=max_offset[d],
);
let handle_travel =
scroll_bar_rect.min[d]..=(scroll_bar_rect.max[d] - handle_rect.size()[d]);
state.offset[d] = if handle_travel.start() == handle_travel.end() {
0.0
} else {
remap(new_handle_top, handle_travel, 0.0..=max_offset[d])
};
// some manual action taken, scroll not stuck
state.scroll_stuck_to_end[d] = false;
@@ -1170,7 +1398,7 @@ impl Prepared {
// Avoid frame-delay by calculating a new handle rect:
let handle_rect = calculate_handle_rect(d, &state.offset);
let visuals = if scrolling_enabled {
let visuals = if scroll_source.scroll_bar && ui.is_enabled() {
// Pick visuals based on interaction with the handle.
// Remember that the response is for the whole scroll bar!
let is_hovering_handle = response.hovered()
@@ -1248,7 +1476,7 @@ impl Prepared {
ui.advance_cursor_after_rect(outer_rect);
if show_scroll_this_frame != state.show_scroll {
ui.ctx().request_repaint();
ui.request_repaint();
}
let available_offset = content_size - inner_rect.size();

View File

@@ -1,4 +1,4 @@
use emath::Align;
use emath::{Align, NumExt as _};
use crate::{Layout, Ui, UiBuilder};
@@ -20,8 +20,13 @@ use crate::{Layout, Ui, UiBuilder};
///
/// If the parent is not wide enough to fit all widgets, the parent will be expanded to the right.
///
/// The left widgets are first added to the ui, left-to-right.
/// Then the right widgets are added, right-to-left.
/// The left widgets are added left-to-right.
/// The right widgets are added right-to-left.
///
/// Which side is first depends on the configuration:
/// - [`Sides::extend`] - left widgets are added first
/// - [`Sides::shrink_left`] - right widgets are added first
/// - [`Sides::shrink_right`] - left widgets are added first
///
/// ```
/// # egui::__run_test_ui(|ui| {
@@ -40,6 +45,16 @@ use crate::{Layout, Ui, UiBuilder};
pub struct Sides {
height: Option<f32>,
spacing: Option<f32>,
kind: SidesKind,
wrap_mode: Option<crate::TextWrapMode>,
}
#[derive(Clone, Copy, Debug, Default)]
enum SidesKind {
#[default]
Extend,
ShrinkLeft,
ShrinkRight,
}
impl Sides {
@@ -68,58 +83,175 @@ impl Sides {
self
}
/// Try to shrink widgets on the left side.
///
/// Right widgets will be added first. The left [`Ui`]s max rect will be limited to the
/// remaining space.
#[inline]
pub fn shrink_left(mut self) -> Self {
self.kind = SidesKind::ShrinkLeft;
self
}
/// Try to shrink widgets on the right side.
///
/// Left widgets will be added first. The right [`Ui`]s max rect will be limited to the
/// remaining space.
#[inline]
pub fn shrink_right(mut self) -> Self {
self.kind = SidesKind::ShrinkRight;
self
}
/// Extend the left and right sides to fill the available space.
///
/// This is the default behavior.
/// The left widgets will be added first, followed by the right widgets.
#[inline]
pub fn extend(mut self) -> Self {
self.kind = SidesKind::Extend;
self
}
/// The text wrap mode for the shrinking side.
///
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn wrap_mode(mut self, wrap_mode: crate::TextWrapMode) -> Self {
self.wrap_mode = Some(wrap_mode);
self
}
/// Truncate the text on the shrinking side.
///
/// This is a shortcut for [`Self::wrap_mode`].
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn truncate(mut self) -> Self {
self.wrap_mode = Some(crate::TextWrapMode::Truncate);
self
}
/// Wrap the text on the shrinking side.
///
/// This is a shortcut for [`Self::wrap_mode`].
/// Does nothing if [`Self::extend`] is used (the default).
#[inline]
pub fn wrap(mut self) -> Self {
self.wrap_mode = Some(crate::TextWrapMode::Wrap);
self
}
pub fn show<RetL, RetR>(
self,
ui: &mut Ui,
add_left: impl FnOnce(&mut Ui) -> RetL,
add_right: impl FnOnce(&mut Ui) -> RetR,
) -> (RetL, RetR) {
let Self { height, spacing } = self;
let Self {
height,
spacing,
mut kind,
mut wrap_mode,
} = self;
let height = height.unwrap_or_else(|| ui.spacing().interact_size.y);
let spacing = spacing.unwrap_or_else(|| ui.spacing().item_spacing.x);
let mut top_rect = ui.available_rect_before_wrap();
top_rect.max.y = top_rect.min.y + height;
let result_left;
let result_right;
let left_rect = {
let left_max_rect = top_rect;
let mut left_ui = ui.new_child(
UiBuilder::new()
.max_rect(left_max_rect)
.layout(Layout::left_to_right(Align::Center)),
);
result_left = add_left(&mut left_ui);
left_ui.min_rect()
};
let right_rect = {
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
let mut right_ui = ui.new_child(
UiBuilder::new()
.max_rect(right_max_rect)
.layout(Layout::right_to_left(Align::Center)),
);
result_right = add_right(&mut right_ui);
right_ui.min_rect()
};
let mut final_rect = left_rect.union(right_rect);
let min_width = left_rect.width() + spacing + right_rect.width();
if ui.is_sizing_pass() {
// Make as small as possible:
final_rect.max.x = left_rect.min.x + min_width;
} else {
// If the rects overlap, make sure we expand the allocated rect so that the parent
// ui knows we overflowed, and resizes:
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
kind = SidesKind::Extend;
wrap_mode = None;
}
ui.advance_cursor_after_rect(final_rect);
match kind {
SidesKind::ShrinkLeft => {
let (right_rect, result_right) = Self::create_ui(
ui,
top_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
let available_width = top_rect.width() - right_rect.width() - spacing;
let left_rect_constraint =
top_rect.with_max_x(top_rect.min.x + available_width.at_least(0.0));
let (left_rect, result_left) = Self::create_ui(
ui,
left_rect_constraint,
Layout::left_to_right(Align::Center),
add_left,
wrap_mode,
);
(result_left, result_right)
ui.advance_cursor_after_rect(left_rect | right_rect);
(result_left, result_right)
}
SidesKind::ShrinkRight => {
let (left_rect, result_left) = Self::create_ui(
ui,
top_rect,
Layout::left_to_right(Align::Center),
add_left,
None,
);
let right_rect_constraint = top_rect.with_min_x(left_rect.max.x + spacing);
let (right_rect, result_right) = Self::create_ui(
ui,
right_rect_constraint,
Layout::right_to_left(Align::Center),
add_right,
wrap_mode,
);
ui.advance_cursor_after_rect(left_rect | right_rect);
(result_left, result_right)
}
SidesKind::Extend => {
let (left_rect, result_left) = Self::create_ui(
ui,
top_rect,
Layout::left_to_right(Align::Center),
add_left,
None,
);
let right_max_rect = top_rect.with_min_x(left_rect.max.x);
let (right_rect, result_right) = Self::create_ui(
ui,
right_max_rect,
Layout::right_to_left(Align::Center),
add_right,
None,
);
let mut final_rect = left_rect | right_rect;
let min_width = left_rect.width() + spacing + right_rect.width();
if ui.is_sizing_pass() {
final_rect.max.x = left_rect.min.x + min_width;
} else {
final_rect.max.x = final_rect.max.x.max(left_rect.min.x + min_width);
}
ui.advance_cursor_after_rect(final_rect);
(result_left, result_right)
}
}
}
fn create_ui<Ret>(
ui: &mut Ui,
max_rect: emath::Rect,
layout: Layout,
add_content: impl FnOnce(&mut Ui) -> Ret,
wrap_mode: Option<crate::TextWrapMode>,
) -> (emath::Rect, Ret) {
let mut child_ui = ui.new_child(UiBuilder::new().max_rect(max_rect).layout(layout));
if let Some(wrap_mode) = wrap_mode {
child_ui.style_mut().wrap_mode = Some(wrap_mode);
}
let result = add_content(&mut child_ui);
(child_ui.min_rect(), result)
}
}

View File

@@ -7,26 +7,49 @@ use emath::Vec2;
pub struct Tooltip<'a> {
pub popup: Popup<'a>,
layer_id: LayerId,
widget_id: Id,
/// The layer of the parent widget.
parent_layer: LayerId,
/// The id of the widget that owns this tooltip.
parent_widget: Id,
}
impl Tooltip<'_> {
/// Show a tooltip that is always open
/// Show a tooltip that is always open.
#[deprecated = "Use `Tooltip::always_open` instead."]
pub fn new(
widget_id: Id,
parent_widget: Id,
ctx: Context,
anchor: impl Into<PopupAnchor>,
layer_id: LayerId,
parent_layer: LayerId,
) -> Self {
Self {
// TODO(lucasmerlin): Set width somehow (we're missing context here)
popup: Popup::new(widget_id, ctx, anchor.into(), layer_id)
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.sense(Sense::hover()),
layer_id,
widget_id,
parent_layer,
parent_widget,
}
}
/// Show a tooltip that is always open.
pub fn always_open(
ctx: Context,
parent_layer: LayerId,
parent_widget: Id,
anchor: impl Into<PopupAnchor>,
) -> Self {
let width = ctx.global_style().spacing.tooltip_width;
Self {
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.width(width)
.sense(Sense::hover()),
parent_layer,
parent_widget,
}
}
@@ -35,12 +58,12 @@ impl Tooltip<'_> {
let popup = Popup::from_response(response)
.kind(PopupKind::Tooltip)
.gap(4.0)
.width(response.ctx.style().spacing.tooltip_width)
.width(response.ctx.global_style().spacing.tooltip_width)
.sense(Sense::hover());
Self {
popup,
layer_id: response.layer_id,
widget_id: response.id,
parent_layer: response.layer_id,
parent_widget: response.id,
}
}
@@ -49,7 +72,7 @@ impl Tooltip<'_> {
let mut tooltip = Self::for_widget(response);
tooltip.popup = tooltip
.popup
.open(response.enabled() && Self::should_show_tooltip(response));
.open(response.enabled() && Self::should_show_tooltip(response, true));
tooltip
}
@@ -58,7 +81,7 @@ impl Tooltip<'_> {
let mut tooltip = Self::for_widget(response);
tooltip.popup = tooltip
.popup
.open(!response.enabled() && Self::should_show_tooltip(response));
.open(!response.enabled() && Self::should_show_tooltip(response, true));
tooltip
}
@@ -96,8 +119,8 @@ impl Tooltip<'_> {
pub fn show<R>(self, content: impl FnOnce(&mut crate::Ui) -> R) -> Option<InnerResponse<R>> {
let Self {
mut popup,
layer_id: parent_layer,
widget_id,
parent_layer,
parent_widget,
} = self;
if !popup.is_open() {
@@ -111,11 +134,11 @@ impl Tooltip<'_> {
fs.layers
.entry(parent_layer)
.or_default()
.widget_with_tooltip = Some(widget_id);
.widget_with_tooltip = Some(parent_widget);
fs.tooltips
.widget_tooltips
.get(&widget_id)
.get(&parent_widget)
.copied()
.unwrap_or(PerWidgetTooltipState {
bounding_rect: rect,
@@ -123,7 +146,7 @@ impl Tooltip<'_> {
})
});
let tooltip_area_id = Self::tooltip_id(widget_id, state.tooltip_count);
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
let response = popup.show(|ui| {
@@ -140,11 +163,11 @@ impl Tooltip<'_> {
// The popup might not be shown on at_pointer if there is no pointer.
if let Some(response) = &response {
state.tooltip_count += 1;
state.bounding_rect = state.bounding_rect.union(response.response.rect);
state.bounding_rect |= response.response.rect;
response
.response
.ctx
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(widget_id, state));
.pass_state_mut(|fs| fs.tooltips.widget_tooltips.insert(parent_widget, state));
Self::remember_that_tooltip_was_shown(&response.response.ctx);
}
@@ -188,7 +211,10 @@ impl Tooltip<'_> {
}
/// Should we show a tooltip for this response?
pub fn should_show_tooltip(response: &Response) -> bool {
///
/// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that
/// contains interactive widgets
pub fn should_show_tooltip(response: &Response, allow_interactive_tooltip: bool) -> bool {
if response.ctx.memory(|mem| mem.everything_is_visible()) {
return true;
}
@@ -203,7 +229,7 @@ impl Tooltip<'_> {
return false;
}
let style = response.ctx.style();
let style = response.ctx.global_style();
let tooltip_delay = style.interaction.tooltip_delay;
let tooltip_grace_time = style.interaction.tooltip_grace_time;
@@ -241,12 +267,13 @@ impl Tooltip<'_> {
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 = response.ctx.viewport(|vp| {
vp.prev_pass
.widgets
.get_layer(tooltip_layer_id)
.any(|w| w.enabled && w.sense.interactive())
});
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())
});
if tooltip_has_interactive_widget {
// We keep the tooltip open if hovered,
@@ -331,7 +358,7 @@ impl Tooltip<'_> {
// We only show the tooltip when the mouse pointer is still.
if !response
.ctx
.input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO)
.input(|i| i.pointer.is_still() && !i.is_scrolling())
{
// wait for mouse to stop
response.ctx.request_repaint();

View File

@@ -8,8 +8,8 @@ use epaint::{CornerRadiusF32, RectShape};
use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::ScrollBarVisibility;
use super::{area, resize, Area, Frame, Resize, ScrollArea};
use super::scroll_area::{ScrollBarVisibility, ScrollSource};
use super::{Area, Frame, Resize, ScrollArea, area, resize};
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
///
@@ -70,6 +70,41 @@ impl<'open> Window<'open> {
}
}
/// Construct a [`Window`] that follows the given viewport.
pub fn from_viewport(id: ViewportId, viewport: ViewportBuilder) -> Self {
let ViewportBuilder {
title,
app_id,
inner_size,
min_inner_size,
max_inner_size,
resizable,
decorations,
title_shown,
minimize_button,
.. // A lot of things not implemented yet
} = viewport;
let mut window = Self::new(title.or(app_id).unwrap_or_else(String::new)).id(Id::new(id));
if let Some(inner_size) = inner_size {
window = window.default_size(inner_size);
}
if let Some(min_inner_size) = min_inner_size {
window = window.min_size(min_inner_size);
}
if let Some(max_inner_size) = max_inner_size {
window = window.max_size(max_inner_size);
}
if let Some(resizable) = resizable {
window = window.resizable(resizable);
}
window = window.title_bar(decorations.unwrap_or(true) && title_shown.unwrap_or(true));
window = window.collapsible(minimize_button.unwrap_or(true));
window
}
/// Assign a unique id to the Window. Required if the title changes, or is shared with another window.
#[inline]
pub fn id(mut self, id: Id) -> Self {
@@ -376,14 +411,6 @@ impl<'open> Window<'open> {
self
}
/// Enable/disable horizontal/vertical scrolling. `false` by default.
#[deprecated = "Renamed to `scroll`"]
#[inline]
pub fn scroll2(mut self, scroll: impl Into<Vec2b>) -> Self {
self.scroll = self.scroll.scroll(scroll);
self
}
/// Enable/disable horizontal scrolling. `false` by default.
#[inline]
pub fn hscroll(mut self, hscroll: bool) -> Self {
@@ -403,7 +430,10 @@ impl<'open> Window<'open> {
/// See [`ScrollArea::drag_to_scroll`] for more.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll = self.scroll.drag_to_scroll(drag_to_scroll);
self.scroll = self.scroll.scroll_source(ScrollSource {
drag: drag_to_scroll,
..Default::default()
});
self
}
@@ -445,9 +475,11 @@ impl Window<'_> {
fade_out,
} = self;
let style = ctx.global_style();
let header_color =
frame.map_or_else(|| ctx.style().visuals.widgets.open.weak_bg_fill, |f| f.fill);
let mut window_frame = frame.unwrap_or_else(|| Frame::window(&ctx.style()));
frame.map_or_else(|| style.visuals.widgets.open.weak_bg_fill, |f| f.fill);
let mut window_frame = frame.unwrap_or_else(|| Frame::window(&style));
let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
@@ -479,9 +511,8 @@ impl Window<'_> {
// Calculate roughly how much larger the full window inner size is compared to the content rect
let (title_bar_height_with_margin, title_content_spacing) = if with_title_bar {
let style = ctx.style();
let title_bar_inner_height = ctx
.fonts(|fonts| title.font_height(fonts, &style))
.fonts_mut(|fonts| title.font_height(fonts, &style))
.at_least(style.spacing.interact_size.y);
let title_bar_inner_height = title_bar_inner_height + window_frame.inner_margin.sum().y;
let half_height = (title_bar_inner_height / 2.0).round() as _;
@@ -510,15 +541,14 @@ impl Window<'_> {
// First check for resize to avoid frame delay:
let last_frame_outer_rect = area.state().rect();
let resize_interaction = ctx.with_accessibility_parent(area.id(), || {
resize_interaction(
ctx,
possible,
area_layer_id,
last_frame_outer_rect,
window_frame,
)
});
let resize_interaction = do_resize_interaction(
ctx,
possible,
area.id(),
area_layer_id,
last_frame_outer_rect,
window_frame,
);
{
let margins = window_frame.total_margin().sum()
@@ -543,117 +573,126 @@ impl Window<'_> {
}
let content_inner = {
ctx.with_accessibility_parent(area.id(), || {
// BEGIN FRAME --------------------------------
let mut frame = window_frame.begin(&mut area_content_ui);
// BEGIN FRAME --------------------------------
let mut frame = window_frame.begin(&mut area_content_ui);
let show_close_button = open.is_some();
let show_close_button = open.is_some();
let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
let where_to_put_header_background = &area_content_ui.painter().add(Shape::Noop);
let title_bar = if with_title_bar {
let title_bar = TitleBar::new(
&frame.content_ui,
title,
show_close_button,
collapsible,
window_frame,
title_bar_height_with_margin,
);
resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width
frame.content_ui.set_min_size(title_bar.inner_rect.size());
// Skip the title bar (and separator):
if is_collapsed {
frame.content_ui.add_space(title_bar.inner_rect.height());
} else {
frame.content_ui.add_space(
title_bar.inner_rect.height()
+ title_content_spacing
+ window_frame.inner_margin.sum().y,
);
}
Some(title_bar)
} else {
None
};
let (content_inner, content_response) = collapsing
.show_body_unindented(&mut frame.content_ui, |ui| {
resize.show(ui, |ui| {
if scroll.is_any_scroll_enabled() {
scroll.show(ui, add_contents).inner
} else {
add_contents(ui)
}
})
})
.map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
let outer_rect = frame.end(&mut area_content_ui).rect;
paint_resize_corner(
&area_content_ui,
&possible,
outer_rect,
&window_frame,
resize_interaction,
let title_bar = if with_title_bar {
let title_bar = TitleBar::new(
&frame.content_ui,
title,
show_close_button,
collapsible,
window_frame,
title_bar_height_with_margin,
);
resize.min_size.x = resize.min_size.x.at_least(title_bar.inner_rect.width()); // Prevent making window smaller than title bar width
// END FRAME --------------------------------
frame.content_ui.set_min_size(title_bar.inner_rect.size());
if let Some(mut title_bar) = title_bar {
title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
title_bar.inner_rect.max.y =
title_bar.inner_rect.min.y + title_bar_height_with_margin;
if on_top && area_content_ui.visuals().window_highlight_topmost {
let mut round =
window_frame.corner_radius - window_frame.stroke.width.round() as u8;
if !is_collapsed {
round.se = 0;
round.sw = 0;
}
area_content_ui.painter().set(
*where_to_put_header_background,
RectShape::filled(title_bar.inner_rect, round, header_color),
);
};
if false {
ctx.debug_painter().debug_rect(
title_bar.inner_rect,
Color32::LIGHT_BLUE,
"title_bar.rect",
);
}
title_bar.ui(
&mut area_content_ui,
&content_response,
open.as_deref_mut(),
&mut collapsing,
collapsible,
// Skip the title bar (and separator):
if is_collapsed {
frame.content_ui.add_space(title_bar.inner_rect.height());
} else {
frame.content_ui.add_space(
title_bar.inner_rect.height()
+ title_content_spacing
+ window_frame.inner_margin.sum().y,
);
}
collapsing.store(ctx);
Some(title_bar)
} else {
None
};
paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
let (content_inner, content_response) = collapsing
.show_body_unindented(&mut frame.content_ui, |ui| {
resize.show(ui, |ui| {
if scroll.is_any_scroll_enabled() {
scroll.show(ui, add_contents).inner
} else {
add_contents(ui)
}
})
})
.map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
content_inner
})
let outer_rect = frame.end(&mut area_content_ui).rect;
// Do resize interaction _again_, to move their widget rectangles on TOP of the rest of the window.
let resize_interaction = do_resize_interaction(
ctx,
possible,
area.id(),
area_layer_id,
last_frame_outer_rect,
window_frame,
);
paint_resize_corner(
&area_content_ui,
&possible,
outer_rect,
&window_frame,
resize_interaction,
);
// END FRAME --------------------------------
if let Some(mut title_bar) = title_bar {
title_bar.inner_rect = outer_rect.shrink(window_frame.stroke.width);
title_bar.inner_rect.max.y =
title_bar.inner_rect.min.y + title_bar_height_with_margin;
if on_top && area_content_ui.visuals().window_highlight_topmost {
let mut round =
window_frame.corner_radius - window_frame.stroke.width.round() as u8;
if !is_collapsed {
round.se = 0;
round.sw = 0;
}
area_content_ui.painter().set(
*where_to_put_header_background,
RectShape::filled(title_bar.inner_rect, round, header_color),
);
}
if false {
ctx.debug_painter().debug_rect(
title_bar.inner_rect,
Color32::LIGHT_BLUE,
"title_bar.rect",
);
}
title_bar.ui(
&mut area_content_ui,
&content_response,
open.as_deref_mut(),
&mut collapsing,
collapsible,
);
}
collapsing.store(ctx);
paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
content_inner
};
let full_response = area.end(ctx, area_content_ui);
if full_response.should_close() {
if let Some(open) = open {
*open = false;
}
if full_response.should_close()
&& let Some(open) = open
{
*open = false;
}
let inner_response = InnerResponse {
@@ -833,7 +872,7 @@ fn resize_response(
area: &mut area::Prepared,
resize_id: Id,
) {
let Some(mut new_rect) = move_and_resize_window(ctx, &resize_interaction) else {
let Some(mut new_rect) = move_and_resize_window(ctx, resize_id, &resize_interaction) else {
return;
};
@@ -844,38 +883,49 @@ fn resize_response(
// TODO(emilk): add this to a Window state instead as a command "move here next frame"
area.state_mut().set_left_top_pos(new_rect.left_top());
if resize_interaction.any_dragged() {
if let Some(mut state) = resize::State::load(ctx, resize_id) {
state.requested_size = Some(new_rect.size() - margins);
state.store(ctx, resize_id);
}
if resize_interaction.any_dragged()
&& let Some(mut state) = resize::State::load(ctx, resize_id)
{
state.requested_size = Some(new_rect.size() - margins);
state.store(ctx, resize_id);
}
ctx.memory_mut(|mem| mem.areas_mut().move_to_top(area_layer_id));
}
/// Acts on outer rect (outside the stroke)
fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Option<Rect> {
fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction) -> Option<Rect> {
// Used to prevent drift
let rect_at_start_of_drag_id = id.with("window_rect_at_drag_start");
if !interaction.any_dragged() {
ctx.data_mut(|data| {
data.remove::<Rect>(rect_at_start_of_drag_id);
});
return None;
}
let pointer_pos = ctx.input(|i| i.pointer.interact_pos())?;
let mut rect = interaction.outer_rect; // prevent drift
let total_drag_delta = ctx.input(|i| i.pointer.total_drag_delta())?;
let rect_at_start_of_drag = ctx.data_mut(|data| {
*data.get_temp_mut_or::<Rect>(rect_at_start_of_drag_id, interaction.outer_rect)
});
let mut rect = rect_at_start_of_drag; // prevent drift
// Put the rect in the center of the stroke:
rect = rect.shrink(interaction.window_frame.stroke.width / 2.0);
if interaction.left.drag {
rect.min.x = pointer_pos.x;
rect.min.x += total_drag_delta.x;
} else if interaction.right.drag {
rect.max.x = pointer_pos.x;
rect.max.x += total_drag_delta.x;
}
if interaction.top.drag {
rect.min.y = pointer_pos.y;
rect.min.y += total_drag_delta.y;
} else if interaction.bottom.drag {
rect.max.y = pointer_pos.y;
rect.max.y += total_drag_delta.y;
}
// Return to having the rect outside the stroke:
@@ -884,9 +934,10 @@ fn move_and_resize_window(ctx: &Context, interaction: &ResizeInteraction) -> Opt
Some(rect.round_ui())
}
fn resize_interaction(
fn do_resize_interaction(
ctx: &Context,
possible: PossibleInteractions,
accessibility_parent: Id,
layer_id: LayerId,
outer_rect: Rect,
window_frame: Frame,
@@ -906,17 +957,27 @@ fn resize_interaction(
let rect = outer_rect.shrink(window_frame.stroke.width / 2.0);
let side_response = |rect, id| {
ctx.register_accesskit_parent(id, accessibility_parent);
let response = ctx.create_widget(
WidgetRect {
layer_id,
id,
rect,
interact_rect: rect,
sense: Sense::drag(),
sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable
enabled: true,
},
true,
InteractOptions {
// We call this multiple times.
// First to read the result (to avoid frame delay)
// and the second time to move it to the top, above the window contents.
move_to_top: true,
},
);
response.widget_info(|| WidgetInfo::new(crate::WidgetType::ResizeHandle));
SideResponse {
hover: response.hovered(),
drag: response.dragged(),
@@ -925,8 +986,10 @@ fn resize_interaction(
let id = Id::new(layer_id).with("edge_drag");
let side_grab_radius = ctx.style().interaction.resize_grab_radius_side;
let corner_grab_radius = ctx.style().interaction.resize_grab_radius_corner;
let style = ctx.global_style();
let side_grab_radius = style.interaction.resize_grab_radius_side;
let corner_grab_radius = style.interaction.resize_grab_radius_corner;
let vetrtical_rect = |a: Pos2, b: Pos2| {
Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
@@ -1136,8 +1199,7 @@ impl TitleBar {
title_bar_height_with_margin: f32,
) -> Self {
if false {
ui.ctx()
.debug_painter()
ui.debug_painter()
.debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
}
@@ -1165,8 +1227,7 @@ impl TitleBar {
let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
if false {
ui.ctx()
.debug_painter()
ui.debug_painter()
.debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
}
@@ -1203,8 +1264,7 @@ impl TitleBar {
let title_inner_rect = self.inner_rect;
if false {
ui.ctx()
.debug_painter()
ui.debug_painter()
.debug_rect(self.inner_rect, Color32::RED, "TitleBar");
}
@@ -1235,7 +1295,7 @@ impl TitleBar {
let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
ui.painter().galley(
text_pos,
self.title_galley.clone(),
Arc::clone(&self.title_galley),
ui.visuals().text_color(),
);
@@ -1243,8 +1303,7 @@ impl TitleBar {
// Paint separator between title and content:
let content_rect = content_response.rect;
if false {
ui.ctx()
.debug_painter()
ui.debug_painter()
.debug_rect(content_rect, Color32::RED, "content_rect");
}
let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
@@ -1258,17 +1317,14 @@ impl TitleBar {
let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
if false {
ui.ctx().debug_painter().debug_rect(
double_click_rect,
Color32::GREEN,
"double_click_rect",
);
ui.debug_painter()
.debug_rect(double_click_rect, Color32::GREEN, "double_click_rect");
}
let id = ui.unique_id().with("__window_title_bar");
if ui
.interact(double_click_rect, id, Sense::click())
.interact(double_click_rect, id, Sense::CLICK)
.double_clicked()
&& collapsible
{

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
//! The input needed by egui.
use epaint::ColorImage;
use epaint::{ColorImage, MarginF32};
use crate::{
Key, OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap,
emath::{Pos2, Rect, Vec2},
Key, Theme, ViewportId, ViewportIdMap,
};
/// What the integrations provides to egui at the start of each frame.
@@ -27,6 +27,11 @@ pub struct RawInput {
/// Information about all egui viewports.
pub viewports: ViewportIdMap<ViewportInfo>,
/// The insets used to only render content in a mobile safe area
///
/// `None` will be treated as "same as last frame"
pub safe_area_insets: Option<SafeAreaInsets>,
/// Position and size of the area that egui should use, in points.
/// Usually you would set this to
///
@@ -98,6 +103,7 @@ impl Default for RawInput {
dropped_files: Default::default(),
focused: true, // integrations opt into global focus tracking
system_theme: None,
safe_area_insets: Default::default(),
}
}
}
@@ -122,6 +128,7 @@ impl RawInput {
.map(|(id, info)| (*id, info.take()))
.collect(),
screen_rect: self.screen_rect.take(),
safe_area_insets: self.safe_area_insets.take(),
max_texture_side: self.max_texture_side.take(),
time: self.time,
predicted_dt: self.predicted_dt,
@@ -149,6 +156,7 @@ impl RawInput {
mut dropped_files,
focused,
system_theme,
safe_area_insets: safe_area,
} = newer;
self.viewport_id = viewport_ids;
@@ -163,6 +171,7 @@ impl RawInput {
self.dropped_files.append(&mut dropped_files);
self.focused = focused;
self.system_theme = system_theme;
self.safe_area_insets = safe_area;
}
}
@@ -256,9 +265,7 @@ impl ViewportInfo {
/// If this is not the root viewport,
/// it is up to the user to hide this viewport the next frame.
pub fn close_requested(&self) -> bool {
self.events
.iter()
.any(|&event| event == ViewportEvent::Close)
self.events.contains(&ViewportEvent::Close)
}
/// Helper: move [`Self::events`], clone the other fields.
@@ -481,6 +488,9 @@ pub enum Event {
/// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame.
Zoom(f32),
/// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture).
Rotate(f32),
/// IME Event
Ime(ImeEvent),
@@ -525,6 +535,11 @@ pub enum Event {
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
delta: Vec2,
/// The phase of the scroll, useful for trackpads.
///
/// If unknown set this to [`TouchPhase::Move`].
phase: TouchPhase,
/// The state of the modifier keys at the time of the event.
modifiers: Modifiers,
},
@@ -533,7 +548,6 @@ pub enum Event {
WindowFocused(bool),
/// An assistive technology (e.g. screen reader) requested an action.
#[cfg(feature = "accesskit")]
AccessKitActionRequest(accesskit::ActionRequest),
/// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`].
@@ -592,7 +606,14 @@ pub const NUM_POINTER_BUTTONS: usize = 5;
/// State of the modifier keys. These must be fed to egui.
///
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches`].
/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`].
///
/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function
///
/// ```rust
/// # let ctx = egui::Context::default();
/// let modifiers = ctx.input(|i| i.modifiers);
/// ```
///
/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers
/// as on mac that is how you type special characters,
@@ -775,8 +796,8 @@ impl Modifiers {
/// ```
/// # use egui::Modifiers;
/// # let pressed_modifiers = Modifiers::default();
/// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, and nothing else
/// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, but not ctrl/command
/// }
/// ```
///
@@ -817,7 +838,7 @@ impl Modifiers {
/// ```
/// # use egui::Modifiers;
/// # let pressed_modifiers = Modifiers::default();
/// if pressed_modifiers.matches(Modifiers::ALT | Modifiers::SHIFT) {
/// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) {
/// // Alt and Shift are pressed, and nothing else
/// }
/// ```
@@ -825,13 +846,13 @@ impl Modifiers {
/// ## Behavior:
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches(Modifiers::MAC_CMD));
/// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL));
/// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL));
/// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
/// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND));
/// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD));
/// ```
pub fn matches_exact(&self, pattern: Self) -> bool {
// alt and shift must always match the pattern:
@@ -842,9 +863,35 @@ impl Modifiers {
self.cmd_ctrl_matches(pattern)
}
#[deprecated = "Renamed `matches_exact`, but maybe you want to use `matches_logically` instead"]
pub fn matches(&self, pattern: Self) -> bool {
self.matches_exact(pattern)
/// Check if any of the modifiers match exactly.
///
/// Returns true if the same modifier is pressed in `self` as in `pattern`,
/// for at least one modifier.
///
/// ## Behavior:
/// ```
/// # use egui::Modifiers;
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL));
/// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT));
/// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL));
/// ```
pub fn matches_any(&self, pattern: Self) -> bool {
if self.alt && pattern.alt {
return true;
}
if self.shift && pattern.shift {
return true;
}
if self.ctrl && pattern.ctrl {
return true;
}
if self.mac_cmd && pattern.mac_cmd {
return true;
}
if (self.mac_cmd || self.command || self.ctrl) && pattern.command {
return true;
}
false
}
/// Checks only cmd/ctrl, not alt/shift.
@@ -960,6 +1007,12 @@ impl std::ops::BitOrAssign for Modifiers {
}
}
impl Modifiers {
pub fn ui(&self, ui: &mut crate::Ui) {
ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac()));
}
}
// ----------------------------------------------------------------------------
/// Names of different modifier keys.
@@ -1099,10 +1152,15 @@ impl RawInput {
dropped_files,
focused,
system_theme,
safe_area_insets: safe_area,
} = self;
ui.label(format!("Active viewport: {viewport_id:?}"));
for (id, viewport) in viewports {
let ordered_viewports = viewports
.iter()
.map(|(id, value)| (*id, value))
.collect::<OrderedViewportIdMap<_>>();
for (id, viewport) in ordered_viewports {
ui.group(|ui| {
ui.label(format!("Viewport {id:?}"));
ui.push_id(id, |ui| {
@@ -1124,6 +1182,7 @@ impl RawInput {
ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.label(format!("focused: {focused}"));
ui.label(format!("system_theme: {system_theme:?}"));
ui.label(format!("safe_area: {safe_area:?}"));
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {events:#?}"))
@@ -1233,7 +1292,7 @@ pub struct EventFilter {
pub escape: bool,
}
#[allow(clippy::derivable_impls)] // let's be explicit
#[expect(clippy::derivable_impls)] // let's be explicit
impl Default for EventFilter {
fn default() -> Self {
Self {
@@ -1260,3 +1319,19 @@ impl EventFilter {
}
}
}
/// The 'safe area' insets of the screen
///
/// This represents the area taken up by the status bar, navigation controls, notches,
/// or any other items that obscure parts of the screen.
#[derive(Debug, PartialEq, Copy, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct SafeAreaInsets(pub MarginF32);
impl std::ops::Sub<SafeAreaInsets> for Rect {
type Output = Self;
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {
self - rhs.0
}
}

View File

@@ -183,6 +183,11 @@ pub enum Key {
F33,
F34,
F35,
/// Back navigation key from multimedia keyboard.
/// Android sends this key on Back button press.
/// Does not work on Web.
BrowserBack,
// When adding keys, remember to also update:
// * crates/egui-winit/src/lib.rs
// * Key::ALL
@@ -307,6 +312,8 @@ impl Key {
Self::F33,
Self::F34,
Self::F35,
// Navigation keys:
Self::BrowserBack,
];
/// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc.
@@ -435,6 +442,8 @@ impl Key {
"F34" => Self::F34,
"F35" => Self::F35,
"BrowserBack" => Self::BrowserBack,
_ => return None,
})
}
@@ -588,6 +597,8 @@ impl Key {
Self::F33 => "F33",
Self::F34 => "F34",
Self::F35 => "F35",
Self::BrowserBack => "BrowserBack",
}
}
}
@@ -596,7 +607,7 @@ impl Key {
fn test_key_from_name() {
assert_eq!(
Key::ALL.len(),
Key::F35 as usize + 1,
Key::BrowserBack as usize + 1,
"Some keys are missing in Key::ALL"
);

View File

@@ -1,6 +1,6 @@
//! All the data egui returns to the backend at the end of each frame.
use crate::{RepaintCause, ViewportIdMap, ViewportOutput, WidgetType};
use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
/// What egui emits each frame from [`crate::Context::run`].
///
@@ -32,12 +32,14 @@ pub struct FullOutput {
///
/// It is up to the integration to spawn a native window for each viewport,
/// and to close any window that no longer has a viewport in this map.
pub viewport_output: ViewportIdMap<ViewportOutput>,
pub viewport_output: OrderedViewportIdMap<ViewportOutput>,
}
impl FullOutput {
/// Add on new output.
pub fn append(&mut self, newer: Self) {
use std::collections::btree_map::Entry;
let Self {
platform_output,
textures_delta,
@@ -53,10 +55,10 @@ impl FullOutput {
for (id, new_viewport) in viewport_output {
match self.viewport_output.entry(id) {
std::collections::hash_map::Entry::Vacant(entry) => {
Entry::Vacant(entry) => {
entry.insert(new_viewport);
}
std::collections::hash_map::Entry::Occupied(mut entry) => {
Entry::Occupied(mut entry) => {
entry.get_mut().append(new_viewport);
}
}
@@ -95,9 +97,6 @@ pub enum OutputCommand {
/// Open this url in a browser.
OpenUrl(OpenUrl),
/// Set the mouse cursor position (if the platform supports it).
SetPointerPosition(emath::Pos2),
}
/// The non-rendering part of what egui emits each frame.
@@ -114,24 +113,6 @@ pub struct PlatformOutput {
/// Set the cursor to this icon.
pub cursor_icon: CursorIcon,
/// If set, open this url.
#[deprecated = "Use `Context::open_url` or `PlatformOutput::commands` instead"]
pub open_url: Option<OpenUrl>,
/// If set, put this text in the system clipboard. Ignore if empty.
///
/// This is often a response to [`crate::Event::Copy`] or [`crate::Event::Cut`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.button("📋").clicked() {
/// ui.output_mut(|o| o.copied_text = "some_text".to_string());
/// }
/// # });
/// ```
#[deprecated = "Use `Context::copy_text` or `PlatformOutput::commands` instead"]
pub copied_text: String,
/// Events that may be useful to e.g. a screen reader.
pub events: Vec<OutputEvent>,
@@ -147,7 +128,6 @@ pub struct PlatformOutput {
/// The difference in the widget tree since last frame.
///
/// NOTE: this needs to be per-viewport.
#[cfg(feature = "accesskit")]
pub accesskit_update: Option<accesskit::TreeUpdate>,
/// How many ui passes is this the sum of?
@@ -188,17 +168,12 @@ impl PlatformOutput {
/// Add on new output.
pub fn append(&mut self, newer: Self) {
#![allow(deprecated)]
let Self {
mut commands,
cursor_icon,
open_url,
copied_text,
mut events,
mutable_text_under_cursor,
ime,
#[cfg(feature = "accesskit")]
accesskit_update,
num_completed_passes,
mut request_discard_reasons,
@@ -206,12 +181,6 @@ impl PlatformOutput {
self.commands.append(&mut commands);
self.cursor_icon = cursor_icon;
if open_url.is_some() {
self.open_url = open_url;
}
if !copied_text.is_empty() {
self.copied_text = copied_text;
}
self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor;
self.ime = ime.or(self.ime);
@@ -219,12 +188,8 @@ impl PlatformOutput {
self.request_discard_reasons
.append(&mut request_discard_reasons);
#[cfg(feature = "accesskit")]
{
// egui produces a complete AccessKit tree for each frame,
// so overwrite rather than appending.
self.accesskit_update = accesskit_update;
}
// egui produces a complete AccessKit tree for each frame, so overwrite rather than append:
self.accesskit_update = accesskit_update;
}
/// Take everything ephemeral (everything except `cursor_icon` currently)
@@ -255,7 +220,7 @@ pub struct OpenUrl {
}
impl OpenUrl {
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn same_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -263,7 +228,7 @@ impl OpenUrl {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn new_tab(url: impl ToString) -> Self {
Self {
url: url.to_string(),
@@ -295,10 +260,11 @@ pub enum UserAttentionType {
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
///
/// Loosely based on <https://developer.mozilla.org/en-US/docs/Web/CSS/cursor>.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum CursorIcon {
/// Normal cursor icon, whatever that is.
#[default]
Default,
/// Show no cursor
@@ -458,12 +424,6 @@ impl CursorIcon {
];
}
impl Default for CursorIcon {
fn default() -> Self {
Self::Default
}
}
/// Things that happened during this frame that the integration may be interested in.
///
/// In particular, these events may be useful for accessibility, i.e. for screen readers.
@@ -610,7 +570,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn labeled(typ: WidgetType, enabled: bool, label: impl ToString) -> Self {
Self {
enabled,
@@ -620,7 +580,7 @@ impl WidgetInfo {
}
/// checkboxes, radio-buttons etc
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn selected(typ: WidgetType, enabled: bool, selected: bool, label: impl ToString) -> Self {
Self {
enabled,
@@ -638,7 +598,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn slider(enabled: bool, value: f64, label: impl ToString) -> Self {
let label = label.to_string();
Self {
@@ -649,7 +609,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text_edit(
enabled: bool,
prev_text_value: impl ToString,
@@ -673,7 +633,7 @@ impl WidgetInfo {
}
}
#[allow(clippy::needless_pass_by_value)]
#[expect(clippy::needless_pass_by_value)]
pub fn text_selection_changed(
enabled: bool,
text_selection: std::ops::RangeInclusive<usize>,
@@ -714,11 +674,13 @@ impl WidgetInfo {
WidgetType::Slider => "slider",
WidgetType::DragValue => "drag value",
WidgetType::ColorButton => "color button",
WidgetType::ImageButton => "image button",
WidgetType::Image => "image",
WidgetType::CollapsingHeader => "collapsing header",
WidgetType::Panel => "panel",
WidgetType::ProgressIndicator => "progress indicator",
WidgetType::Window => "window",
WidgetType::ScrollBar => "scroll bar",
WidgetType::ResizeHandle => "resize handle",
WidgetType::Label | WidgetType::Other => "",
};
@@ -730,7 +692,7 @@ impl WidgetInfo {
description = format!("{state} {description}");
} else {
description += if *selected { "selected" } else { "" };
};
}
}
if let Some(label) = label {
@@ -742,7 +704,7 @@ impl WidgetInfo {
if text_value.is_empty() {
"blank".into()
} else {
text_value.to_string()
text_value.clone()
}
} else {
"blank".into()

View File

@@ -1,24 +1,14 @@
//! This is an example of how to create a plugin for egui.
//!
//! A plugin usually consist of a struct that holds some state,
//! which is stored using [`Context::data_mut`].
//! The plugin registers itself onto a specific [`Context`]
//! to get callbacks on certain events ([`Context::on_begin_pass`], [`Context::on_end_pass`]).
//! A plugin is a struct that implements the [`Plugin`] trait and holds some state.
//! The plugin is registered with the [`Context`] using [`Context::add_plugin`]
//! to get callbacks on certain events ([`Plugin::on_begin_pass`], [`Plugin::on_end_pass`]).
use crate::{
text, Align, Align2, Color32, Context, FontFamily, FontId, Id, Rect, Shape, Vec2, WidgetText,
Align, Align2, Color32, Context, FontFamily, FontId, Plugin, Rect, Shape, Ui, Vec2, WidgetText,
text,
};
/// Register this plugin on the given egui context,
/// so that it will be called every pass.
///
/// This is a built-in plugin in egui,
/// meaning [`Context`] calls this from its `Default` implementation,
/// so this is marked as `pub(crate)`.
pub(crate) fn register(ctx: &Context) {
ctx.on_end_pass("debug_text", std::sync::Arc::new(State::end_pass));
}
/// Print this text next to the cursor at the end of the pass.
///
/// If you call this multiple times, the text will be appended.
@@ -38,15 +28,12 @@ pub fn print(ctx: &Context, text: impl Into<WidgetText>) {
let location = std::panic::Location::caller();
let location = format!("{}:{}", location.file(), location.line());
ctx.data_mut(|data| {
// We use `Id::NULL` as the id, since we only have one instance of this plugin.
// We use the `temp` version instead of `persisted` since we don't want to
// persist state on disk when the egui app is closed.
let state = data.get_temp_mut_or_default::<State>(Id::NULL);
state.entries.push(Entry {
location,
text: text.into(),
});
let plugin = ctx.plugin::<DebugTextPlugin>();
let mut state = plugin.lock();
state.entries.push(Entry {
location,
text: text.into(),
});
}
@@ -58,24 +45,26 @@ struct Entry {
/// A plugin for easily showing debug-text on-screen.
///
/// This is a built-in plugin in egui.
/// This is a built-in plugin in egui, automatically registered during [`Context`] creation.
#[derive(Clone, Default)]
struct State {
pub struct DebugTextPlugin {
// This gets re-filled every pass.
entries: Vec<Entry>,
}
impl State {
fn end_pass(ctx: &Context) {
let state = ctx.data_mut(|data| data.remove_temp::<Self>(Id::NULL));
if let Some(state) = state {
state.paint(ctx);
}
impl Plugin for DebugTextPlugin {
fn debug_name(&self) -> &'static str {
"DebugTextPlugin"
}
fn paint(self, ctx: &Context) {
let Self { entries } = self;
fn on_end_pass(&mut self, ui: &mut Ui) {
let entries = std::mem::take(&mut self.entries);
Self::paint_entries(ui, entries);
}
}
impl DebugTextPlugin {
fn paint_entries(ctx: &Context, entries: Vec<Entry>) {
if entries.is_empty() {
return;
}
@@ -83,7 +72,7 @@ impl State {
// Show debug-text next to the cursor.
let mut pos = ctx
.input(|i| i.pointer.latest_pos())
.unwrap_or_else(|| ctx.screen_rect().center())
.unwrap_or_else(|| ctx.content_rect().center())
+ 8.0 * Vec2::Y;
let painter = ctx.debug_painter();
@@ -98,26 +87,26 @@ impl State {
{
// Paint location to left of `pos`:
let location_galley =
ctx.fonts(|f| f.layout(location, font_id.clone(), color, f32::INFINITY));
ctx.fonts_mut(|f| f.layout(location, font_id.clone(), color, f32::INFINITY));
let location_rect =
Align2::RIGHT_TOP.anchor_size(pos - 4.0 * Vec2::X, location_galley.size());
painter.galley(location_rect.min, location_galley, color);
bounding_rect = bounding_rect.union(location_rect);
bounding_rect |= location_rect;
}
{
// Paint `text` to right of `pos`:
let available_width = ctx.screen_rect().max.x - pos.x;
let available_width = ctx.content_rect().max.x - pos.x;
let galley = text.into_galley_impl(
ctx,
&ctx.style(),
&ctx.global_style(),
text::TextWrapping::wrap_at_width(available_width),
font_id.clone().into(),
Align::TOP,
);
let rect = Align2::LEFT_TOP.anchor_size(pos, galley.size());
painter.galley(rect.min, galley, color);
bounding_rect = bounding_rect.union(rect);
bounding_rect |= rect;
}
pos.y = bounding_rect.max.y + 4.0;

View File

@@ -1,44 +1,46 @@
use std::{any::Any, sync::Arc};
use crate::{Context, CursorIcon, Id};
use crate::{Context, CursorIcon, Plugin, Ui};
/// Tracking of drag-and-drop payload.
/// Plugin for tracking drag-and-drop payload.
///
/// This is a low-level API.
/// This plugin stores the current drag-and-drop payload internally and handles
/// automatic cleanup when the drag operation ends (via Escape key or mouse release).
///
/// For a higher-level API, see:
/// This is a low-level API. For a higher-level API, see:
/// - [`crate::Ui::dnd_drag_source`]
/// - [`crate::Ui::dnd_drop_zone`]
/// - [`crate::Response::dnd_set_drag_payload`]
/// - [`crate::Response::dnd_hover_payload`]
/// - [`crate::Response::dnd_release_payload`]
///
/// See [this example](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
/// This is a built-in plugin in egui, automatically registered during [`Context`] creation.
///
/// See [this example](https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
#[doc(alias = "drag and drop")]
#[derive(Clone, Default)]
pub struct DragAndDrop {
/// If set, something is currently being dragged
/// The current drag-and-drop payload, if any. Automatically cleared when drag ends.
payload: Option<Arc<dyn Any + Send + Sync>>,
}
impl DragAndDrop {
pub(crate) fn register(ctx: &Context) {
ctx.on_begin_pass("drag_and_drop_begin_pass", Arc::new(Self::begin_pass));
ctx.on_end_pass("drag_and_drop_end_pass", Arc::new(Self::end_pass));
impl Plugin for DragAndDrop {
fn debug_name(&self) -> &'static str {
"DragAndDrop"
}
/// Interrupt drag-and-drop if the user presses the escape key.
///
/// This needs to happen at frame start so we can properly capture the escape key.
fn begin_pass(ctx: &Context) {
let has_any_payload = Self::has_any_payload(ctx);
fn on_begin_pass(&mut self, ui: &mut Ui) {
let has_any_payload = self.payload.is_some();
if has_any_payload {
let abort_dnd_due_to_escape_key =
ctx.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
ui.input_mut(|i| i.consume_key(crate::Modifiers::NONE, crate::Key::Escape));
if abort_dnd_due_to_escape_key {
Self::clear_payload(ctx);
self.payload = None;
}
}
}
@@ -48,18 +50,18 @@ impl DragAndDrop {
/// This is a catch-all safety net in case user code doesn't capture the drag payload itself.
/// This must happen at end-of-frame such that we don't shadow the mouse release event from user
/// code.
fn end_pass(ctx: &Context) {
let has_any_payload = Self::has_any_payload(ctx);
fn on_end_pass(&mut self, ui: &mut Ui) {
let has_any_payload = self.payload.is_some();
if has_any_payload {
let abort_dnd_due_to_mouse_release = ctx.input_mut(|i| i.pointer.any_released());
let abort_dnd_due_to_mouse_release = ui.input_mut(|i| i.pointer.any_released());
if abort_dnd_due_to_mouse_release {
Self::clear_payload(ctx);
self.payload = None;
} else {
// We set the cursor icon only if its default, as the user code might have
// explicitly set it already.
ctx.output_mut(|o| {
ui.output_mut(|o| {
if o.cursor_icon == CursorIcon::Default {
o.cursor_icon = CursorIcon::Grabbing;
}
@@ -67,7 +69,9 @@ impl DragAndDrop {
}
}
}
}
impl DragAndDrop {
/// Set a drag-and-drop payload.
///
/// This can be read by [`Self::payload`] until the pointer is released.
@@ -75,18 +79,12 @@ impl DragAndDrop {
where
Payload: Any + Send + Sync,
{
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
state.payload = Some(Arc::new(payload));
});
ctx.plugin::<Self>().lock().payload = Some(Arc::new(payload));
}
/// Clears the payload, setting it to `None`.
pub fn clear_payload(ctx: &Context) {
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
state.payload = None;
});
ctx.plugin::<Self>().lock().payload = None;
}
/// Retrieve the payload, if any.
@@ -99,11 +97,9 @@ impl DragAndDrop {
where
Payload: Any + Send + Sync,
{
ctx.data(|data| {
let state = data.get_temp::<Self>(Id::NULL)?;
let payload = state.payload?;
payload.downcast().ok()
})
Arc::clone(ctx.plugin::<Self>().lock().payload.as_ref()?)
.downcast()
.ok()
}
/// Retrieve and clear the payload, if any.
@@ -116,11 +112,7 @@ impl DragAndDrop {
where
Payload: Any + Send + Sync,
{
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
let payload = state.payload.take()?;
payload.downcast().ok()
})
ctx.plugin::<Self>().lock().payload.take()?.downcast().ok()
}
/// Are we carrying a payload of the given type?
@@ -139,9 +131,6 @@ impl DragAndDrop {
/// Returns `true` both during a drag and on the frame the pointer is released
/// (if there is a payload).
pub fn has_any_payload(ctx: &Context) -> bool {
ctx.data(|data| {
let state = data.get_temp::<Self>(Id::NULL);
state.is_some_and(|state| state.payload.is_some())
})
ctx.plugin::<Self>().lock().payload.is_some()
}
}

View File

@@ -1,9 +1,11 @@
use std::sync::Arc;
use emath::GuiRounding as _;
use std::fmt::Debug;
use crate::{
vec2, Align2, Color32, Context, Id, InnerResponse, NumExt, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2,
Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2, vec2,
};
#[cfg(debug_assertions)]
@@ -103,7 +105,7 @@ impl GridLayout {
Self {
ctx: ui.ctx().clone(),
style: ui.style().clone(),
style: Arc::clone(ui.style()),
id,
is_first_frame,
prev_state,
@@ -185,7 +187,7 @@ impl GridLayout {
Rect::from_min_size(cursor.min, size).round_ui()
}
#[allow(clippy::unused_self)]
#[expect(clippy::unused_self)]
pub(crate) fn align_size_within_rect(&self, size: Vec2, frame: Rect) -> Rect {
// TODO(emilk): allow this alignment to be customized
Align2::LEFT_CENTER
@@ -450,7 +452,7 @@ impl Grid {
if ui.is_visible() {
// Try to cover up the glitchy initial frame:
ui.ctx().request_discard("new Grid");
ui.request_discard("new Grid");
}
// Hide the ui this frame, and make things as narrow as possible:

View File

@@ -1,4 +1,4 @@
//! Helpers for zooming the whole GUI of an app (changing [`Context::pixels_per_point`].
//! Helpers for zooming the whole GUI of an app (changing [`Context::pixels_per_point`]).
//!
use crate::{Button, Context, Key, KeyboardShortcut, Modifiers, Ui};

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{ahash, emath, id::IdSet, LayerId, Pos2, Rect, Sense, WidgetRect, WidgetRects};
use crate::{LayerId, Pos2, Sense, WidgetRect, WidgetRects, ahash, emath, id::IdSet};
/// Result of a hit-test against [`WidgetRects`].
///
@@ -65,7 +65,7 @@ pub fn hit_test(
.filter(|layer| layer.order.allow_interaction())
.flat_map(|&layer_id| widgets.get_layer(layer_id))
.filter(|&w| {
if w.interact_rect.is_negative() {
if w.interact_rect.is_negative() || w.interact_rect.any_nan() {
return false;
}
@@ -91,6 +91,8 @@ pub fn hit_test(
}
}
close.retain(|rect| !rect.interact_rect.any_nan()); // Protect against bad input and transforms
// When using layer transforms it is common to stack layers close to each other.
// For instance, you may have a resize-separator on a panel, with two
// transform-layers on either side.
@@ -199,7 +201,7 @@ fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool {
}
fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
#![allow(clippy::collapsible_else_if)]
#![expect(clippy::collapsible_else_if)]
// First find the best direct hits:
let hit_click = find_closest_within(
@@ -315,19 +317,18 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
pos,
);
if let Some(closest_drag) = closest_drag {
if hit_drag
if let Some(closest_drag) = closest_drag
&& hit_drag
.interact_rect
.contains_rect(closest_drag.interact_rect)
{
// `hit_drag` is a big background thing and `closest_drag` is something small on top of it.
// Be helpful and return the small things:
return WidgetHits {
click: None,
drag: Some(closest_drag),
..Default::default()
};
}
{
// `hit_drag` is a big background thing and `closest_drag` is something small on top of it.
// Be helpful and return the small things:
return WidgetHits {
click: None,
drag: Some(closest_drag),
..Default::default()
};
}
WidgetHits {
@@ -360,7 +361,10 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits {
(Some(hit_click), Some(hit_drag)) => {
// We have a perfect hit on both click and drag. Which is the topmost?
#[expect(clippy::unwrap_used)]
let click_idx = close.iter().position(|w| *w == hit_click).unwrap();
#[expect(clippy::unwrap_used)]
let drag_idx = close.iter().position(|w| *w == hit_drag).unwrap();
let click_is_on_top_of_drag = drag_idx < click_idx;
@@ -423,16 +427,6 @@ fn find_closest_within(
let dist_sq = widget.interact_rect.distance_sq_to_pos(pos);
if let Some(closest) = closest {
if dist_sq == closest_dist_sq {
// It's a tie! Pick the thin candidate over the thick one.
// This makes it easier to hit a thin resize-handle, for instance:
if should_prioritize_hits_on_back(closest.interact_rect, widget.interact_rect) {
continue;
}
}
}
// In case of a tie, take the last one = the one on top.
if dist_sq <= closest_dist_sq {
closest_dist_sq = dist_sq;
@@ -443,30 +437,11 @@ fn find_closest_within(
closest
}
/// Should we prioritize hits on `back` over those on `front`?
///
/// `back` should be behind the `front` widget.
///
/// Returns true if `back` is a small hit-target and `front` is not.
fn should_prioritize_hits_on_back(back: Rect, front: Rect) -> bool {
if front.contains_rect(back) {
return false; // back widget is fully occluded; no way to hit it
}
// Reduce each rect to its width or height, whichever is smaller:
let back = back.width().min(back.height());
let front = front.width().min(front.height());
// These are hard-coded heuristics that could surely be improved.
let back_is_much_thinner = back <= 0.5 * front;
let back_is_thin = back <= 16.0;
back_is_much_thinner && back_is_thin
}
#[cfg(test)]
mod tests {
use emath::{pos2, vec2, Rect};
#![expect(clippy::print_stdout)]
use emath::{Rect, pos2, vec2};
use crate::{Id, Sense};

View File

@@ -70,7 +70,7 @@ impl Id {
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl AsId) -> Self {
use std::hash::{BuildHasher, Hasher};
use std::hash::{BuildHasher as _, Hasher as _};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0.get());
(&child).hash(&mut hasher);
@@ -95,11 +95,29 @@ impl Id {
self.0.get()
}
#[cfg(feature = "accesskit")]
pub(crate) fn accesskit_id(&self) -> accesskit::NodeId {
self.value().into()
}
/// Create a new [`Id`] from a high-entropy value. No hashing is done.
///
/// This can be useful if you have an [`Id`] that was converted to some other type
/// (e.g. accesskit::NodeId) and you want to convert it back to an [`Id`].
///
/// # Safety
/// You need to ensure that the value is high-entropy since it might be used in
/// a [`IdSet`] or [`IdMap`], which rely on the assumption that [`Id`]s have good entropy.
///
/// The method is not unsafe in terms of memory safety.
///
/// # Panics
/// If the value is zero, this will panic.
#[doc(hidden)]
#[expect(unsafe_code)]
pub unsafe fn from_high_entropy_bits(value: u64) -> Self {
Self(NonZeroU64::new(value).expect("Id must be non-zero."))
}
fn source_ui(ui: &mut crate::Ui, source: IdSource) {
match source {
IdSource::Id(id) => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
use std::{collections::BTreeMap, fmt::Debug};
use crate::{
data::input::TouchDeviceId,
emath::{normalized_angle, Pos2, Vec2},
Event, RawInput, TouchId, TouchPhase,
data::input::TouchDeviceId,
emath::{Pos2, Vec2, normalized_angle},
};
/// All you probably need to know about a multi-touch gesture.
@@ -174,7 +174,7 @@ impl TouchState {
if added_or_removed_touches {
// Adding or removing fingers makes the average values "jump". We better forget
// about the previous values, and don't create delta information for this frame:
if let Some(ref mut state) = &mut self.gesture_state {
if let Some(state) = &mut self.gesture_state {
state.previous = None;
}
}
@@ -194,7 +194,7 @@ impl TouchState {
let zoom_delta = state.current.avg_distance / state_previous.avg_distance;
let zoom_delta2 = match state.pinch_type {
let zoom_delta_2d = match state.pinch_type {
PinchType::Horizontal => Vec2::new(
state.current.avg_abs_distance2.x / state_previous.avg_abs_distance2.x,
1.0,
@@ -213,7 +213,7 @@ impl TouchState {
start_pos: state.start_pointer_pos,
num_touches: self.active_touches.len(),
zoom_delta,
zoom_delta_2d: zoom_delta2,
zoom_delta_2d,
rotation_delta: normalized_angle(state.current.heading - state_previous.heading),
translation_delta: state.current.avg_pos - state_previous.avg_pos,
force: state.current.avg_force,
@@ -224,7 +224,7 @@ impl TouchState {
fn update_gesture(&mut self, time: f64, pointer_pos: Option<Pos2>) {
if let Some(dyn_state) = self.calc_dynamic_state() {
if let Some(ref mut state) = &mut self.gesture_state {
if let Some(state) = &mut self.gesture_state {
// updating an ongoing gesture
state.previous = Some(state.current);
state.current = dyn_state;
@@ -288,6 +288,7 @@ impl TouchState {
// touch individually, and then calculate the average of all individual changes in
// direction. But this approach cannot be implemented locally in this method, making
// everything a bit more complicated.
#[expect(clippy::unwrap_used)] // guarded against already
let first_touch = self.active_touches.values().next().unwrap();
state.heading = (state.avg_pos - first_touch.pos).angle();
@@ -323,13 +324,14 @@ enum PinchType {
impl PinchType {
fn classify(touches: &BTreeMap<TouchId, ActiveTouch>) -> Self {
#![expect(clippy::unwrap_used)]
// For non-proportional 2d zooming:
// If the user is pinching with two fingers that have roughly the same Y coord,
// then the Y zoom is unstable and should be 1.
// Similarly, if the fingers are directly above/below each other,
// we should only zoom on the Y axis.
// If the fingers are roughly on a diagonal, we revert to the proportional zooming.
if touches.len() == 2 {
let mut touches = touches.values();
let t0 = touches.next().unwrap().pos;

View File

@@ -0,0 +1,233 @@
use emath::{Rect, Vec2, vec2};
use crate::{InputOptions, Modifiers, MouseWheelUnit, TouchPhase};
/// The current state of scrolling.
///
/// There are two important types of scroll input deviced:
/// * Discreen scroll wheels on a mouse
/// * Smooth scroll input from a trackpad
///
/// Scroll wheels will usually fire one single scroll event,
/// so it is important that egui smooths it out over time.
///
/// On the contrary, trackpads usually provide smooth scroll input,
/// and with kinetic scrolling (which on Mac is implemented by the OS)
/// scroll events can arrive _after_ the user lets go of the trackpad.
///
/// In either case, we consider use to be scrolling until there is no more
/// scroll events expected.
///
/// This means there are a few different states we can be in:
/// * Not scrolling
/// * "Smooth scrolling" (low-pass filter of discreet scroll events)
/// * Trackpad-scrolling (we receive begin/end phases for these)
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Status {
/// Not scrolling,
Static,
/// We're smoothing out previous scroll events
Smoothing,
// We're in-between [`TouchPhase::Start`] and [`TouchPhase::End`] of a trackpad scroll.
InTouch,
}
/// Keeps track of wheel (scroll) input.
#[derive(Clone, Debug)]
pub struct WheelState {
/// Are we currently in a scroll action?
///
/// This may be true even if no scroll events came in this frame,
/// but we are in a kinetic scroll or in a smoothed scroll.
pub status: Status,
/// The modifiers at the start of the scroll.
pub modifiers: Modifiers,
/// Time of the last scroll event.
pub last_wheel_event: f64,
/// Used for smoothing the scroll delta.
pub unprocessed_wheel_delta: Vec2,
/// How many points the user scrolled, smoothed over a few frames.
///
/// The delta dictates how the _content_ should move.
///
/// A positive X-value indicates the content is being moved right,
/// as when swiping right on a touch-screen or track-pad with natural scrolling.
///
/// A positive Y-value indicates the content is being moved down,
/// as when swiping down on a touch-screen or track-pad with natural scrolling.
///
/// [`crate::ScrollArea`] will both read and write to this field, so that
/// at the end of the frame this will be zero if a scroll-area consumed the delta.
pub smooth_wheel_delta: Vec2,
}
impl Default for WheelState {
fn default() -> Self {
Self {
status: Status::Static,
modifiers: Default::default(),
last_wheel_event: f64::NEG_INFINITY,
unprocessed_wheel_delta: Vec2::ZERO,
smooth_wheel_delta: Vec2::ZERO,
}
}
}
impl WheelState {
#[expect(clippy::too_many_arguments)]
pub fn on_wheel_event(
&mut self,
viewport_rect: Rect,
options: &InputOptions,
time: f64,
unit: MouseWheelUnit,
delta: Vec2,
phase: TouchPhase,
latest_modifiers: Modifiers,
) {
self.last_wheel_event = time;
match phase {
crate::TouchPhase::Start => {
self.status = Status::InTouch;
self.modifiers = latest_modifiers;
}
crate::TouchPhase::Move => {
match self.status {
Status::Static | Status::Smoothing => {
self.modifiers = latest_modifiers;
self.status = Status::Smoothing;
}
Status::InTouch => {
// If the user lets go of a modifier - ignore it.
// More kinematic scrolling may arrive.
// But if the users presses down new modifiers - heed it!
self.modifiers |= latest_modifiers;
}
}
let mut delta = match unit {
MouseWheelUnit::Point => delta,
MouseWheelUnit::Line => options.line_scroll_speed * delta,
MouseWheelUnit::Page => viewport_rect.height() * delta,
};
let is_horizontal = self
.modifiers
.matches_any(options.horizontal_scroll_modifier);
let is_vertical = self.modifiers.matches_any(options.vertical_scroll_modifier);
if is_horizontal && !is_vertical {
// Treat all scrolling as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = vec2(delta.x + delta.y, 0.0);
}
if !is_horizontal && is_vertical {
// Treat all scrolling as vertical scrolling.
delta = vec2(0.0, delta.x + delta.y);
}
// Mouse wheels often go very large steps.
// A single notch on a logitech mouse wheel connected to a Macbook returns 14.0 raw scroll delta.
// So we smooth it out over several frames for a nicer user experience when scrolling in egui.
// BUT: if the user is using a nice smooth mac trackpad, we don't add smoothing,
// because it adds latency.
let is_smooth = self.status == Status::InTouch
|| match unit {
MouseWheelUnit::Point => delta.length() < 8.0, // a bit arbitrary here
MouseWheelUnit::Line | MouseWheelUnit::Page => false,
};
if is_smooth {
self.smooth_wheel_delta += delta;
} else {
self.unprocessed_wheel_delta += delta;
}
}
crate::TouchPhase::End | crate::TouchPhase::Cancel => {
self.status = Status::Static;
self.modifiers = Default::default();
self.unprocessed_wheel_delta = Default::default();
self.smooth_wheel_delta = Default::default();
}
}
}
pub fn after_events(&mut self, time: f64, dt: f32) {
let t = crate::emath::exponential_smooth_factor(0.90, 0.1, dt); // reach _% in _ seconds. TODO(emilk): parameterize
if self.unprocessed_wheel_delta != Vec2::ZERO {
for d in 0..2 {
if self.unprocessed_wheel_delta[d].abs() < 1.0 {
self.smooth_wheel_delta[d] += self.unprocessed_wheel_delta[d];
self.unprocessed_wheel_delta[d] = 0.0;
} else {
let applied = t * self.unprocessed_wheel_delta[d];
self.smooth_wheel_delta[d] += applied;
self.unprocessed_wheel_delta[d] -= applied;
}
}
}
let time_since_last_scroll = time - self.last_wheel_event;
if self.status == Status::Smoothing
&& self.smooth_wheel_delta == Vec2::ZERO
&& 0.150 < time_since_last_scroll
{
// On certain platforms, like web, we don't get the start & stop scrolling events, so
// we rely on a timer there.
//
// Tested on a mac touchpad 2025, where the largest observed gap between scroll events
// was 68 ms. But we add some margin to be safe
self.status = Status::Static;
self.modifiers = Default::default();
}
}
/// True if there is an active scroll action that might scroll more when using [`Self::smooth_wheel_delta`].
pub fn is_scrolling(&self) -> bool {
self.status != Status::Static
}
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
status,
modifiers,
last_wheel_event,
unprocessed_wheel_delta,
smooth_wheel_delta,
} = self;
let time = ui.input(|i| i.time);
crate::Grid::new("ScrollState")
.num_columns(2)
.show(ui, |ui| {
ui.label("status");
ui.monospace(format!("{status:?}"));
ui.end_row();
ui.label("modifiers");
ui.monospace(format!("{modifiers:?}"));
ui.end_row();
ui.label("last_wheel_event");
ui.monospace(format!("{:.1}s ago", time - *last_wheel_event));
ui.end_row();
ui.label("unprocessed_wheel_delta");
ui.monospace(unprocessed_wheel_delta.to_string());
ui.end_row();
ui.label("smooth_wheel_delta");
ui.monospace(smooth_wheel_delta.to_string());
ui.end_row();
});
}
}

View File

@@ -1,6 +1,6 @@
//! How mouse and touch interzcts with widgets.
use crate::{hit_test, id, input_state, memory, Id, InputState, Key, WidgetRects};
use crate::{Id, InputState, Key, WidgetRects, hit_test, id, input_state, memory};
use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState};
@@ -115,19 +115,19 @@ pub(crate) fn interact(
) -> InteractionSnapshot {
profiling::function_scope!();
if let Some(id) = interaction.potential_click_id {
if !widgets.contains(id) {
// The widget we were interested in clicking is gone.
interaction.potential_click_id = None;
}
if let Some(id) = interaction.potential_click_id
&& !widgets.contains(id)
{
// The widget we were interested in clicking is gone.
interaction.potential_click_id = None;
}
if let Some(id) = interaction.potential_drag_id {
if !widgets.contains(id) {
// The widget we were interested in dragging is gone.
// This is fine! This could be drag-and-drop,
// and the widget being dragged is now "in the air" and thus
// not registered in the new frame.
}
if let Some(id) = interaction.potential_drag_id
&& !widgets.contains(id)
{
// The widget we were interested in dragging is gone.
// This is fine! This could be drag-and-drop,
// and the widget being dragged is now "in the air" and thus
// not registered in the new frame.
}
let mut clicked = None;
@@ -172,13 +172,13 @@ pub(crate) fn interact(
}
PointerEvent::Released { click, button: _ } => {
if click.is_some() && !input.pointer.is_decidedly_dragging() {
if let Some(widget) = interaction
if click.is_some()
&& !input.pointer.is_decidedly_dragging()
&& let Some(widget) = interaction
.potential_click_id
.and_then(|id| widgets.get(id))
{
clicked = Some(widget.id);
}
{
clicked = Some(widget.id);
}
interaction.potential_drag_id = None;
@@ -190,21 +190,21 @@ pub(crate) fn interact(
if dragged.is_none() {
// Check if we started dragging something new:
if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) {
if widget.enabled {
let is_dragged = if widget.sense.senses_click() && widget.sense.senses_drag() {
// 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()
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.senses_drag()
};
if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id))
&& widget.enabled
{
let is_dragged = if widget.sense.senses_click() && widget.sense.senses_drag() {
// 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()
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.senses_drag()
};
if is_dragged {
dragged = Some(widget.id);
}
if is_dragged {
dragged = Some(widget.id);
}
}
}
@@ -293,7 +293,7 @@ pub(crate) fn interact(
drag_started,
dragged,
drag_stopped,
contains_pointer,
hovered,
contains_pointer,
}
}

View File

@@ -1,7 +1,7 @@
//! Showing UI:s for egui/epaint types.
use crate::{
epaint, memory, pos2, remap_clamp, vec2, Color32, CursorIcon, FontFamily, FontId, Label, Mesh,
NumExt, Rect, Response, Sense, Shape, Slider, TextStyle, TextWrapMode, Ui, Widget,
Color32, CursorIcon, FontFamily, FontId, Label, Mesh, NumExt as _, Rect, Response, Sense,
Shape, Slider, TextStyle, TextWrapMode, Ui, Widget, epaint, memory, pos2, remap_clamp, vec2,
};
pub fn font_family_ui(ui: &mut Ui, font_family: &mut FontFamily) {

View File

@@ -1,8 +1,8 @@
//! Handles paint layers, i.e. how things
//! are sometimes painted behind or in front of other things.
use crate::{ahash, epaint, Id, IdMap, Rect};
use epaint::{emath::TSTransform, ClippedShape, Shape};
use crate::{Id, IdMap, Rect, ahash, epaint};
use epaint::{ClippedShape, Shape, emath::TSTransform};
/// Different layer categories
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
@@ -154,7 +154,6 @@ impl PaintList {
#[inline(always)]
pub fn set(&mut self, idx: ShapeIdx, clip_rect: Rect, shape: Shape) {
if self.0.len() <= idx.0 {
#[cfg(feature = "log")]
log::warn!("Index {} is out of bounds for PaintList", idx.0);
return;
}
@@ -236,27 +235,28 @@ impl GraphicLayers {
// First do the layers part of area_order:
for layer_id in area_order {
if layer_id.order == order {
if let Some(list) = order_map.get_mut(&layer_id.id) {
if let Some(to_global) = to_global.get(layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect;
clipped_shape.shape.transform(*to_global);
}
if layer_id.order == order
&& let Some(list) = order_map.get_mut(&layer_id.id)
{
if let Some(to_global) = to_global.get(layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.transform(*to_global);
}
all_shapes.append(&mut list.0);
}
all_shapes.append(&mut list.0);
}
}
// Also draw areas that are missing in `area_order`:
// NOTE: We don't think we end up here in normal situations.
// This is just a safety net in case we have some bug somewhere.
#[expect(clippy::iter_over_hash_type)]
for (id, list) in order_map {
let layer_id = LayerId::new(order, *id);
if let Some(to_global) = to_global.get(&layer_id) {
for clipped_shape in &mut list.0 {
clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect;
clipped_shape.shape.transform(*to_global);
clipped_shape.transform(*to_global);
}
}

View File

@@ -1,8 +1,8 @@
use emath::GuiRounding as _;
use crate::{
emath::{pos2, vec2, Align2, NumExt, Pos2, Rect, Vec2},
Align,
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
};
const INFINITY: f32 = f32::INFINITY;
@@ -50,8 +50,8 @@ pub(crate) struct Region {
impl Region {
/// Expand the `min_rect` and `max_rect` of this ui to include a child at the given rect.
pub fn expand_to_include_rect(&mut self, rect: Rect) {
self.min_rect = self.min_rect.union(rect);
self.max_rect = self.max_rect.union(rect);
self.min_rect |= rect;
self.max_rect |= rect;
}
/// Ensure we are big enough to contain the given X-coordinate.
@@ -725,7 +725,7 @@ impl Layout {
if self.main_wrap {
if cursor.intersects(frame_rect.shrink(1.0)) {
// make row/column larger if necessary
*cursor = cursor.union(frame_rect);
*cursor |= frame_rect;
} else {
// this is a new row or column. We temporarily use NAN for what will be filled in later.
match self.main_dir {
@@ -753,7 +753,7 @@ impl Layout {
pos2(frame_rect.max.x, f32::NAN),
);
}
};
}
}
} else {
// Make sure we also expand where we consider adding things (the cursor):
@@ -779,7 +779,7 @@ impl Layout {
Direction::BottomUp => {
cursor.max.y = widget_rect.min.y - item_spacing.y;
}
};
}
}
/// Move to the next row in a wrapping layout.

View File

@@ -3,13 +3,13 @@
//! Try the live web demo: <https://www.egui.rs/#demo>. Read more about egui at <https://github.com/emilk/egui>.
//!
//! `egui` is in heavy development, with each new version having breaking changes.
//! You need to have rust 1.81.0 or later to use `egui`.
//! You need to have rust 1.92.0 or later to use `egui`.
//!
//! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template)
//! which uses [`eframe`](https://docs.rs/eframe).
//!
//! To create a GUI using egui you first need a [`Context`] (by convention referred to by `ctx`).
//! Then you add a [`Window`] or a [`SidePanel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need.
//! Then you add a [`Window`] or a [`Panel`] to get a [`Ui`], which is what you'll be using to add all the buttons and labels that you need.
//!
//!
//! ## Feature flags
@@ -43,23 +43,6 @@
//! In some GUI frameworks this would require defining multiple types and functions with callbacks or message handlers,
//! but thanks to `egui` being immediate mode everything is one self-contained function!
//!
//! ### Getting a [`Ui`]
//!
//! Use one of [`SidePanel`], [`TopBottomPanel`], [`CentralPanel`], [`Window`] or [`Area`] to
//! get access to an [`Ui`] where you can put widgets. For example:
//!
//! ```
//! # egui::__run_test_ctx(|ctx| {
//! egui::CentralPanel::default().show(&ctx, |ui| {
//! ui.add(egui::Label::new("Hello World!"));
//! ui.label("A shorter and more convenient way to add a label.");
//! if ui.button("Click me").clicked() {
//! // take some action here
//! }
//! });
//! # });
//! ```
//!
//! ### Quick start
//!
//! ```
@@ -143,7 +126,7 @@
//! }
//! ```
//!
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/crates/egui_glow/src/painter.rs).
//! For a reference OpenGL renderer, see [the `egui_glow` painter](https://github.com/emilk/egui/blob/main/crates/egui_glow/src/painter.rs).
//!
//!
//! ### Debugging your renderer
@@ -161,12 +144,10 @@
//!
//! * egui uses premultiplied alpha, so make sure your blending function is `(ONE, ONE_MINUS_SRC_ALPHA)`.
//! * Make sure your texture sampler is clamped (`GL_CLAMP_TO_EDGE`).
//! * egui prefers linear color spaces for all blending so:
//! * Use an sRGBA-aware texture if available (e.g. `GL_SRGB8_ALPHA8`).
//! * Otherwise: remember to decode gamma in the fragment shader.
//! * Decode the gamma of the incoming vertex colors in your vertex shader.
//! * Turn on sRGBA/linear framebuffer if available (`GL_FRAMEBUFFER_SRGB`).
//! * Otherwise: gamma-encode the colors before you write them again.
//! * egui prefers gamma color spaces for all blending so:
//! * Do NOT use an sRGBA-aware texture (NOT `GL_SRGB8_ALPHA8`).
//! * Multiply texture and vertex colors in gamma space
//! * Turn OFF sRGBA/gamma framebuffer (NO `GL_FRAMEBUFFER_SRGB`).
//!
//!
//! # Understanding immediate mode
@@ -197,7 +178,7 @@
//! * lays out the letters `click me` in order to figure out the size of the button
//! * decides where on screen to place the button
//! * check if the mouse is hovering or clicking that location
//! * chose button colors based on if it is being hovered or clicked
//! * choose button colors based on if it is being hovered or clicked
//! * add a [`Shape::Rect`] and [`Shape::Text`] to the list of shapes to be painted later this frame
//! * return a [`Response`] with the [`clicked`](`Response::clicked`) member so the user can check for interactions
//!
@@ -219,7 +200,7 @@
//! This means it is responsibility of the egui user to store the state (`value`) so that it persists between frames.
//!
//! It can be useful to read the code for the toggle switch example widget to get a better understanding
//! of how egui works: <https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//! of how egui works: <https://github.com/emilk/egui/blob/main/crates/egui_demo_lib/src/demo/toggle_switch.rs>.
//!
//! Read more about the pros and cons of immediate mode at <https://github.com/emilk/egui#why-immediate-mode>.
//!
@@ -324,7 +305,7 @@
//! when you release the panel/window shrinks again.
//! This is an artifact of immediate mode, and here are some alternatives on how to avoid it:
//!
//! 1. Turn off resizing with [`Window::resizable`], [`SidePanel::resizable`], [`TopBottomPanel::resizable`].
//! 1. Turn off resizing with [`Window::resizable`], [`Panel::resizable`].
//! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`].
//! 3. Use a justified layout:
//!
@@ -400,11 +381,15 @@
//! profile-with-puffin = ["profiling/profile-with-puffin"]
//! ```
//!
//! ## Custom allocator
//! egui apps can run significantly (~20%) faster by using a custom allocator, like [mimalloc](https://crates.io/crates/mimalloc) or [talc](https://crates.io/crates/talc).
//!
#![allow(clippy::float_cmp)]
#![allow(clippy::manual_range_contains)]
#![expect(clippy::float_cmp)]
#![expect(clippy::manual_range_contains)]
mod animation_manager;
mod atomics;
pub mod cache;
pub mod containers;
mod context;
@@ -428,6 +413,7 @@ pub mod os;
mod painter;
mod pass_state;
pub(crate) mod placer;
pub mod plugin;
pub mod response;
mod sense;
pub mod style;
@@ -438,6 +424,7 @@ mod ui_stack;
pub mod util;
pub mod viewport;
mod widget_rect;
pub mod widget_style;
pub mod widget_text;
pub mod widgets;
@@ -445,7 +432,6 @@ pub mod widgets;
#[cfg(debug_assertions)]
mod callstack;
#[cfg(feature = "accesskit")]
pub use accesskit;
#[deprecated = "Use the ahash crate directly."]
@@ -459,46 +445,47 @@ pub use epaint::emath;
pub use ecolor::hex_color;
pub use ecolor::{Color32, Rgba};
pub use emath::{
lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign,
Vec2, Vec2b,
Align, Align2, NumExt, Pos2, Rangef, Rect, RectAlign, Vec2, Vec2b, lerp, pos2, remap,
remap_clamp, vec2,
};
pub use epaint::{
mutex,
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
ClippedPrimitive, ColorImage, CornerRadius, FontImage, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId,
};
pub mod text {
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
cursor::CCursor, FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob,
LayoutSection, TextFormat, TextWrapping, TAB_SIZE,
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TAB_SIZE,
TextFormat, TextWrapping, cursor::CCursor,
};
}
pub use self::{
containers::*,
atomics::*,
containers::{menu::MenuBar, *},
context::{Context, RepaintCause, RequestRepaintInfo},
data::{
Key, UserData,
input::*,
output::{
self, CursorIcon, FullOutput, OpenUrl, OutputCommand, PlatformOutput,
UserAttentionType, WidgetInfo,
},
Key, UserData,
},
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
grid::Grid,
id::{AsId, Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState, SurrenderFocusOn},
layers::{LayerId, Order},
layout::*,
load::SizeHint,
memory::{Memory, Options, Theme, ThemePreference},
memory::{FocusDirection, Memory, Options, Theme, ThemePreference},
painter::Painter,
plugin::Plugin,
response::{InnerResponse, Response},
sense::Sense,
style::{FontSelection, Spacing, Style, TextStyle, Visuals},
@@ -507,7 +494,7 @@ pub use self::{
ui_builder::UiBuilder,
ui_stack::*,
viewport::*,
widget_rect::{WidgetRect, WidgetRects},
widget_rect::{InteractOptions, WidgetRect, WidgetRects},
widget_text::{RichText, WidgetText},
widgets::*,
};
@@ -564,7 +551,7 @@ macro_rules! include_image {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// ui.add(egui::github_link_file_line!("https://github.com/YOUR/PROJECT/blob/main/", "(source code)"));
/// # });
/// ```
#[macro_export]
@@ -579,7 +566,7 @@ macro_rules! github_link_file_line {
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/master/", "(source code)"));
/// ui.add(egui::github_link_file!("https://github.com/YOUR/PROJECT/blob/main/", "(source code)"));
/// # });
/// ```
#[macro_export]
@@ -666,16 +653,20 @@ pub enum WidgetType {
ColorButton,
ImageButton,
Image,
CollapsingHeader,
Panel,
ProgressIndicator,
Window,
ResizeHandle,
ScrollBar,
/// If you cannot fit any of the above slots.
///
/// If this is something you think should be added, file an issue.
@@ -688,8 +679,8 @@ pub enum WidgetType {
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run(Default::default(), |ctx| {
run_ui(ctx);
let _ = ctx.run_ui(Default::default(), |ui| {
run_ui(ui.ctx());
});
}
@@ -697,14 +688,11 @@ pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run(Default::default(), |ctx| {
crate::CentralPanel::default().show(ctx, |ui| {
add_contents(ui);
});
let _ = ctx.run_ui(Default::default(), |ui| {
add_contents(ui);
});
}
#[cfg(feature = "accesskit")]
pub fn accesskit_root_id() -> Id {
Id::new("accesskit_root")
}

View File

@@ -3,7 +3,7 @@
//! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/)
//! will get you up and running quickly with its reasonable default implementations of the traits described below.
//!
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature.
//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature (`cargo add egui_extras -F all_loaders`).
//! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html)
//! in your app's setup code.
//! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`].
@@ -64,8 +64,8 @@ use std::{
use ahash::HashMap;
use emath::{Float, OrderedFloat};
use epaint::{mutex::Mutex, textures::TextureOptions, ColorImage, TextureHandle, TextureId, Vec2};
use emath::{Float as _, OrderedFloat};
use epaint::{ColorImage, TextureHandle, TextureId, Vec2, mutex::Mutex, textures::TextureOptions};
use crate::Context;
@@ -143,22 +143,53 @@ pub type Result<T, E = LoadError> = std::result::Result<T, E>;
/// Given as a hint for image loading requests.
///
/// Used mostly for rendering SVG:s to a good size.
/// The size is measured in texels, with the pixels per point already factored in.
///
/// All variants will preserve the original aspect ratio.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// The [`SizeHint`] determines at what resolution the image should be rasterized.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SizeHint {
/// Scale original size by some factor.
/// Scale original size by some factor, keeping the original aspect ratio.
///
/// The original size of the image is usually its texel resolution,
/// but for an SVG it's the point size of the SVG.
///
/// For instance, setting `Scale(2.0)` will rasterize SVG:s to twice their original size,
/// which is useful for high-DPI displays.
Scale(OrderedFloat<f32>),
/// Scale to width.
/// Scale to exactly this pixel width, keeping the original aspect ratio.
Width(u32),
/// Scale to height.
/// Scale to exactly this pixel height, keeping the original aspect ratio.
Height(u32),
/// Scale to size.
Size(u32, u32),
/// Scale to this pixel size.
Size {
width: u32,
height: u32,
/// If true, the image will be as large as possible
/// while still fitting within the given width/height.
maintain_aspect_ratio: bool,
},
}
impl SizeHint {
/// Multiply size hint by a factor.
pub fn scale_by(self, factor: f32) -> Self {
match self {
Self::Scale(scale) => Self::Scale(OrderedFloat(factor * scale.0)),
Self::Width(width) => Self::Width((factor * width as f32).round() as _),
Self::Height(height) => Self::Height((factor * height as f32).round() as _),
Self::Size {
width,
height,
maintain_aspect_ratio,
} => Self::Size {
width: (factor * width as f32).round() as _,
height: (factor * height as f32).round() as _,
maintain_aspect_ratio,
},
}
}
}
impl Default for SizeHint {
@@ -168,13 +199,6 @@ impl Default for SizeHint {
}
}
impl From<Vec2> for SizeHint {
#[inline]
fn from(value: Vec2) -> Self {
Self::Size(value.x.round() as u32, value.y.round() as u32)
}
}
/// Represents a byte buffer.
///
/// This is essentially `Cow<'static, [u8]>` but with the `Owned` variant being an `Arc`.
@@ -249,12 +273,16 @@ impl Deref for Bytes {
pub enum BytesPoll {
/// Bytes are being loaded.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
/// Bytes are loaded.
Ready {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
@@ -322,12 +350,17 @@ pub trait BytesLoader {
/// Implementations may use this to perform work at the end of a frame,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
/// Returns `true` if some data is currently being loaded.
fn has_pending(&self) -> bool {
false
}
}
/// Represents an image which is currently being loaded.
@@ -339,6 +372,8 @@ pub trait BytesLoader {
pub enum ImagePoll {
/// Image is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -352,7 +387,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 {
pub trait ImageLoader: std::any::Any {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
@@ -389,18 +424,27 @@ pub trait ImageLoader {
/// Implementations may use this to perform work at the end of a pass,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
fn byte_size(&self) -> usize;
/// Returns `true` if some image is currently being loaded.
///
/// NOTE: You probably also want to check [`BytesLoader::has_pending`].
fn has_pending(&self) -> bool {
false
}
}
/// A texture with a known size.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SizedTexture {
pub id: TextureId,
/// Point size of the original SVG, or the size of the image in texels.
pub size: Vec2,
}
@@ -446,6 +490,8 @@ impl<'a> From<&'a TextureHandle> for SizedTexture {
pub enum TexturePoll {
/// Texture is loading.
Pending {
/// Point size of the image.
///
/// Set if known (e.g. from a HTTP header, or by parsing the image file header).
size: Option<Vec2>,
},
@@ -455,6 +501,7 @@ pub enum TexturePoll {
}
impl TexturePoll {
/// Point size of the original SVG, or the size of the image in texels.
#[inline]
pub fn size(&self) -> Option<Vec2> {
match self {
@@ -470,6 +517,16 @@ impl TexturePoll {
Self::Ready { texture } => Some(texture.id),
}
}
#[inline]
pub fn is_pending(&self) -> bool {
matches!(self, Self::Pending { .. })
}
#[inline]
pub fn is_ready(&self) -> bool {
matches!(self, Self::Ready { .. })
}
}
pub type TextureLoadResult = Result<TexturePoll>;
@@ -527,8 +584,8 @@ pub trait TextureLoader {
/// Implementations may use this to perform work at the end of a pass,
/// such as evicting unused entries from a cache.
fn end_pass(&self, frame_index: usize) {
let _ = frame_index;
fn end_pass(&self, pass_index: u64) {
let _ = pass_index;
}
/// If the loader caches any data, this should return the size of that cache.
@@ -552,7 +609,7 @@ impl Default for Loaders {
fn default() -> Self {
let include = Arc::new(DefaultBytesLoader::default());
Self {
bytes: Mutex::new(vec![include.clone()]),
bytes: Mutex::new(vec![Arc::clone(&include) as _]),
image: Mutex::new(Vec::new()),
// By default we only include `DefaultTextureLoader`.
texture: Mutex::new(vec![Arc::new(DefaultTextureLoader::default())]),
@@ -560,3 +617,26 @@ impl Default for Loaders {
}
}
}
impl Loaders {
/// The given pass has just ended.
pub fn end_pass(&self, pass_index: u64) {
let Self {
include,
bytes,
image,
texture,
} = self;
include.end_pass(pass_index);
for loader in bytes.lock().iter() {
loader.end_pass(pass_index);
}
for loader in image.lock().iter() {
loader.end_pass(pass_index);
}
for loader in texture.lock().iter() {
loader.end_pass(pass_index);
}
}
}

View File

@@ -1,6 +1,6 @@
use super::{
generate_loader_id, Bytes, BytesLoadResult, BytesLoader, BytesPoll, Context, Cow, HashMap,
LoadError, Mutex,
Bytes, BytesLoadResult, BytesLoader, BytesPoll, Context, Cow, HashMap, LoadError, Mutex,
generate_loader_id,
};
/// Maps URI:s to [`Bytes`], e.g. found with `include_bytes!`.
@@ -19,7 +19,6 @@ impl DefaultBytesLoader {
.or_insert_with_key(|_uri| {
let bytes: Bytes = bytes.into();
#[cfg(feature = "log")]
log::trace!("loaded {} bytes for uri {_uri:?}", bytes.len());
bytes
@@ -28,7 +27,7 @@ impl DefaultBytesLoader {
}
impl BytesLoader for DefaultBytesLoader {
fn id(&self) -> &str {
fn id(&self) -> &'static str {
generate_loader_id!(DefaultBytesLoader)
}
@@ -53,14 +52,12 @@ impl BytesLoader for DefaultBytesLoader {
}
fn forget(&self, uri: &str) {
#[cfg(feature = "log")]
log::trace!("forget {uri:?}");
self.cache.lock().remove(uri);
}
fn forget_all(&self) {
#[cfg(feature = "log")]
log::trace!("forget all");
self.cache.lock().clear();

Some files were not shown because too many files have changed in this diff Show More