1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-26 22:53:14 -04:00

Merge branch 'main' into lucas/malmal/main

Re-port the WASM canvas font fallback onto main's new skrifa/harfrust
shaping pipeline:
- Add `chr` to `ShapedGlyph` and trigger the canvas fallback from
  `allocate_glyph` on NOTDEF (WASM only).
- Drive the fallback from `text_layout`'s NOTDEF branch via `has_glyph`,
  so it only kicks in when no loaded font contains the character.
- Update `allocate_canvas_glyph` for main's `GlyphAllocation` (uv_rect
  only) and `color_transfer_function` API; fix `get_image_data` f64 args.

Merge the per-axis scroll fade gating into main's refactored
`paint_fade_areas_impl`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucasmerlin
2026-06-02 21:45:11 +02:00
359 changed files with 7078 additions and 5602 deletions

View File

@@ -1,19 +0,0 @@
name: Cargo Machete
on: [push, pull_request]
jobs:
cargo-machete:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92
- name: Machete install
## The official cargo-machete action
uses: bnjbvr/cargo-machete@v0.9.1
- name: Checkout
uses: actions/checkout@v4
- name: Machete Check
run: cargo machete

25
.github/workflows/cargo_shear.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Looks for unused crates.
name: Cargo Shear
on:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize]
jobs:
cargo-shear:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Cargo Shear
uses: taiki-e/install-action@v2.48.7
with:
tool: cargo-shear@1.11.2
- name: Run Cargo Shear
run: |
cargo shear

25
.github/workflows/taplo.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
# Checks that all TOML files are formatted with taplo.
name: Taplo
on:
push:
branches:
- "main"
pull_request:
types: [opened, synchronize]
jobs:
taplo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Taplo
uses: taiki-e/install-action@v2.48.7
with:
tool: taplo-cli@0.9.3
- name: Check TOML formatting
run: |
taplo fmt --check

View File

@@ -4,6 +4,7 @@
[default.extend-words]
ime = "ime" # Input Method Editor
abou = "abou" # part of @AmmarAbouZor username
nknown = "nknown" # part of @55nknown username
isse = "isse" # part of @IsseW username
tye = "tye" # part of @tye-exe username

View File

@@ -14,6 +14,24 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.3 - 2026-05-27
* Fix `ScrollArea::scroll_to_*` calls when `stick_to_bottom` is Active [#8033](https://github.com/emilk/egui/pull/8033) by [@AmmarAbouZor](https://github.com/AmmarAbouZor)
## 0.34.2 - 2026-05-04
### ⭐ Added
* Add regression test for O(n²) word boundary scan [#8077](https://github.com/emilk/egui/pull/8077) by [@hallyhaa](https://github.com/hallyhaa)
### 🐛 Fixed
* Fix wrong color of last glyph of selected text [#8075](https://github.com/emilk/egui/pull/8075) by [@emilk](https://github.com/emilk)
* Fix text selection of centered and right-aligned text [#8076](https://github.com/emilk/egui/pull/8076) by [@emilk](https://github.com/emilk)
* Fix `Context::is_pointer_over_egui` and `Context::egui_wants_pointer_input` [#8081](https://github.com/emilk/egui/pull/8081) by [@emilk](https://github.com/emilk)
* Fix centered & right aligned `TextEdit` [#8082](https://github.com/emilk/egui/pull/8082) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🚀 Performance
* Optimize text selection performance for large documents [#7917](https://github.com/emilk/egui/pull/7917) by [@rustbasic](https://github.com/rustbasic)
## 0.34.1 - 2026-03-27
Nothing new

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ members = [
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.92"
version = "0.34.1"
version = "0.34.3"
[profile.release]
@@ -55,18 +55,18 @@ opt-level = 2
[workspace.dependencies]
emath = { version = "0.34.1", path = "crates/emath", default-features = false }
ecolor = { version = "0.34.1", path = "crates/ecolor", default-features = false }
epaint = { version = "0.34.1", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.34.1", path = "crates/epaint_default_fonts" }
egui = { version = "0.34.1", path = "crates/egui", default-features = false }
egui-winit = { version = "0.34.1", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.34.1", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.34.1", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.34.1", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.34.1", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.34.1", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.34.1", path = "crates/eframe", default-features = false }
emath = { version = "0.34.3", path = "crates/emath", default-features = false }
ecolor = { version = "0.34.3", path = "crates/ecolor", default-features = false }
epaint = { version = "0.34.3", path = "crates/epaint", default-features = false }
epaint_default_fonts = { version = "0.34.3", path = "crates/epaint_default_fonts" }
egui = { version = "0.34.3", path = "crates/egui", default-features = false }
egui-winit = { version = "0.34.3", path = "crates/egui-winit", default-features = false }
egui_extras = { version = "0.34.3", path = "crates/egui_extras", default-features = false }
egui-wgpu = { version = "0.34.3", path = "crates/egui-wgpu", default-features = false }
egui_demo_lib = { version = "0.34.3", path = "crates/egui_demo_lib", default-features = false }
egui_glow = { version = "0.34.3", path = "crates/egui_glow", default-features = false }
egui_kittest = { version = "0.34.3", path = "crates/egui_kittest", default-features = false }
eframe = { version = "0.34.3", path = "crates/eframe", default-features = false }
accesskit = "0.24.0"
accesskit_consumer = "0.35.0"
@@ -82,7 +82,7 @@ bitflags = "2.9.4"
bytemuck = "1.24.0"
cint = "0.3.1"
color-hex = "0.2.0"
criterion = { version = "0.7.0", default-features = false }
criterion = { version = "0.8.2", default-features = false }
dify = { version = "0.8", default-features = false }
directories = "6.0.0"
document-features = "0.2.11"
@@ -93,8 +93,10 @@ font-types = { version = "0.11.0", default-features = false, features = ["std"]
glow = "0.17.0"
glutin = { version = "0.32.3", default-features = false }
glutin-winit = { version = "0.5.0", default-features = false }
harfrust = "0.7.0"
home = "0.5.9"
image = { version = "0.25.6", default-features = false }
itertools = "0.14.0"
jiff = { version = "0.2.23", default-features = false }
js-sys = "0.3.77"
kittest = { version = "0.4.0" }
@@ -113,9 +115,9 @@ parking_lot = "0.12.5"
percent-encoding = "2.3.2"
poll-promise = { version = "0.3.0", default-features = false }
pollster = "0.4.0"
profiling = { version = "1.0.17", default-features = false }
puffin = "0.19.1"
puffin_http = "0.16.1"
profiling = { version = "1.0.18", default-features = false }
puffin = "0.20.0"
puffin_http = "0.17.0"
rand = "0.9.2"
raw-window-handle = "0.6.2"
rayon = "1.11.0"
@@ -125,7 +127,7 @@ ron = "0.12.0"
self_cell = "1.2.1"
serde = { version = "1.0.228", features = ["derive"] }
similar-asserts = "1.7.0"
skrifa = { version = "0.40.0", default-features = false, features = ["std", "autohint_shaping"] }
skrifa = { version = "0.42.1", default-features = false, features = ["std", "autohint_shaping"] }
smallvec = "1.15.1"
smithay-clipboard = "0.7.2"
static_assertions = "1.1.0"
@@ -133,18 +135,23 @@ syntect = { version = "5.3.0", default-features = false }
tempfile = "3.23.0"
thiserror = "2.0.17"
tokio = "1.49"
toml = {version = "1.0.0", default-features = false }
toml = { version = "1.0.0", default-features = false }
type-map = "0.5.1"
unicode_names2 = { version = "2.0.0", default-features = false }
unicode-general-category = "1.1.0"
unicode-segmentation = "1.12.0"
vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] }
vello_cpu = { version = "0.0.8", default-features = false, features = [
"std",
"u8_pipeline",
"f32_pipeline",
] }
wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766
wasm-bindgen-futures = "0.4.58"
wayland-cursor = { version = "0.31.11", default-features = false }
web-sys = "0.3.77"
web-time = "1.1.0" # Timekeeping for native and web
webbrowser = "1.0.5"
wgpu = { version = "29.0.0", default-features = false, features = ["std"] }
wgpu = { version = "29.0.1", default-features = false, features = ["std"] }
windows-sys = "0.61.2"
winit = { version = "0.30.13", default-features = false }
@@ -218,10 +225,12 @@ flat_map_option = "warn"
float_cmp_const = "warn"
fn_params_excessive_bools = "warn"
fn_to_numeric_cast_any = "warn"
format_push_string = "warn"
from_iter_instead_of_collect = "warn"
get_unwrap = "warn"
if_let_mutex = "warn"
ignore_without_reason = "warn"
ignored_unit_patterns = "warn"
implicit_clone = "warn"
implied_bounds_in_impls = "warn"
imprecise_flops = "warn"
@@ -270,6 +279,7 @@ mismatching_type_param_order = "warn"
missing_assert_message = "warn"
missing_enforced_import_renames = "warn"
missing_errors_doc = "warn"
missing_fields_in_debug = "warn"
missing_safety_doc = "warn"
mixed_attributes_style = "warn"
mut_mut = "warn"
@@ -279,6 +289,7 @@ needless_continue = "warn"
needless_for_each = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
needless_raw_string_hashes = "warn"
negative_feature_names = "warn"
non_std_lazy_statics = "warn"
non_zero_suggestions = "warn"
@@ -299,6 +310,7 @@ rc_mutex = "warn"
readonly_write_lock = "warn"
redundant_type_annotations = "warn"
ref_as_ptr = "warn"
ref_option = "warn"
ref_option_ref = "warn"
ref_patterns = "warn"
rest_pat_in_fully_bound_structs = "warn"

View File

@@ -6,6 +6,14 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.3 - 2026-05-27
Nothing new
## 0.34.2 - 2026-05-04
Nothing new
## 0.34.1 - 2026-03-27
Nothing new

View File

@@ -489,7 +489,7 @@ mod test {
} else {
// There will be small rounding errors whenever the alpha is not 0 or 255,
// because we multiply and then unmultiply the alpha.
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) {
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
}
}

View File

@@ -336,7 +336,7 @@ mod test {
} else {
// There will be small rounding errors whenever the alpha is not 0 or 255,
// because we multiply and then unmultiply the alpha.
for (&a, &b) in in_rgba.iter().zip(out_rgba.iter()) {
for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) {
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
}
}

View File

@@ -7,6 +7,15 @@ 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.34.3 - 2026-05-27
* Default `app_id` to `app_name` on native [#8172](https://github.com/emilk/egui/pull/8172) by [@grtlr](https://github.com/grtlr)
* Add winit window access to `eframe::Frame` and `CreationContext` [#8205](https://github.com/emilk/egui/pull/8205) by [@emilk](https://github.com/emilk)
## 0.34.2 - 2026-05-04
* Document glow-only fields in `NativeOptions` [#8104](https://github.com/emilk/egui/pull/8104) by [@emilk](https://github.com/emilk)
## 0.34.1 - 2026-03-27
* `wgpu` backend: Enable WebGL fallback [#8038](https://github.com/emilk/egui/pull/8038) by [@emilk](https://github.com/emilk)
* Only apply cursor style to the `<canvas>` [#8036](https://github.com/emilk/egui/pull/8036) by [@mkeeter](https://github.com/mkeeter)

View File

@@ -2,7 +2,7 @@
//!
//! `epi` provides interfaces for window management and serialization.
//!
//! Start by looking at the [`App`] trait, and implement [`App::update`].
//! Start by looking at the [`App`] trait, and implement [`App::ui`].
#![warn(missing_docs)] // Let's keep `epi` well-documented.
@@ -83,6 +83,10 @@ pub struct CreationContext<'s> {
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
/// The root [`winit::window::Window`].
#[cfg(not(target_arch = "wasm32"))]
pub(crate) window: Option<std::sync::Arc<winit::window::Window>>,
/// Raw platform window handle
#[cfg(not(target_arch = "wasm32"))]
pub(crate) raw_window_handle: Result<RawWindowHandle, HandleError>,
@@ -125,11 +129,21 @@ impl CreationContext<'_> {
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
#[cfg(not(target_arch = "wasm32"))]
window: None,
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
#[cfg(not(target_arch = "wasm32"))]
raw_display_handle: Err(HandleError::NotSupported),
}
}
/// Access to the root [`winit::window::Window`].
///
/// `None` for headless (tests etc).
#[cfg(not(target_arch = "wasm32"))]
pub fn winit_window(&self) -> Option<&std::sync::Arc<winit::window::Window>> {
self.window.as_ref()
}
}
// ----------------------------------------------------------------------------
@@ -161,22 +175,6 @@ pub trait App {
/// (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.
///
/// 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).
#[deprecated = "Use Self::ui instead"]
fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) {
_ = (ctx, frame);
}
/// Get a handle to the app.
///
/// Can be used from web to interact or other external context.
@@ -256,7 +254,7 @@ pub trait App {
true
}
/// A hook for manipulating or filtering raw input before it is processed by [`Self::update`].
/// A hook for manipulating or filtering raw input before it is processed by [`Self::ui`].
///
/// This function provides a way to modify or filter input events before they are processed by egui.
///
@@ -275,22 +273,6 @@ pub trait App {
fn raw_input_hook(&mut self, _ctx: &egui::Context, _raw_input: &mut egui::RawInput) {}
}
/// Selects the level of hardware graphics acceleration.
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HardwareAcceleration {
/// Require graphics acceleration.
Required,
/// Prefer graphics acceleration, but fall back to software.
Preferred,
/// Do NOT use graphics acceleration.
///
/// On some platforms (macOS) this is ignored and treated the same as [`Self::Preferred`].
Off,
}
/// Options controlling the behavior of a native window.
///
/// Additional windows can be opened using (egui viewports)[`egui::viewport`].
@@ -314,11 +296,6 @@ pub struct NativeOptions {
/// To avoid this, set the icon to [`egui::IconData::default`].
pub viewport: egui::ViewportBuilder,
/// Turn on vertical syncing, limiting the FPS to the display refresh rate.
///
/// The default is `true`.
pub vsync: bool,
/// Set the level of the multisampling anti-aliasing (MSAA).
///
/// Must be a power-of-two. Higher = more smooth 3D.
@@ -340,11 +317,6 @@ pub struct NativeOptions {
/// `egui` doesn't need the stencil buffer, so the default value is 0.
pub stencil_buffer: u8,
/// Specify whether or not hardware acceleration is preferred, required, or not.
///
/// Default: [`HardwareAcceleration::Preferred`].
pub hardware_acceleration: HardwareAcceleration,
/// What rendering backend to use.
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub renderer: Renderer,
@@ -381,13 +353,6 @@ pub struct NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub window_builder: Option<WindowBuilderHook>,
#[cfg(feature = "glow")]
/// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture.
/// See <https://github.com/emilk/egui/pull/1993>.
///
/// For OpenGL ES 2.0: set this to [`egui_glow::ShaderVersion::Es100`] to solve blank texture problem (by using the "fallback shader").
pub shader_version: Option<egui_glow::ShaderVersion>,
/// On desktop: make the window position to be centered at initialization.
///
/// Platform specific:
@@ -395,6 +360,10 @@ pub struct NativeOptions {
/// Wayland desktop currently not supported.
pub centered: bool,
/// Configures glow instance.
#[cfg(feature = "glow")]
pub glow_options: egui_glow::GlowConfiguration,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
@@ -439,6 +408,9 @@ impl Clone for NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None, // Skip any builder callbacks if cloning
#[cfg(feature = "glow")]
glow_options: self.glow_options.clone(),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: self.wgpu_options.clone(),
@@ -458,11 +430,9 @@ impl Default for NativeOptions {
Self {
viewport: Default::default(),
vsync: true,
multisampling: 0,
depth_buffer: 0,
stencil_buffer: 0,
hardware_acceleration: HardwareAcceleration::Preferred,
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
renderer: Renderer::default(),
@@ -475,13 +445,14 @@ impl Default for NativeOptions {
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
window_builder: None,
#[cfg(feature = "glow")]
shader_version: None,
centered: false,
#[cfg(feature = "glow")]
glow_options: egui_glow::GlowConfiguration::default(),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
wgpu_options: egui_wgpu::WgpuConfiguration::default()
.with_surface_config(egui_wgpu::SurfaceConfig::LOW_LATENCY),
persist_window: true,
@@ -516,6 +487,10 @@ pub struct WebOptions {
#[cfg(feature = "glow")]
pub webgl_context_option: WebGlContextOption,
/// Configures glow instance.
#[cfg(feature = "glow")]
pub glow_options: egui_glow::GlowConfiguration,
/// Configures wgpu instance/device/adapter/surface creation and renderloop.
#[cfg(feature = "wgpu_no_default_features")]
pub wgpu_options: egui_wgpu::WgpuConfiguration,
@@ -560,6 +535,9 @@ impl Default for WebOptions {
#[cfg(feature = "glow")]
webgl_context_option: WebGlContextOption::BestFirst,
#[cfg(feature = "glow")]
glow_options: egui_glow::GlowConfiguration::default(),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_options: egui_wgpu::WgpuConfiguration::default(),
@@ -695,6 +673,10 @@ pub struct Frame {
#[doc(hidden)]
pub wgpu_render_state: Option<egui_wgpu::RenderState>,
/// The current [`winit::window::Window`] (i.e. the one the active viewport is rendered to).
#[cfg(not(target_arch = "wasm32"))]
pub(crate) window: Option<std::sync::Arc<winit::window::Window>>,
/// Raw platform window handle
#[cfg(not(target_arch = "wasm32"))]
pub(crate) raw_window_handle: Result<RawWindowHandle, HandleError>,
@@ -740,6 +722,8 @@ impl Frame {
raw_display_handle: Err(HandleError::NotSupported),
#[cfg(not(target_arch = "wasm32"))]
raw_window_handle: Err(HandleError::NotSupported),
#[cfg(not(target_arch = "wasm32"))]
window: None,
storage: None,
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
@@ -769,6 +753,14 @@ impl Frame {
self.storage.as_deref_mut()
}
/// Access to the current [`winit::window::Window`] (i.e. the one the active viewport is rendered to).
///
/// `None` for headless (tests etc).
#[cfg(not(target_arch = "wasm32"))]
pub fn winit_window(&self) -> Option<&std::sync::Arc<winit::window::Window>> {
self.window.as_ref()
}
/// A reference to the underlying [`glow`] (OpenGL) context.
///
/// This can be used, for instance, to:
@@ -776,7 +768,7 @@ impl Frame {
/// * Read the pixel buffer from the previous frame (`glow::Context::read_pixels`).
/// * Render things behind the egui windows.
///
/// Note that all egui painting is deferred to after the call to [`App::update`]
/// Note that all egui painting is deferred to after the call to [`App::ui`]
/// ([`egui`] only collects [`egui::Shape`]s and then eframe paints them all in one go later on).
///
/// To get a [`glow`] context you need to compile with the `glow` feature flag,
@@ -805,6 +797,28 @@ impl Frame {
pub fn wgpu_render_state(&self) -> Option<&egui_wgpu::RenderState> {
self.wgpu_render_state.as_ref()
}
/// The currently-applied runtime surface config (present mode, frame latency)
/// used by the `wgpu` renderer, if any.
///
/// Returns `None` when not using the `wgpu` backend.
#[cfg(feature = "wgpu_no_default_features")]
pub fn wgpu_surface_config(&self) -> Option<egui_wgpu::SurfaceConfig> {
self.wgpu_render_state
.as_ref()
.map(|state| state.surface_config)
}
/// Set the runtime surface config (present mode, frame latency) for the `wgpu`
/// renderer. The surface is reconfigured on the next paint.
///
/// No-op when not using the `wgpu` backend.
#[cfg(feature = "wgpu_no_default_features")]
pub fn set_wgpu_surface_config(&mut self, config: egui_wgpu::SurfaceConfig) {
if let Some(state) = &mut self.wgpu_render_state {
state.surface_config = config;
}
}
}
/// Information about the web environment (if applicable).
@@ -882,7 +896,7 @@ pub struct IntegrationInfo {
/// Seconds of cpu usage (in seconds) on the previous frame.
///
/// This includes [`App::update`] as well as rendering (except for vsync waiting).
/// This includes [`App::ui`] as well as rendering (except for vsync waiting).
///
/// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling).
///

View File

@@ -6,7 +6,7 @@
//! 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
//! In short, you implement [`App`] (especially [`App::ui`]) and then
//! call [`crate::run_native`] from your `main.rs`, and/or use `eframe::WebRunner` from your `lib.rs`.
//!
//! ## Compiling for web
@@ -19,7 +19,7 @@
//!
//! ## Simplified usage
//! If your app is only for native, and you don't need advanced features like state persistence,
//! then you can use the simpler function [`run_simple_native`].
//! then you can use the simpler function [`run_ui_native`].
//!
//! ## Usage, native:
//! ``` no_run
@@ -45,7 +45,7 @@
//!
//! impl eframe::App for MyEguiApp {
//! fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
//! egui::CentralPanel::default().show_inside(ui, |ui| {
//! egui::CentralPanel::default().show(ui, |ui| {
//! ui.heading("Hello World!");
//! });
//! }
@@ -159,7 +159,7 @@ pub use {egui, egui::emath, egui::epaint};
pub use {egui_glow, glow};
#[cfg(feature = "wgpu_no_default_features")]
pub use {egui_wgpu, egui_wgpu::wgpu};
pub use {egui_wgpu, egui_wgpu::SurfaceConfig, egui_wgpu::WgpuConfiguration, egui_wgpu::wgpu};
mod epi;
@@ -244,7 +244,7 @@ pub mod icon_data;
///
/// impl eframe::App for MyEguiApp {
/// fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show_inside(ui, |ui| {
/// egui::CentralPanel::default().show(ui, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
@@ -257,8 +257,27 @@ pub mod icon_data;
#[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,
native_options: NativeOptions,
app_creator: AppCreator<'_>,
) -> Result {
run_native_ext(app_name, native_options, None, app_creator)
}
/// Like [`run_native`], but lets you supply a pre-existing [`egui::Context`].
///
/// If `egui_ctx` is `Some`, that context will be used by eframe instead of creating a fresh one.
/// If it is `None`, eframe creates a new context (same behavior as [`run_native`]).
///
/// # 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"))]
#[allow(clippy::allow_attributes, clippy::needless_pass_by_value)]
pub fn run_native_ext(
app_name: &str,
mut native_options: NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: AppCreator<'_>,
) -> Result {
let renderer = init_native(app_name, &mut native_options);
@@ -267,13 +286,13 @@ pub fn run_native(
#[cfg(feature = "glow")]
Renderer::Glow => {
log::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator)
native::run::run_glow(app_name, native_options, egui_ctx, 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)
native::run::run_wgpu(app_name, native_options, egui_ctx, app_creator)
}
}
}
@@ -315,7 +334,7 @@ pub fn run_native(
///
/// impl eframe::App for MyEguiApp {
/// fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
/// egui::CentralPanel::default().show_inside(ui, |ui| {
/// egui::CentralPanel::default().show(ui, |ui| {
/// ui.heading("Hello World!");
/// });
/// }
@@ -370,6 +389,9 @@ fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
if native_options.viewport.title.is_none() {
native_options.viewport.title = Some(app_name.to_owned());
}
if native_options.viewport.app_id.is_none() {
native_options.viewport.app_id = Some(app_name.to_owned());
}
let renderer = native_options.renderer;
@@ -403,7 +425,7 @@ fn init_native(app_name: &str, native_options: &mut NativeOptions) -> Renderer {
/// 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| {
/// egui::CentralPanel::default().show(ui, |ui| {
/// ui.heading("My egui Application");
/// ui.horizontal(|ui| {
/// let name_label = ui.label("Your name: ");
@@ -446,67 +468,6 @@ pub fn run_ui_native(
)
}
/// 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_simple_native("My egui App", options, move |ctx, _frame| {
/// egui::CentralPanel::default().show(ctx, |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.
#[deprecated = "Use run_ui_native instead"]
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub fn run_simple_native(
app_name: &str,
native_options: NativeOptions,
update_fun: impl FnMut(&egui::Context, &mut Frame) + 'static,
) -> Result {
struct SimpleApp<U> {
update_fun: U,
}
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);
}
}
run_native(
app_name,
native_options,
Box::new(|_cc| Ok(Box::new(SimpleApp { update_fun }))),
)
}
// ----------------------------------------------------------------------------
/// The different problems that can occur when trying to run `eframe`.

View File

@@ -2,7 +2,7 @@
use web_time::Instant;
use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};
use winit::event_loop::ActiveEventLoop;
use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
@@ -171,7 +171,7 @@ impl EpiIntegration {
#[allow(clippy::allow_attributes, clippy::too_many_arguments)]
pub fn new(
egui_ctx: egui::Context,
window: &winit::window::Window,
window: &Arc<winit::window::Window>,
app_name: &str,
native_options: &crate::NativeOptions,
storage: Option<Box<dyn epi::Storage>>,
@@ -192,6 +192,7 @@ impl EpiIntegration {
glow_register_native_texture,
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state,
window: Some(Arc::clone(window)),
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
};
@@ -214,15 +215,17 @@ impl EpiIntegration {
Self {
frame,
last_auto_save: Instant::now(),
egui_ctx,
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
#[cfg(feature = "persistence")]
persist_window: native_options.persist_window,
app_icon_setter,
beginning: Instant::now(),
beginning: Instant::now()
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
.unwrap_or_else(Instant::now),
is_first_frame: true,
egui_ctx,
}
}
@@ -259,7 +262,7 @@ impl EpiIntegration {
/// Run user code - this can create immediate viewports, so hold no locks over this!
///
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`].
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
pub fn update(
&mut self,
app: &mut dyn epi::App,
@@ -287,12 +290,6 @@ impl EpiIntegration {
}
if is_visible {
{
profiling::scope!("App::update");
#[expect(deprecated)]
app.update(ui.ctx(), &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);

View File

@@ -207,7 +207,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap<String, String>) {
profiling::scope!("ron::serialize");
if let Err(err) = ron::Options::default()
.to_io_writer_pretty(&mut writer, &kv, config)
.and_then(|_| writer.flush().map_err(|err| err.into()))
.and_then(|()| writer.flush().map_err(|err| err.into()))
{
log::warn!("Failed to serialize app state: {err}");
} else {

View File

@@ -55,6 +55,10 @@ pub struct GlowWinitApp<'app> {
// re-initializing the `GlowWinitRunning` state on Android if the application
// suspends and resumes.
app_creator: Option<AppCreator<'app>>,
/// An optional pre-existing egui context. If `Some`, it is used instead of
/// creating a new one via [`create_egui_context`]. Taken during initialization.
egui_ctx: Option<egui::Context>,
}
/// State that is initialized when the application is first starts running via
@@ -128,6 +132,7 @@ impl<'app> GlowWinitApp<'app> {
event_loop: &EventLoop<UserEvent>,
app_name: &str,
native_options: NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: AppCreator<'app>,
) -> Self {
profiling::function_scope!();
@@ -137,6 +142,7 @@ impl<'app> GlowWinitApp<'app> {
native_options,
running: None,
app_creator: Some(app_creator),
egui_ctx,
}
}
@@ -184,7 +190,7 @@ impl<'app> GlowWinitApp<'app> {
let painter = egui_glow::Painter::new(
gl,
"",
native_options.shader_version,
native_options.glow_options.shader_version,
native_options.dithering,
)?;
@@ -209,7 +215,10 @@ impl<'app> GlowWinitApp<'app> {
)
};
let egui_ctx = create_egui_context(storage.as_deref());
let egui_ctx = self
.egui_ctx
.take()
.unwrap_or_else(|| create_egui_context(storage.as_deref()));
let (mut glutin, painter) = Self::create_glutin_windowed_context(
&egui_ctx,
@@ -305,6 +314,7 @@ impl<'app> GlowWinitApp<'app> {
get_proc_address: Some(Arc::new(get_proc_address)),
#[cfg(feature = "wgpu_no_default_features")]
wgpu_render_state: None,
window: Some(Arc::clone(&window)),
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
};
@@ -689,7 +699,7 @@ impl GlowWinitRunning<'_> {
let gl_surface = viewport.gl_surface.as_ref().unwrap();
let egui_winit = viewport.egui_winit.as_mut().unwrap();
egui_winit.handle_platform_output(&window, platform_output);
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
@@ -971,12 +981,12 @@ impl GlutinWindowContext {
use glutin::prelude::*;
// convert native options to glutin options
let hardware_acceleration = match native_options.hardware_acceleration {
crate::HardwareAcceleration::Required => Some(true),
crate::HardwareAcceleration::Preferred => None,
crate::HardwareAcceleration::Off => Some(false),
let hardware_acceleration = match native_options.glow_options.hardware_acceleration {
egui_glow::HardwareAcceleration::Required => Some(true),
egui_glow::HardwareAcceleration::Preferred => None,
egui_glow::HardwareAcceleration::Off => Some(false),
};
let swap_interval = if native_options.vsync {
let swap_interval = if native_options.glow_options.vsync {
glutin::surface::SwapInterval::Wait(NonZeroU32::MIN)
} else {
glutin::surface::SwapInterval::DontWait

View File

@@ -399,6 +399,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, winit_app: impl WinitApp) -> R
pub fn run_glow(
app_name: &str,
mut native_options: epi::NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: epi::AppCreator<'_>,
) -> Result {
use super::glow_integration::GlowWinitApp;
@@ -406,13 +407,15 @@ pub fn run_glow(
#[cfg(not(target_os = "ios"))]
if native_options.run_and_return {
return with_event_loop(native_options, |event_loop, native_options| {
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
let glow_eframe =
GlowWinitApp::new(event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_return(event_loop, glow_eframe)
})?;
}
let event_loop = create_event_loop(&mut native_options)?;
let glow_eframe = GlowWinitApp::new(&event_loop, app_name, native_options, app_creator);
let glow_eframe =
GlowWinitApp::new(&event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_exit(event_loop, glow_eframe)
}
@@ -425,7 +428,7 @@ pub fn create_glow<'a>(
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::glow_integration::GlowWinitApp;
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
let glow_eframe = GlowWinitApp::new(event_loop, app_name, native_options, None, app_creator);
WinitAppWrapper::new(glow_eframe, true)
}
@@ -435,6 +438,7 @@ pub fn create_glow<'a>(
pub fn run_wgpu(
app_name: &str,
mut native_options: epi::NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: epi::AppCreator<'_>,
) -> Result {
use super::wgpu_integration::WgpuWinitApp;
@@ -442,13 +446,15 @@ pub fn run_wgpu(
#[cfg(not(target_os = "ios"))]
if native_options.run_and_return {
return with_event_loop(native_options, |event_loop, native_options| {
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
let wgpu_eframe =
WgpuWinitApp::new(event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_return(event_loop, wgpu_eframe)
})?;
}
let event_loop = create_event_loop(&mut native_options)?;
let wgpu_eframe = WgpuWinitApp::new(&event_loop, app_name, native_options, app_creator);
let wgpu_eframe =
WgpuWinitApp::new(&event_loop, app_name, native_options, egui_ctx, app_creator);
run_and_exit(event_loop, wgpu_eframe)
}
@@ -461,7 +467,7 @@ pub fn create_wgpu<'a>(
) -> impl ApplicationHandler<UserEvent> + 'a {
use super::wgpu_integration::WgpuWinitApp;
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
let wgpu_eframe = WgpuWinitApp::new(event_loop, app_name, native_options, None, app_creator);
WinitAppWrapper::new(wgpu_eframe, true)
}

View File

@@ -48,6 +48,10 @@ pub struct WgpuWinitApp<'app> {
/// Set when we are actually up and running.
running: Option<WgpuWinitRunning<'app>>,
/// An optional pre-existing egui context. If `Some`, it is used instead of
/// creating a new one via [`winit_integration::create_egui_context`]. Taken during initialization.
egui_ctx: Option<egui::Context>,
}
/// State that is initialized when the application is first starts running via
@@ -105,6 +109,7 @@ impl<'app> WgpuWinitApp<'app> {
event_loop: &EventLoop<UserEvent>,
app_name: &str,
native_options: NativeOptions,
egui_ctx: Option<egui::Context>,
app_creator: AppCreator<'app>,
) -> Self {
profiling::function_scope!();
@@ -121,6 +126,7 @@ impl<'app> WgpuWinitApp<'app> {
native_options,
running: None,
app_creator: Some(app_creator),
egui_ctx,
}
}
@@ -294,6 +300,7 @@ impl<'app> WgpuWinitApp<'app> {
#[cfg(feature = "glow")]
get_proc_address: None,
wgpu_render_state,
window: Some(Arc::clone(&window)),
raw_display_handle: window.display_handle().map(|h| h.as_raw()),
raw_window_handle: window.window_handle().map(|h| h.as_raw()),
};
@@ -403,7 +410,7 @@ impl WinitApp for WgpuWinitApp<'_> {
self.initialized_all_windows(event_loop);
if let Some(running) = &mut self.running {
running.run_ui_and_paint(window_id)
running.run_ui_and_paint(window_id, event_loop)
} else {
Ok(EventResult::Wait)
}
@@ -428,7 +435,10 @@ impl WinitApp for WgpuWinitApp<'_> {
.unwrap_or(&self.app_name),
)
};
let egui_ctx = winit_integration::create_egui_context(storage.as_deref());
let egui_ctx = self
.egui_ctx
.take()
.unwrap_or_else(|| winit_integration::create_egui_context(storage.as_deref()));
let (window, builder) = create_window(
&egui_ctx,
event_loop,
@@ -580,7 +590,11 @@ impl WgpuWinitRunning<'_> {
}
/// This is called both for the root viewport, and all deferred viewports
fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result<EventResult> {
fn run_ui_and_paint(
&mut self,
window_id: WindowId,
event_loop: &ActiveEventLoop,
) -> Result<EventResult> {
profiling::function_scope!();
let Some(viewport_id) = self
@@ -721,7 +735,7 @@ impl WgpuWinitRunning<'_> {
return Ok(EventResult::Wait);
};
egui_winit.handle_platform_output(window, platform_output);
egui_winit.handle_platform_output_with_event_loop(window, event_loop, platform_output);
let vsync_secs = if is_visible {
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
@@ -742,6 +756,7 @@ impl WgpuWinitRunning<'_> {
&clipped_primitives,
&textures_delta,
screenshot_commands,
window,
);
for action in viewport.actions_requested.drain(..) {
@@ -1131,6 +1146,7 @@ fn render_immediate_viewport(
&clipped_primitives,
&textures_delta,
vec![],
window,
);
egui_winit.handle_platform_output(window, platform_output);

View File

@@ -284,9 +284,6 @@ impl AppRunner {
self.app.logic(ui.ctx(), &mut self.frame);
if is_visible {
#[expect(deprecated)]
self.app.update(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame);
}
});
@@ -371,7 +368,8 @@ impl AppRunner {
let egui::PlatformOutput {
commands,
cursor_icon,
events: _, // already handled
cursor_image: _, // TODO(alextournai): support custom bitmap cursors on the web (via CSS `url(...)`)
events: _, // already handled
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
ime,
accesskit_update: _, // not currently implemented

View File

@@ -1114,16 +1114,16 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32
} else if JsValue::from_str("contentBoxSize").js_in(entry.as_ref()) {
let content_box_size = entry.content_box_size();
let idx0 = content_box_size.at(0);
if !idx0.is_undefined() {
let size: web_sys::ResizeObserverSize = idx0.dyn_into()?;
width = size.inline_size();
height = size.block_size();
} else {
if idx0.is_undefined() {
// legacy
let size = JsValue::clone(content_box_size.as_ref());
let size: web_sys::ResizeObserverSize = size.dyn_into()?;
width = size.inline_size();
height = size.block_size();
} else {
let size: web_sys::ResizeObserverSize = idx0.dyn_into()?;
width = size.inline_size();
height = size.block_size();
}
if DEBUG_RESIZE {
log::info!("contentBoxSize {width}x{height}");

View File

@@ -31,11 +31,12 @@ pub fn primary_touch_pos(
runner: &mut AppRunner,
event: &web_sys::TouchEvent,
) -> Option<(egui::Pos2, web_sys::Touch)> {
let all_touches: Vec<_> = (0..event.touches().length())
.filter_map(|i| event.touches().get(i))
// On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
.chain((0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)))
.collect();
// On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
let all_touches: Vec<_> = std::iter::chain(
(0..event.touches().length()).filter_map(|i| event.touches().get(i)),
(0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)),
)
.collect();
if let Some(primary_touch) = runner.input.primary_touch {
// Is the primary touch is gone?

View File

@@ -9,7 +9,19 @@ pub fn local_storage_get(key: &str) -> Option<String> {
/// Write data to local storage.
pub fn local_storage_set(key: &str, value: &str) {
local_storage().map(|storage| storage.set_item(key, value));
match local_storage() {
Some(storage) => {
if let Err(err) = storage.set_item(key, value) {
log::warn!(
"local_storage_set failed: key={key}, err={}",
crate::web::string_from_js_value(&err)
);
}
}
None => {
log::warn!("local_storage unavailable");
}
}
}
#[cfg(feature = "persistence")]

View File

@@ -56,8 +56,13 @@ impl TextAgent {
let input = input.clone();
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
let text = input.value();
// Fix android virtual keyboard Gboard
// This removes the virtual keyboard's suggestion.
// Workaround for an Android Gboard issue: after typing a word,
// the user has to delete invisible characters (whose count
// matches the length of the current suggestion) before actual
// characters are deleted, unless the focus has been reset.
//
// this issue appears to have been fixed in Gboard sometime
// between versions 14.7.09 and 17.0.12.
if !event.is_composing() {
input.blur().ok();
input.focus().ok();
@@ -75,11 +80,7 @@ impl TextAgent {
};
let on_composition_start = {
let input = input.clone();
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
input.set_value("");
let event = egui::Event::Ime(egui::ImeEvent::Enabled);
runner.input.raw.events.push(event);
// Repaint moves the text agent into place,
// see `move_to` in `AppRunner::handle_platform_output`.
runner.needs_repaint.repaint_asap();
@@ -136,6 +137,12 @@ impl TextAgent {
let Some(ime) = ime else { return Ok(()) };
if ime.should_interrupt_composition {
// no-op for now: currently, the text agent is sizeless, so any
// click shifts focus to the canvas, which naturally interrupts the
// composition.
}
let mut canvas_rect = super::canvas_content_rect(canvas);
// Fix for safari with virtual keyboard flapping position
if is_mobile_safari() {

View File

@@ -31,8 +31,13 @@ impl WebPainterGlow {
#[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)
.map_err(|err| format!("Error starting glow painter: {err}"))?;
let painter = egui_glow::Painter::new(
gl,
shader_prefix,
options.glow_options.shader_version,
options.dithering,
)
.map_err(|err| format!("Error starting glow painter: {err}"))?;
Ok(Self {
canvas,

View File

@@ -12,6 +12,7 @@ use super::web_painter::WebPainter;
pub(crate) struct WebPainterWgpu {
canvas: HtmlCanvasElement,
instance: wgpu::Instance,
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
@@ -23,6 +24,7 @@ pub(crate) struct WebPainterWgpu {
capture_rx: CaptureReceiver,
ctx: egui::Context,
needs_reconfigure: bool,
needs_recreate: bool,
}
/// Owned web display handle that is `Send + Sync`.
@@ -118,7 +120,7 @@ impl WebPainterWgpu {
let surface_configuration = wgpu::SurfaceConfiguration {
format: render_state.target_format,
present_mode: wgpu_options.present_mode,
present_mode: wgpu_options.surface.present_mode,
view_formats: vec![render_state.target_format],
..default_configuration
};
@@ -129,6 +131,7 @@ impl WebPainterWgpu {
Ok(Self {
canvas,
instance,
render_state: Some(render_state),
surface,
surface_configuration,
@@ -140,6 +143,7 @@ impl WebPainterWgpu {
capture_rx,
ctx,
needs_reconfigure: false,
needs_recreate: false,
})
}
}
@@ -173,6 +177,24 @@ impl WebPainter for WebPainterWgpu {
));
};
// If the previous frame produced `CurrentSurfaceTexture::Lost`, drop and recreate the
// surface from the canvas before re-borrowing `self.render_state` for the rest of paint.
if self.needs_recreate {
self.needs_recreate = false;
match self
.instance
.create_surface(wgpu::SurfaceTarget::Canvas(self.canvas.clone()))
{
Ok(new_surface) => {
new_surface.configure(&render_state.device, &self.surface_configuration);
self.surface = new_surface;
}
Err(err) => {
log::error!("Failed to recreate wgpu surface for canvas: {err}");
}
}
}
let mut encoder =
render_state
.device
@@ -239,10 +261,18 @@ impl WebPainter for WebPainterWgpu {
}
other => {
match (*self.on_surface_status)(&other) {
SurfaceErrorAction::RecreateSurface => {
SurfaceErrorAction::Reconfigure => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
}
SurfaceErrorAction::RecreateSurface => {
// Full recovery needs `&mut self`, which conflicts with the live
// `render_state` / `self.surface` borrows here. Defer to the top
// of the next paint via the `needs_recreate` flag, and request a
// repaint so the next frame actually invokes `paint` to consume it.
self.needs_recreate = true;
self.ctx.request_repaint();
}
SurfaceErrorAction::SkipFrame => {}
}
return Ok(());
@@ -335,7 +365,7 @@ impl WebPainter for WebPainterWgpu {
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
.submit(user_cmd_bufs.into_iter().chain([encoder.finish()]));
.submit(std::iter::chain(user_cmd_bufs, [encoder.finish()]));
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
if let Some(capture_buffer) = capture_buffer

View File

@@ -6,6 +6,15 @@ 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.34.3 - 2026-05-27
* Fix random hangs by improving `wgpu::Surface` lifecycle handling [#8171](https://github.com/emilk/egui/pull/8171) by [@grtlr](https://github.com/grtlr)
## 0.34.2 - 2026-05-04
* Update to wgpu 29.0.1 [#8073](https://github.com/emilk/egui/pull/8073) by [@emilk](https://github.com/emilk)
* Warn if using a software rasterizer [#8101](https://github.com/emilk/egui/pull/8101) by [@emilk](https://github.com/emilk)
## 0.34.1 - 2026-03-27
* `wgpu` backend: Enable WebGL fallback [#8038](https://github.com/emilk/egui/pull/8038) by [@emilk](https://github.com/emilk)

View File

@@ -64,6 +64,44 @@ pub enum WgpuError {
HandleError(#[from] ::winit::raw_window_handle::HandleError),
}
/// Runtime-mutable subset of [`WgpuConfiguration`].
///
/// Edit any field to have the surface reconfigured on the next paint.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SurfaceConfig {
/// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode,
/// Desired maximum number of frames that the presentation engine should queue in advance.
///
/// Use `1` for low-latency, and `2` for high-throughput.
///
/// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
///
/// `None` => Let `wgpu` pick a default (currently `2`).
pub desired_maximum_frame_latency: Option<u32>,
}
impl SurfaceConfig {
/// Good default for GUIs with very little (or no) extra GPU work.
pub const LOW_LATENCY: Self = Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: if cfg!(target_os = "ios") {
None // The default is good on iOS, while `Some(1)` cuts FPS in half
} else {
Some(1)
},
};
/// Good default for GUIs with a lot of extra GPU work,
/// or that want to prioritize smoothness over latency.
pub const HIGH_THROUGHPUT: Self = Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: Some(2), // High-throughput.
};
}
/// Access to the render state for egui.
#[derive(Clone)]
pub struct RenderState {
@@ -88,6 +126,11 @@ pub struct RenderState {
/// Egui renderer responsible for drawing the UI.
pub renderer: Arc<RwLock<Renderer>>,
/// Runtime-mutable subset of the wgpu configuration.
///
/// Update this to have the surface reconfigured on the next paint.
pub surface_config: SurfaceConfig,
}
async fn request_adapter(
@@ -138,29 +181,12 @@ async fn request_adapter(
}
})?;
if cfg!(target_arch = "wasm32") {
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
if 1 < available_adapters.len() {
log::info!(
"There are {} available wgpu adapters: {}",
available_adapters.len(),
describe_adapters(available_adapters)
);
} 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)
@@ -236,6 +262,8 @@ impl RenderState {
}) => (adapter, device, queue),
};
log_adapter_info(&adapter.get_info());
let surface_formats = {
profiling::scope!("get_capabilities");
compatible_surface.map_or_else(
@@ -258,6 +286,7 @@ impl RenderState {
queue,
target_format,
renderer: Arc::new(RwLock::new(renderer)),
surface_config: config.surface,
})
}
}
@@ -281,24 +310,28 @@ pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
/// Instructs egui to recreate the surface, then skip the current frame.
/// Reconfigure the existing surface, then skip the current frame.
///
/// Calls [`wgpu::Surface::configure`] on the current surface object.
/// Use for [`wgpu::CurrentSurfaceTexture::Outdated`].
Reconfigure,
/// Drop the surface, create a new one via [`wgpu::Instance::create_surface`], configure it,
/// then skip the current frame.
///
/// Use for [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the same surface
/// object cannot recover.
RecreateSurface,
}
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
#[derive(Clone)]
pub struct WgpuConfiguration {
/// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode,
/// Desired maximum number of frames that the presentation engine should queue in advance.
/// Runtime-mutable configuration for the surface (present mode, frame latency).
///
/// Use `1` for low-latency, and `2` for high-throughput.
///
/// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
///
/// `None` = `wgpu` default.
pub desired_maximum_frame_latency: Option<u32>,
/// These are the fields exposed via [`RenderState::surface_config`] for live
/// reconfiguration at runtime.
pub surface: SurfaceConfig,
/// How to create the wgpu adapter & device
pub wgpu_setup: WgpuSetup,
@@ -323,47 +356,55 @@ fn wgpu_config_impl_send_sync() {
impl std::fmt::Debug for WgpuConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
present_mode,
desired_maximum_frame_latency,
surface,
wgpu_setup,
on_surface_status: _,
} = self;
f.debug_struct("WgpuConfiguration")
.field("present_mode", &present_mode)
.field(
"desired_maximum_frame_latency",
&desired_maximum_frame_latency,
)
.field("surface", &surface)
.field("wgpu_setup", &wgpu_setup)
.finish_non_exhaustive()
}
}
impl WgpuConfiguration {
#[inline]
pub fn with_surface_config(mut self, surface_config: SurfaceConfig) -> Self {
self.surface = surface_config;
self
}
}
impl Default for WgpuConfiguration {
fn default() -> Self {
Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: None,
surface: SurfaceConfig::HIGH_THROUGHPUT,
// No display handle available at this point — callers should replace this with
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
wgpu_setup: WgpuSetup::without_display_handle(),
on_surface_status: Arc::new(|status| {
match status {
wgpu::CurrentSurfaceTexture::Outdated => {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"
}
wgpu::CurrentSurfaceTexture::Occluded => {
// This error occurs when the application is occluded (e.g. minimized or behind another window).
log::debug!("Dropped frame with error: {status:?}");
}
_ => {
log::warn!("Dropped frame with error: {status:?}");
}
on_surface_status: Arc::new(|status| match status {
wgpu::CurrentSurfaceTexture::Outdated => {
// The compositor changed the surface (resize, scale, output, …). wgpu
// requires us to reconfigure before the next acquire. Skipping would mean
// we are stuck in `Outdated` forever.
log::trace!("Dropped frame with error: {status:?}");
SurfaceErrorAction::Reconfigure
}
wgpu::CurrentSurfaceTexture::Lost => {
// The underlying surface is gone and we need a fresh one from the `wgpu::Instance`.
log::debug!("Dropped frame with error: {status:?}");
SurfaceErrorAction::RecreateSurface
}
wgpu::CurrentSurfaceTexture::Occluded => {
// App is hidden (minimized / behind another window). Skip silently.
log::trace!("Skipping frame due to occlusion.");
SurfaceErrorAction::SkipFrame
}
_ => {
log::warn!("Dropped frame with error: {status:?}");
SurfaceErrorAction::SkipFrame
}
SurfaceErrorAction::SkipFrame
}),
}
}
@@ -406,6 +447,18 @@ pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wg
// ---------------------------------------------------------------------------
fn log_adapter_info(info: &wgpu::AdapterInfo) {
let summary = adapter_info_summary(info);
let is_test = cfg!(test); // Software rasterizers are expected (and preferred) during testing!
if info.device_type == wgpu::DeviceType::Cpu && !is_test {
log::warn!("Software rasterizer detected - loss of performance expected. {summary}");
} else {
log::debug!("wgpu adapter: {summary}");
}
}
/// A human-readable summary about an adapter
pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
let wgpu::AdapterInfo {
@@ -427,37 +480,52 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
use std::fmt::Write as _;
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
if !name.is_empty() {
summary += &format!(", name: {name:?}");
write!(summary, ", name: {name:?}").ok();
}
if !driver.is_empty() {
summary += &format!(", driver: {driver:?}");
write!(summary, ", driver: {driver:?}").ok();
}
if !driver_info.is_empty() {
summary += &format!(", driver_info: {driver_info:?}");
write!(summary, ", driver_info: {driver_info:?}").ok();
}
if *vendor != 0 {
#[cfg(not(target_arch = "wasm32"))]
{
summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor));
write!(
summary,
", vendor: {} (0x{vendor:04X})",
parse_vendor_id(*vendor)
)
.ok();
}
#[cfg(target_arch = "wasm32")]
{
summary += &format!(", vendor: 0x{vendor:04X}");
write!(summary, ", vendor: 0x{vendor:04X}").ok();
}
}
if *device != 0 {
summary += &format!(", device: 0x{device:02X}");
write!(summary, ", device: 0x{device:02X}").ok();
}
if !device_pci_bus_id.is_empty() {
summary += &format!(", pci_bus_id: {device_pci_bus_id:?}");
write!(summary, ", pci_bus_id: {device_pci_bus_id:?}").ok();
}
if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
summary += &format!(", subgroup_size: {subgroup_min_size}..={subgroup_max_size}");
write!(
summary,
", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"
)
.ok();
}
summary += &format!(", transient_saves_memory: {transient_saves_memory}");
write!(
summary,
", transient_saves_memory: {transient_saves_memory}"
)
.ok();
summary
}

View File

@@ -296,15 +296,22 @@ impl Clone for WgpuSetupCreateNew {
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
instance_descriptor,
display_handle,
power_preference,
native_adapter_selector,
device_descriptor: _,
} = self;
f.debug_struct("WgpuSetupCreateNew")
.field("instance_descriptor", &self.instance_descriptor)
.field("display_handle", &self.display_handle)
.field("power_preference", &self.power_preference)
.field("instance_descriptor", instance_descriptor)
.field("display_handle", display_handle)
.field("power_preference", power_preference)
.field(
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
&native_adapter_selector.is_some(),
)
.finish()
.finish_non_exhaustive()
}
}

View File

@@ -3,7 +3,7 @@
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
#![expect(unsafe_code)]
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
@@ -18,6 +18,7 @@ struct SurfaceState {
height: u32,
resizing: bool,
needs_reconfigure: bool,
needs_recreate: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -27,7 +28,7 @@ struct SurfaceState {
/// NOTE: all egui viewports share the same painter.
pub struct Painter {
context: Context,
configuration: WgpuConfiguration,
config: WgpuConfiguration,
options: RendererOptions,
support_transparent_backbuffer: bool,
screen_capture_state: Option<CaptureState>,
@@ -58,16 +59,16 @@ impl Painter {
/// associated.
pub async fn new(
context: Context,
configuration: WgpuConfiguration,
config: WgpuConfiguration,
support_transparent_backbuffer: bool,
options: RendererOptions,
) -> Self {
let (capture_tx, capture_rx) = capture_channel();
let instance = configuration.wgpu_setup.new_instance().await;
let instance = config.wgpu_setup.new_instance().await;
Self {
context,
configuration,
config,
options,
support_transparent_backbuffer,
screen_capture_state: None,
@@ -94,17 +95,22 @@ impl Painter {
fn configure_surface(
surface_state: &SurfaceState,
render_state: &RenderState,
config: &WgpuConfiguration,
config: &SurfaceConfig,
) {
profiling::function_scope!();
let SurfaceConfig {
present_mode,
desired_maximum_frame_latency,
} = *config;
let width = surface_state.width;
let height = surface_state.height;
let mut surf_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: render_state.target_format,
present_mode: config.present_mode,
present_mode,
alpha_mode: surface_state.alpha_mode,
view_formats: vec![render_state.target_format],
..surface_state
@@ -113,7 +119,7 @@ impl Painter {
.expect("The surface isn't supported by this adapter")
};
if let Some(desired_maximum_frame_latency) = config.desired_maximum_frame_latency {
if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
}
@@ -122,6 +128,33 @@ impl Painter {
.configure(&render_state.device, &surf_config);
}
/// Drop the existing [`wgpu::Surface`] for `viewport_id` and create a fresh one for the
/// given window via [`wgpu::Instance::create_surface`], then configure it.
///
/// Used to recover from [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the
/// existing surface object cannot recover.
fn recreate_surface(
&mut self,
viewport_id: ViewportId,
window: &Arc<winit::window::Window>,
) -> Result<(), crate::WgpuError> {
profiling::function_scope!();
let Some(old_state) = self.surfaces.remove(&viewport_id) else {
return Ok(());
};
let surface = self.instance.create_surface(Arc::clone(window))?;
self.install_surface(
surface,
viewport_id,
old_state.width,
old_state.height,
old_state.resizing,
);
Ok(())
}
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
///
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
@@ -198,56 +231,74 @@ impl Painter {
viewport_id: ViewportId,
size: winit::dpi::PhysicalSize<u32>,
) -> Result<(), crate::WgpuError> {
let render_state = if let Some(render_state) = &self.render_state {
render_state
} else {
let render_state = RenderState::create(
&self.configuration,
&self.instance,
Some(&surface),
self.options,
)
.await?;
self.render_state.get_or_insert(render_state)
};
let alpha_mode = if self.support_transparent_backbuffer {
let supported_alpha_modes = surface.get_capabilities(&render_state.adapter).alpha_modes;
if self.render_state.is_none() {
let render_state =
RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
.await?;
self.render_state = Some(render_state);
}
self.install_surface(surface, viewport_id, size.width, size.height, false);
Ok(())
}
// Prefer pre multiplied over post multiplied!
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied) {
wgpu::CompositeAlphaMode::PostMultiplied
/// Inserts a freshly created surface into [`Self::surfaces`] and configures it.
///
/// Render state must already be initialised before calling this.
// NOTE: The same assumption is already required by `resize_and_generate_depth_texture_view_and_msaa_view`.
fn install_surface(
&mut self,
surface: wgpu::Surface<'static>,
viewport_id: ViewportId,
width: u32,
height: u32,
resizing: bool,
) {
let alpha_mode = {
// Panic: We use the same failure mode as `resize_and_generate_depth_texture_view_and_msaa_view`
let render_state = self
.render_state
.as_ref()
.expect("install_surface called before render_state initialization");
if self.support_transparent_backbuffer {
let supported_alpha_modes =
surface.get_capabilities(&render_state.adapter).alpha_modes;
// Prefer pre multiplied over post multiplied!
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
wgpu::CompositeAlphaMode::PreMultiplied
} 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."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
log::warn!(
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
);
wgpu::CompositeAlphaMode::Auto
}
} else {
wgpu::CompositeAlphaMode::Auto
};
self.surfaces.insert(
viewport_id,
SurfaceState {
surface,
width: size.width,
height: size.height,
width,
height,
alpha_mode,
resizing: false,
resizing,
needs_reconfigure: false,
needs_recreate: false,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
let Some(width) = NonZeroU32::new(width) else {
log::debug!("The window width was zero; skipping generate textures");
return Ok(());
return;
};
let Some(height) = NonZeroU32::new(size.height) else {
let Some(height) = NonZeroU32::new(height) else {
log::debug!("The window height was zero; skipping generate textures");
return Ok(());
return;
};
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
Ok(())
}
/// Returns the maximum texture dimension supported if known
@@ -278,7 +329,7 @@ impl Painter {
surface_state.width = width;
surface_state.height = height;
Self::configure_surface(surface_state, render_state, &self.configuration);
Self::configure_surface(surface_state, render_state, &self.config.surface);
if let Some(depth_format) = self.options.depth_stencil_format {
self.depth_texture_view.insert(
@@ -363,20 +414,27 @@ impl Painter {
// 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.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
hal_surface
.render_layer()
.lock()
.setPresentsWithTransaction(resizing);
// setPresentsWithTransaction causes hangs when desired_maximum_frame_latency == 1
let is_low_latency = self
.render_state
.as_ref()
.is_some_and(|rs| rs.surface_config.desired_maximum_frame_latency == Some(1));
if !is_low_latency {
// SAFETY: The cast is checked with if condition. If the used backend is not metal
// it gracefully fails.
unsafe {
if let Some(hal_surface) = state.surface.as_hal::<wgpu::hal::api::Metal>() {
hal_surface
.render_layer()
.lock()
.setPresentsWithTransaction(resizing);
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.configuration,
);
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.config.surface,
);
}
}
}
}
@@ -411,6 +469,7 @@ impl Painter {
/// and the captures captured screenshot if it was requested.
///
/// If `capture_data` isn't empty, a screenshot will be captured.
#[expect(clippy::too_many_arguments)]
pub fn paint_and_update_textures(
&mut self,
viewport_id: ViewportId,
@@ -419,6 +478,7 @@ impl Painter {
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
capture_data: Vec<UserData>,
window: &Arc<winit::window::Window>,
) -> f32 {
profiling::function_scope!();
@@ -447,6 +507,33 @@ impl Painter {
let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0;
// If the previous frame produced `CurrentSurfaceTexture::Lost`, the action match
// below set `needs_recreate`. Recreate the surface now, before re-borrowing
// `self.render_state` / `self.surfaces` for the rest of the paint.
if self
.surfaces
.get(&viewport_id)
.is_some_and(|s| s.needs_recreate)
&& let Err(err) = self.recreate_surface(viewport_id, window)
{
log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
return vsync_sec;
}
// Apply any runtime changes requested via `RenderState::surface_config`.
// We diff against the already-applied values in `self.config.surface`
// and, if anything differs, mark every surface as needing reconfiguration so
// the existing `needs_reconfigure` pathway below picks them up.
if let Some(render_state) = self.render_state.as_ref()
&& render_state.surface_config != self.config.surface
{
self.config.surface = render_state.surface_config;
#[expect(clippy::iter_over_hash_type)]
for surface in self.surfaces.values_mut() {
surface.needs_reconfigure = true;
}
}
let Some(render_state) = self.render_state.as_mut() else {
return vsync_sec;
};
@@ -494,7 +581,7 @@ impl Painter {
};
if surface_state.needs_reconfigure {
Self::configure_surface(surface_state, render_state, &self.configuration);
Self::configure_surface(surface_state, render_state, &self.config.surface);
surface_state.needs_reconfigure = false;
}
@@ -514,9 +601,19 @@ impl Painter {
frame
}
other => {
match (*self.configuration.on_surface_status)(&other) {
match (*self.config.on_surface_status)(&other) {
SurfaceErrorAction::Reconfigure => {
Self::configure_surface(surface_state, render_state, &self.config.surface);
self.context.request_repaint_of(viewport_id);
}
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
// Because of ownership, I could not find an easy way to do a full recovery here,
// as that would involve dropping the old surface and creating a new one.
// For now, we defer the recreation to the beginning of the next frame (which
// we ensure to arrive via `request_repaint_of`). A cleaner solution would be
// to untangle the ownership of `RenderState`.
surface_state.needs_recreate = true;
self.context.request_repaint_of(viewport_id);
}
SurfaceErrorAction::SkipFrame => {}
}
@@ -625,7 +722,7 @@ impl Painter {
let start = web_time::Instant::now();
render_state
.queue
.submit(user_cmd_bufs.into_iter().chain([encoded]));
.submit(std::iter::chain(user_cmd_bufs, [encoded]));
vsync_sec += start.elapsed().as_secs_f32();
};
@@ -654,6 +751,8 @@ impl Painter {
);
}
window.pre_present_notify();
{
profiling::scope!("present");
// wgpu doesn't document where vsync can happen. Maybe here?

View File

@@ -5,6 +5,14 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.3 - 2026-05-27
Nothing new
## 0.34.2 - 2026-05-04
Nothing new
## 0.34.1 - 2026-03-27
Nothing new

View File

@@ -20,6 +20,9 @@ workspace = true
all-features = true
rustdoc-args = ["--generate-link-to-definition"]
[package.metadata.cargo-shear]
ignored = ["wayland-cursor"] # TODO(emilk): remove when we update winit
[features]
default = ["clipboard", "links", "wayland", "winit/default", "x11"]

View File

@@ -32,7 +32,7 @@ use winit::{
dpi::{PhysicalPosition, PhysicalSize},
event::ElementState,
event_loop::ActiveEventLoop,
window::{CursorGrabMode, Window, WindowButtons, WindowLevel},
window::{CursorGrabMode, CustomCursor, Window, WindowButtons, WindowLevel},
};
pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
@@ -88,6 +88,14 @@ pub struct State {
any_pointer_button_down: bool,
current_cursor_icon: Option<egui::CursorIcon>,
/// Cached `CustomCursor` for the last RGBA bitmap pushed through
/// `PlatformOutput::cursor_image`. We dedupe by `Arc::as_ptr` so the
/// integration only re-uploads the bitmap to the OS when the app
/// switches sprite, not every frame the cursor moves. `usize` is the
/// raw pointer of the source `Arc<[u8]>` — opaque, only used as a
/// cache key.
current_custom_cursor: Option<(usize, CustomCursor)>,
clipboard: clipboard::Clipboard,
/// If `true`, mouse inputs will be treated as touches.
@@ -101,9 +109,6 @@ pub struct State {
/// Only one touch will be interpreted as pointer at any time.
pointer_touch_id: Option<u64>,
/// track ime state
has_sent_ime_enabled: bool,
#[cfg(feature = "accesskit")]
pub accesskit: Option<accesskit_winit::Adapter>,
@@ -135,13 +140,16 @@ impl State {
};
let mut slf = Self {
egui_ctx,
viewport_id,
start_time: web_time::Instant::now(),
start_time: web_time::Instant::now()
.checked_sub(web_time::Duration::from_secs_f64(egui_ctx.time()))
.unwrap_or_else(web_time::Instant::now),
egui_ctx,
egui_input,
pointer_pos_in_points: None,
any_pointer_button_down: false,
current_cursor_icon: None,
current_custom_cursor: None,
clipboard: clipboard::Clipboard::new(
display_target.display_handle().ok().map(|h| h.as_raw()),
@@ -150,8 +158,6 @@ impl State {
simulate_touch_screen: false,
pointer_touch_id: None,
has_sent_ime_enabled: false,
#[cfg(feature = "accesskit")]
accesskit: None,
@@ -689,17 +695,11 @@ impl State {
// }
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();
// [`winit::event::Ime::Enabled`] means different things in X11 and
// Wayland, but it doesn't matter to us.
// See <https://github.com/rust-windowing/winit/issues/2498>
winit::event::Ime::Enabled | winit::event::Ime::Disabled => {}
winit::event::Ime::Preedit(text, _) => {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone())));
@@ -708,53 +708,10 @@ impl State {
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) => {
if cfg!(target_os = "macos") {
// On macOS, when the user presses backspace to delete the
// last character in an IME composition, `winit` only emits
// `winit::event::Ime::Preedit("", None)` without a
// preceding `winit::event::Ime::Preedit("", Some(0, 0))`.
//
// The current implementation of `egui::TextEdit` relies on
// receiving an `egui::ImeEvent::Preedit("")` to remove the
// last character in the composition in this case, so we
// emit it here.
//
// This is guarded to macOS-only, as applying it on other
// platforms is unnecessary and can cause undesired
// behavior.
// See: https://github.com/emilk/egui/pull/7973
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new())));
}
self.ime_event_disable();
}
}
}
pub fn ime_event_enable(&mut self) {
if !self.has_sent_ime_enabled {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Enabled));
self.has_sent_ime_enabled = true;
}
}
pub fn ime_event_disable(&mut self) {
self.egui_input
.events
.push(egui::Event::Ime(egui::ImeEvent::Disabled));
self.has_sent_ime_enabled = false;
}
/// Returns `true` if the event was sent to egui.
pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool {
if !self.is_pointer_in_window() && !self.any_pointer_button_down {
@@ -1072,12 +1029,38 @@ impl State {
&mut self,
window: &Window,
platform_output: egui::PlatformOutput,
) {
self.handle_platform_output_inner(window, None, platform_output);
}
/// Same as [`Self::handle_platform_output`] but threads the
/// `ActiveEventLoop` so we can register a `winit::CustomCursor` from
/// `PlatformOutput::cursor_image`. Integration paths that don't have
/// access to the event loop (e.g. immediate viewports) should call
/// [`Self::handle_platform_output`] instead — any custom cursor
/// request is silently dropped there and the standard `cursor_icon`
/// path still runs.
pub fn handle_platform_output_with_event_loop(
&mut self,
window: &Window,
event_loop: &ActiveEventLoop,
platform_output: egui::PlatformOutput,
) {
self.handle_platform_output_inner(window, Some(event_loop), platform_output);
}
fn handle_platform_output_inner(
&mut self,
window: &Window,
event_loop: Option<&ActiveEventLoop>,
platform_output: egui::PlatformOutput,
) {
profiling::function_scope!();
let egui::PlatformOutput {
commands,
cursor_icon,
cursor_image,
events: _, // handled elsewhere
mutable_text_under_cursor: _, // only used in eframe web
ime,
@@ -1100,10 +1083,11 @@ impl State {
}
}
self.set_cursor_icon(window, cursor_icon);
self.apply_cursor(window, event_loop, cursor_icon, cursor_image.as_ref());
let allow_ime = ime.is_some();
if self.allow_ime != allow_ime {
let is_toggling_ime = self.allow_ime != allow_ime;
if is_toggling_ime {
self.allow_ime = allow_ime;
#[cfg(target_os = "windows")]
if !self.allow_ime {
@@ -1120,6 +1104,14 @@ impl State {
}
if let Some(ime) = ime {
if !is_toggling_ime && ime.should_interrupt_composition {
// TODO(umajho): use a more proper way to interrupt composition
// if `winit` provides one in the future.
window.set_ime_allowed(false);
window.set_ime_allowed(true);
}
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
let ime_rect_px = pixels_per_point * ime.rect;
if self.ime_rect_px != Some(ime_rect_px)
@@ -1154,26 +1146,92 @@ impl State {
let _ = accesskit_update;
}
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
/// Apply either a bitmap cursor (preferred when both `cursor_image`
/// and `event_loop` are `Some`) or the standard `cursor_icon` to the
/// window. Mirrors the no-flicker dedupe the old `set_cursor_icon`
/// did, on the appropriate cache key for whichever path is active.
fn apply_cursor(
&mut self,
window: &Window,
event_loop: Option<&ActiveEventLoop>,
cursor_icon: egui::CursorIcon,
cursor_image: Option<&egui::CustomCursorImage>,
) {
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if !is_pointer_in_window {
// Drop both caches so the cursor gets re-applied (and the
// bitmap re-checked for staleness) once the pointer comes
// back. Same contract the old `set_cursor_icon` followed.
self.current_cursor_icon = None;
self.current_custom_cursor = None;
return;
}
// Bitmap cursor wins over CursorIcon when both are present and we
// have an event loop to register it with. Otherwise the bitmap is
// dropped and we fall through to the icon path — this is the
// documented fallback for integrations that didn't opt in.
if let (Some(image), Some(event_loop)) = (cursor_image, event_loop) {
let key = std::sync::Arc::as_ptr(&image.rgba).cast::<u8>() as usize;
let cached = self
.current_custom_cursor
.as_ref()
.filter(|(k, _)| *k == key)
.map(|(_, c)| c.clone());
let custom = match cached {
Some(c) => c,
None => match winit::window::CustomCursor::from_rgba(
image.rgba.to_vec(),
image.size[0],
image.size[1],
image.hotspot[0],
image.hotspot[1],
) {
Ok(source) => {
let c = event_loop.create_custom_cursor(source);
self.current_custom_cursor = Some((key, c.clone()));
c
}
Err(err) => {
log::warn!(
"egui-winit: invalid cursor bitmap, falling back to cursor_icon: {err:?}"
);
self.current_custom_cursor = None;
self.set_cursor_icon_inner(window, cursor_icon);
return;
}
},
};
window.set_cursor_visible(true);
window.set_cursor(custom);
// Resync `current_cursor_icon` so the next icon-only path
// notices a real change rather than dedupe-skipping it.
self.current_cursor_icon = None;
return;
}
self.current_custom_cursor = None;
self.set_cursor_icon_inner(window, cursor_icon);
}
/// Icon-only path, factored out so `apply_cursor` can fall back to it
/// when the bitmap path bails. Preserves the original dedupe.
fn set_cursor_icon_inner(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
if self.current_cursor_icon == Some(cursor_icon) {
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
// On other platforms: just early-out to save CPU.
return;
}
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
if is_pointer_in_window {
self.current_cursor_icon = Some(cursor_icon);
self.current_cursor_icon = Some(cursor_icon);
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
window.set_cursor(winit_cursor_icon);
} else {
window.set_cursor_visible(false);
}
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
window.set_cursor_visible(true);
window.set_cursor(winit_cursor_icon);
} else {
// Remember to set the cursor again once the cursor returns to the screen:
self.current_cursor_icon = None;
window.set_cursor_visible(false);
}
}
}
@@ -1723,7 +1781,24 @@ fn process_viewport_command(
ViewportCommand::Fullscreen(v) => {
window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None)));
}
ViewportCommand::Decorations(v) => window.set_decorations(v),
ViewportCommand::SetMonitor(idx) => {
if let Some(monitor) = window.available_monitors().nth(idx) {
window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
} else {
log::warn!(
"ViewportCommand::SetMonitor({idx}): index out of range ({} monitors available)",
window.available_monitors().count()
);
}
}
ViewportCommand::Decorations(v) => {
window.set_decorations(v);
#[cfg(target_os = "windows")]
{
use winit::platform::windows::WindowExtWindows as _;
window.set_undecorated_shadow(!v);
}
}
ViewportCommand::WindowLevel(l) => window.set_window_level(match l {
egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom,
egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop,
@@ -1821,7 +1896,24 @@ pub fn create_window(
) -> Result<Window, winit::error::OsError> {
profiling::function_scope!();
let window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
let mut window_attributes = create_winit_window_attributes(egui_ctx, viewport_builder.clone());
// Resolve target monitor index → MonitorHandle, so the window is created
// directly in borderless fullscreen on the requested output. This is the
// only reliable way to target a specific monitor under Wayland, and also
// avoids the Mutter race where OuterPosition is ignored pre-mapping.
if let Some(idx) = viewport_builder.monitor {
if let Some(monitor) = event_loop.available_monitors().nth(idx) {
window_attributes = window_attributes
.with_fullscreen(Some(winit::window::Fullscreen::Borderless(Some(monitor))));
} else {
log::warn!(
"ViewportBuilder::with_monitor({idx}): index out of range ({} monitors available)",
event_loop.available_monitors().count()
);
}
}
let window = event_loop.create_window(window_attributes)?;
apply_viewport_builder_to_window(egui_ctx, &window, viewport_builder);
Ok(window)
@@ -1873,6 +1965,7 @@ pub fn create_winit_window_attributes(
mouse_passthrough: _, // handled in `apply_viewport_builder_to_window`
clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs`
monitor: _, // Handled in `create_window` (needs ActiveEventLoop for monitor handle)
} = viewport_builder;
let mut window_attributes = winit::window::WindowAttributes::default()
@@ -2003,6 +2096,7 @@ pub fn create_winit_window_attributes(
if let Some(show) = _taskbar {
window_attributes = window_attributes.with_skip_taskbar(!show);
}
window_attributes = window_attributes.with_undecorated_shadow(!decorations.unwrap_or(true));
}
#[cfg(target_os = "macos")]

View File

@@ -158,23 +158,30 @@ fn find_active_monitor(
return None; // no monitors 🤷
};
let mut active_monitor_overlap = 0.0;
for monitor in monitors {
let window_size_px = window_size_pts * (egui_zoom_factor * monitor.scale_factor() as f32);
let monitor_x_range = (monitor.position().x - window_size_px.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - window_size_px.y as i32)
..(monitor.position().y + monitor.size().height as i32);
let window_rect = egui::Rect::from_min_size(*position_px, window_size_px);
let overlap = window_rect.intersect(monitor_rect_px(&monitor)).area();
if monitor_x_range.contains(&(position_px.x as i32))
&& monitor_y_range.contains(&(position_px.y as i32))
{
if active_monitor_overlap < overlap {
active_monitor = monitor;
active_monitor_overlap = overlap;
}
}
Some(active_monitor)
}
fn monitor_rect_px(monitor: &winit::monitor::MonitorHandle) -> egui::Rect {
let pos = monitor.position();
let size = monitor.size();
egui::Rect::from_min_size(
egui::pos2(pos.x as f32, pos.y as f32),
egui::vec2(size.width as f32, size.height as f32),
)
}
fn clamp_pos_to_monitors(
egui_zoom_factor: f32,
event_loop: &winit::event_loop::ActiveEventLoop,
@@ -198,19 +205,12 @@ fn clamp_pos_to_monitors(
32.0 * egui_zoom_factor * active_monitor.scale_factor() as f32,
);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size_px = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);
let monitor_rect = monitor_rect_px(&active_monitor);
// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size_px - window_size_px).max(egui::Vec2::ZERO);
let window_size = (monitor_rect.size() - window_size_px).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position_px = position_px.clamp(monitor_position, monitor_position + window_size);
*position_px = position_px.clamp(monitor_rect.min, monitor_rect.min + window_size);
}

View File

@@ -74,6 +74,7 @@ epaint = { workspace = true, default-features = false }
accesskit.workspace = true
ahash.workspace = true
bitflags.workspace = true
itertools.workspace = true
log.workspace = true
nohash-hasher.workspace = true
profiling.workspace = true

View File

@@ -226,7 +226,7 @@ impl<'a> AtomLayout<'a> {
max_size.x = f32::INFINITY;
}
let available_size = ui.available_size().at_most(max_size);
let available_size = ui.available_size().at_most(max_size).at_least(min_size);
// The size available for the content
let available_inner_size = available_size - frame.total_margin().sum();

View File

@@ -1,3 +1,5 @@
use std::fmt::Write as _;
#[derive(Clone)]
struct Frame {
/// `_main` is usually as the deepest depth.
@@ -23,7 +25,7 @@ pub fn capture() -> String {
if let Some(file_and_line) = &mut file_and_line
&& let Some(line_nr) = symbol.lineno()
{
file_and_line.push_str(&format!(":{line_nr}"));
write!(file_and_line, ":{line_nr}").ok();
}
let file_and_line = file_and_line.unwrap_or_default();
@@ -130,12 +132,14 @@ pub fn capture() -> String {
if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth {
// Show that some frames were elided
formatted.push_str(&format!("{:widest_depth$}\n", ""));
writeln!(formatted, "{:widest_depth$} …", "").ok();
}
formatted.push_str(&format!(
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n"
));
writeln!(
formatted,
"{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}"
)
.ok();
last_depth = frame.depth;
}

View File

@@ -280,7 +280,7 @@ impl Area {
self
}
/// Constrains this area to [`Context::screen_rect`]?
/// Constrains this area to [`Context::content_rect`]?
///
/// Default: `true`.
#[inline]
@@ -291,7 +291,7 @@ impl Area {
/// Constrain the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
/// For instance: `.constrain_to(ctx.content_rect())`.
#[inline]
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.constrain = true;
@@ -583,7 +583,7 @@ impl Area {
}
}
fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
pub(crate) 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.
@@ -594,10 +594,6 @@ fn round_area_position(ctx: &Context, pos: Pos2) -> Pos2 {
}
impl Prepared {
pub(crate) fn state(&self) -> &AreaState {
&self.state
}
pub(crate) fn state_mut(&mut self) -> &mut AreaState {
&mut self.state
}

View File

@@ -1,9 +1,7 @@
use std::hash::Hash;
use crate::{
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,
AsIdSalt, Context, Id, IdSalt, InnerResponse, NumExt as _, Rect, Response, Sense, Stroke,
TextStyle, TextWrapMode, Ui, UiBuilder, UiKind, UiStackInfo, WidgetInfo, WidgetText,
WidgetType, emath, epaint, pos2, remap, remap_clamp, vec2,
};
use emath::GuiRounding as _;
use epaint::{Shape, StrokeKind};
@@ -81,30 +79,6 @@ impl CollapsingState {
}
}
/// Will toggle when clicked, etc.
pub(crate) fn show_default_button_with_size(
&mut self,
ui: &mut Ui,
button_size: Vec2,
) -> Response {
let (_id, rect) = ui.allocate_space(button_size);
let response = ui.interact(rect, self.id, Sense::click());
response.widget_info(|| {
WidgetInfo::labeled(
WidgetType::Button,
ui.is_enabled(),
if self.is_open() { "Hide" } else { "Show" },
)
});
if response.clicked() {
self.toggle(ui);
}
let openness = self.openness(ui.ctx());
paint_default_icon(ui, openness, &response);
response
}
/// Will toggle when clicked, etc.
fn show_default_button_indented(&mut self, ui: &mut Ui) -> Response {
self.show_button_indented(ui, paint_default_icon)
@@ -213,20 +187,31 @@ impl CollapsingState {
self.store(ui.ctx()); // we store any earlier toggling as promised in the docstring
None
} else if openness < 1.0 {
Some(ui.scope_builder(builder, |child_ui| {
let max_height = if self.state.open && self.state.open_height.is_none() {
// First frame of expansion.
// We don't know full height yet, but we will next frame.
// Just use a placeholder value that shows some movement:
10.0
} else {
let full_height = self.state.open_height.unwrap_or_default();
remap_clamp(openness, 0.0..=1.0, 0.0..=full_height).round_ui()
};
// The spacing between the header and the body. We animate this too.
let item_spacing = ui.spacing().item_spacing.y;
let mut clip_rect = child_ui.clip_rect();
clip_rect.max.y = clip_rect.max.y.min(child_ui.max_rect().top() + max_height);
child_ui.set_clip_rect(clip_rect);
let fallback_height_guess = 10.0; // Just use a placeholder value that shows some movement for the first frame
let full_height = self.state.open_height.unwrap_or(fallback_height_guess);
let clipped_child_height =
(remap_clamp(openness, 0.0..=1.0, 0.0..=full_height + item_spacing) - item_spacing)
.round_ui();
if clipped_child_height < 0.0 {
ui.add_space(clipped_child_height); // animate the spacing!
}
Some(ui.scope_builder(builder, |child_ui| {
let clipped_child_height = clipped_child_height.at_least(0.0);
{
let mut clip_rect = child_ui.clip_rect();
clip_rect.max.y = f32::min(
clip_rect.max.y,
child_ui.max_rect().top() + clipped_child_height,
);
child_ui.set_clip_rect(clip_rect);
}
let ret = add_body(child_ui);
@@ -237,8 +222,8 @@ impl CollapsingState {
}
self.store(child_ui.ctx()); // remember the height
// Pretend children took up at most `max_height` space:
min_rect.max.y = min_rect.max.y.at_most(min_rect.top() + max_height);
// Pretend children took up at most `clipped_child_height` space:
min_rect.max.y = f32::min(min_rect.max.y, min_rect.top() + clipped_child_height);
child_ui.force_set_min_rect(min_rect);
ret
}))
@@ -393,7 +378,7 @@ pub struct CollapsingHeader {
text: WidgetText,
default_open: bool,
open: Option<bool>,
id_salt: Id,
id_salt: IdSalt,
enabled: bool,
selectable: bool,
selected: bool,
@@ -410,7 +395,7 @@ impl CollapsingHeader {
/// you need to provide a unique id source with [`Self::id_salt`].
pub fn new(text: impl Into<WidgetText>) -> Self {
let text = text.into();
let id_salt = Id::new(text.text());
let id_salt = IdSalt::new(text.text());
Self {
text,
default_open: false,
@@ -446,17 +431,8 @@ impl CollapsingHeader {
/// Explicitly set the source of the [`Id`] of this widget, instead of using title label.
/// This is useful if the title label is dynamic or not unique.
#[inline]
pub fn id_salt(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Id::new(id_salt);
self
}
/// Explicitly set the source of the [`Id`] of this widget, instead of using title label.
/// This is useful if the title label is dynamic or not unique.
#[deprecated = "Renamed id_salt"]
#[inline]
pub fn id_source(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Id::new(id_salt);
pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
self.id_salt = IdSalt::new(id_salt);
self
}

View File

@@ -1,9 +1,11 @@
use epaint::Shape;
use crate::{
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,
Align2, AsIdSalt, Context, Id, IdSalt, InnerResponse, NumExt as _, Painter, Popup,
PopupCloseBehavior, Rect, Response, ScrollArea, Sense, Stroke, TextStyle, TextWrapMode, Ui,
UiBuilder, Vec2, WidgetInfo, WidgetText, WidgetType, epaint,
style::{StyleModifier, WidgetVisuals},
vec2,
};
#[expect(unused_imports)] // Documentation
@@ -36,7 +38,7 @@ pub type IconPainter = Box<dyn FnOnce(&Ui, Rect, &WidgetVisuals, bool)>;
/// ```
#[must_use = "You should call .show*"]
pub struct ComboBox {
id_salt: Id,
id_salt: IdSalt,
label: Option<WidgetText>,
selected_text: WidgetText,
width: Option<f32>,
@@ -49,9 +51,9 @@ pub struct ComboBox {
impl ComboBox {
/// Create new [`ComboBox`] with id and label
pub fn new(id_salt: impl std::hash::Hash, label: impl Into<WidgetText>) -> Self {
pub fn new(id_salt: impl AsIdSalt, label: impl Into<WidgetText>) -> Self {
Self {
id_salt: Id::new(id_salt),
id_salt: IdSalt::new(id_salt),
label: Some(label.into()),
selected_text: Default::default(),
width: None,
@@ -67,7 +69,7 @@ impl ComboBox {
pub fn from_label(label: impl Into<WidgetText>) -> Self {
let label = label.into();
Self {
id_salt: Id::new(label.text()),
id_salt: IdSalt::new(label.text()),
label: Some(label),
selected_text: Default::default(),
width: None,
@@ -80,9 +82,9 @@ impl ComboBox {
}
/// Without label.
pub fn from_id_salt(id_salt: impl std::hash::Hash) -> Self {
pub fn from_id_salt(id_salt: impl AsIdSalt) -> Self {
Self {
id_salt: Id::new(id_salt),
id_salt: IdSalt::new(id_salt),
label: Default::default(),
selected_text: Default::default(),
width: None,
@@ -94,12 +96,6 @@ impl ComboBox {
}
}
/// Without label.
#[deprecated = "Renamed from_id_salt"]
pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self {
Self::from_id_salt(id_salt)
}
/// Set the outer width of the button and menu.
///
/// Default is [`Spacing::combo_width`].

View File

@@ -174,11 +174,6 @@ impl Frame {
Self::NONE
}
#[deprecated = "Use `Frame::NONE` or `Frame::new()` instead."]
pub const fn none() -> Self {
Self::NONE
}
/// For when you want to group a few widgets together within a frame.
pub fn group(style: &Style) -> Self {
Self::new()
@@ -197,6 +192,7 @@ impl Frame {
Self::new().inner_margin(8).fill(style.visuals.panel_fill)
}
/// The default frame for an [`crate::Window`].
pub fn window(style: &Style) -> Self {
Self::new()
.inner_margin(style.spacing.window_margin)
@@ -283,16 +279,6 @@ impl Frame {
self
}
/// The rounding of the _outer_ corner of the [`Self::stroke`]
/// (or, if there is no stroke, the outer corner of [`Self::fill`]).
///
/// In other words, this is the corner radius of the _widget rect_.
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
/// Margin outside the painted frame.
///
/// Similar to what is called `margin` in CSS.
@@ -413,11 +399,15 @@ impl Frame {
}
/// Show the given ui surrounded by this frame.
///
/// The returned [`InnerResponse::response`] will have the rect of the entire frame, including margins.
pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.show_dyn(ui, Box::new(add_contents))
}
/// Show using dynamic dispatch.
///
/// The returned [`InnerResponse::response`] will have the rect of the entire frame, including margins.
pub fn show_dyn<'c, R>(
self,
ui: &mut Ui,

View File

@@ -197,7 +197,7 @@ impl MenuState {
/// Horizontal menu bar where you can add [`MenuButton`]s.
///
/// The menu bar goes well in a [`crate::TopBottomPanel::top`],
/// The menu bar goes well in a [`crate::Panel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
///
@@ -219,9 +219,6 @@ pub struct MenuBar {
style: StyleModifier,
}
#[deprecated = "Renamed to `egui::MenuBar`"]
pub type Bar = MenuBar;
impl Default for MenuBar {
fn default() -> Self {
Self {

View File

@@ -9,7 +9,6 @@ mod combo_box;
pub mod frame;
pub mod menu;
pub mod modal;
pub mod old_popup;
pub mod panel;
mod popup;
pub(crate) mod resize;
@@ -26,7 +25,6 @@ pub use {
combo_box::*,
frame::Frame,
modal::{Modal, ModalResponse},
old_popup::*,
panel::*,
popup::*,
resize::Resize,
@@ -34,5 +32,5 @@ pub use {
scroll_area::ScrollArea,
sides::Sides,
tooltip::*,
window::Window,
window::{Window, WindowDrag},
};

View File

@@ -1,212 +0,0 @@
//! Old and deprecated API for popups. Use [`Popup`] instead.
#![expect(deprecated)]
use crate::containers::tooltip::Tooltip;
use crate::{
Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect,
Response, Ui, Widget as _, WidgetText,
};
use emath::RectAlign;
// ----------------------------------------------------------------------------
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
/// See also [`show_tooltip_text`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// # #[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");
/// });
/// }
/// # });
/// ```
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents)
}
/// Show a tooltip at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_ui`].
///
/// See also [`show_tooltip_text`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| {
/// ui.label("Helpful text");
/// });
/// }
/// # });
/// ```
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_at_pointer<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer)
.gap(12.0)
.show(add_contents)
.map(|response| response.inner)
}
/// Show a tooltip under the given area.
///
/// If the tooltip does not fit under the area, it tries to place it above it instead.
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_for<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
widget_rect: &Rect,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect)
.show(add_contents)
.map(|response| response.inner)
}
/// Show a tooltip at the given position.
///
/// Returns `None` if the tooltip could not be placed.
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_at<R>(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
suggested_position: Pos2,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position)
.show(add_contents)
.map(|response| response.inner)
}
/// Show some text at the current pointer position (if any).
///
/// Most of the time it is easier to use [`Response::on_hover_text`].
///
/// See also [`show_tooltip`].
///
/// Returns `None` if the tooltip could not be placed.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// if ui.ui_contains_pointer() {
/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text");
/// }
/// # });
/// ```
#[deprecated = "Use `egui::Tooltip` instead"]
pub fn show_tooltip_text(
ctx: &Context,
parent_layer: LayerId,
widget_id: Id,
text: impl Into<WidgetText>,
) -> Option<()> {
show_tooltip(ctx, parent_layer, widget_id, |ui| {
crate::widgets::Label::new(text).ui(ui);
})
}
/// Was this tooltip visible last frame?
#[deprecated = "Use `Tooltip::was_tooltip_open_last_frame` instead"]
pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool {
Tooltip::was_tooltip_open_last_frame(ctx, widget_id)
}
/// Indicate whether a popup will be shown above or below the box.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AboveOrBelow {
Above,
Below,
}
/// Helper for [`popup_above_or_below_widget`].
#[deprecated = "Use `egui::Popup` instead"]
pub fn popup_below_widget<R>(
ui: &Ui,
popup_id: Id,
widget_response: &Response,
close_behavior: PopupCloseBehavior,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
popup_above_or_below_widget(
ui,
popup_id,
widget_response,
AboveOrBelow::Below,
close_behavior,
add_contents,
)
}
/// Shows a popup above or below another widget.
///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.
///
/// The opened popup will have a minimum width matching its parent.
///
/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`].
///
/// Returns `None` if the popup is not open.
///
/// ```
/// # egui::__run_test_ui(|ui| {
/// let response = ui.button("Open popup");
/// let popup_id = ui.make_persistent_id("my_unique_id");
/// if response.clicked() {
/// ui.memory_mut(|mem| mem.toggle_popup(popup_id));
/// }
/// let below = egui::AboveOrBelow::Below;
/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside;
/// # #[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:");
/// ui.label("…");
/// });
/// # });
/// ```
#[deprecated = "Use `egui::Popup` instead"]
pub fn popup_above_or_below_widget<R>(
_parent_ui: &Ui,
popup_id: Id,
widget_response: &Response,
above_or_below: AboveOrBelow,
close_behavior: PopupCloseBehavior,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let response = Popup::from_response(widget_response)
.layout(Layout::top_down_justified(Align::LEFT))
.open_memory(None)
.close_behavior(close_behavior)
.id(popup_id)
.align(match above_or_below {
AboveOrBelow::Above => RectAlign::TOP_START,
AboveOrBelow::Below => RectAlign::BOTTOM_START,
})
.width(widget_response.rect.width())
.show(|ui| {
ui.set_min_width(ui.available_width());
add_contents(ui)
})?;
Some(response.inner)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
#![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};
@@ -87,7 +85,7 @@ pub enum PopupCloseBehavior {
/// but in the popup's body
CloseOnClickOutside,
/// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_all_popups`]
/// Clicks will be ignored. Popup might be closed manually by calling [`Popup::close_all`]
/// or by pressing the escape button
IgnoreClicks,
}
@@ -503,17 +501,15 @@ impl<'a> Popup<'a> {
RectAlign::find_best_align(
#[expect(clippy::iter_on_empty_collections)]
#[expect(clippy::or_fun_call)]
once(self.rect_align).chain(
std::iter::chain(
once(self.rect_align),
self.alternative_aligns
// Need the empty slice so the iters have the same type so we can unwrap_or
.map(|a| a.iter().copied().chain([].iter().copied()))
.unwrap_or(
self.rect_align
.symmetries()
.iter()
.copied()
.chain(RectAlign::MENU_ALIGNS.iter().copied()),
),
.map(|a| std::iter::chain(a.iter().copied(), [].iter().copied()))
.unwrap_or(std::iter::chain(
self.rect_align.symmetries().iter().copied(),
RectAlign::MENU_ALIGNS.iter().copied(),
)),
),
self.ctx.content_rect(),
anchor_rect,
@@ -715,10 +711,6 @@ impl Popup<'_> {
}
/// 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));
}

View File

@@ -1,6 +1,6 @@
use crate::{
Align2, Color32, Context, CursorIcon, Id, NumExt as _, Rect, Response, Sense, Shape, Ui,
UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
Align2, AsIdSalt, Color32, Context, CursorIcon, Id, IdSalt, NumExt as _, Rect, Response, Sense,
Shape, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, Vec2b, pos2, vec2,
};
#[derive(Clone, Copy, Debug)]
@@ -17,6 +17,13 @@ pub(crate) struct State {
/// Externally requested size (e.g. by Window) for the next frame
pub(crate) requested_size: Option<Vec2>,
/// Minimum content width measured by a sizing pass at the start of the current
/// interactive resize. We clamp `desired_size.x` against this for the rest of
/// the drag so the user can't shrink the window past what the content actually
/// needs. Reset to `None` whenever a drag is not in progress.
#[cfg_attr(feature = "serde", serde(default))]
min_content_width: Option<f32>,
}
impl State {
@@ -34,7 +41,7 @@ impl State {
#[must_use = "You should call .show()"]
pub struct Resize {
id: Option<Id>,
id_salt: Option<Id>,
id_salt: Option<IdSalt>,
/// If false, we are no enabled
resizable: Vec2b,
@@ -42,7 +49,7 @@ pub struct Resize {
pub(crate) min_size: Vec2,
pub(crate) max_size: Vec2,
default_size: Vec2,
pub(crate) default_size: Vec2,
with_stroke: bool,
}
@@ -69,17 +76,10 @@ impl Resize {
self
}
/// A source for the unique [`Id`], e.g. `.id_source("second_resize_area")` or `.id_source(loop_index)`.
#[inline]
#[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_resize_area")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
self.id_salt = Some(IdSalt::new(id_salt));
self
}
@@ -202,13 +202,14 @@ struct Prepared {
corner_id: Option<Id>,
state: State,
content_ui: Ui,
sizing_pass: bool,
}
impl Resize {
fn begin(&self, ui: &mut Ui) -> Prepared {
let position = ui.available_rect_before_wrap().min;
let id = self.id.unwrap_or_else(|| {
let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize"));
let id_salt = self.id_salt.unwrap_or_else(|| IdSalt::new("resize"));
ui.make_persistent_id(id_salt)
});
@@ -228,6 +229,7 @@ impl Resize {
desired_size: default_size,
last_content_size: vec2(0.0, 0.0),
requested_size: None,
min_content_width: None,
}
});
@@ -249,13 +251,25 @@ impl Resize {
user_requested_size = Some(pointer_pos - position + 0.5 * corner_response.rect.size());
}
if let Some(user_requested_size) = user_requested_size {
let is_actively_resizing = user_requested_size.is_some();
// Drag just started: we don't yet know what the content's minimum width is.
// Run a one-frame sizing pass below to discover it.
let needs_sizing_pass = is_actively_resizing && state.min_content_width.is_none();
if let Some(mut user_requested_size) = user_requested_size {
if let Some(min_width) = state.min_content_width {
user_requested_size.x = user_requested_size.x.at_least(min_width);
}
state.desired_size = user_requested_size;
} else {
// We are not being actively resized, so auto-expand to include size of last frame.
// This prevents auto-shrinking if the contents contain width-filling widgets (separators etc)
// but it makes a lot of interactions with [`Window`]s nicer.
state.desired_size = state.desired_size.max(state.last_content_size);
// Drag ended (if any). Forget the cached min so the next drag re-measures it,
// in case content changed.
state.min_content_width = None;
}
state.desired_size = state
@@ -265,7 +279,15 @@ impl Resize {
// ------------------------------
let inner_rect = Rect::from_min_size(position, state.desired_size);
// For the sizing pass, offer the tightest possible rect so widgets shrink to
// their natural minimum. We render the frame invisibly and discard it so the
// user never sees the squished layout; the measured min then clamps drags.
let inner_rect = if needs_sizing_pass {
ui.ctx().request_discard("Resize sizing pass");
Rect::from_min_size(position, Vec2::new(self.min_size.x, state.desired_size.y))
} else {
Rect::from_min_size(position, state.desired_size)
};
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
@@ -280,11 +302,13 @@ impl Resize {
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region
let mut content_ui = ui.new_child(
UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::Resize))
.max_rect(inner_rect),
);
let mut ui_builder = UiBuilder::new()
.ui_stack_info(UiStackInfo::new(UiKind::Resize))
.max_rect(inner_rect);
if needs_sizing_pass {
ui_builder = ui_builder.sizing_pass();
}
let mut content_ui = ui.new_child(ui_builder);
content_ui.set_clip_rect(content_clip_rect);
Prepared {
@@ -292,6 +316,7 @@ impl Resize {
corner_id,
state,
content_ui,
sizing_pass: needs_sizing_pass,
}
}
@@ -308,9 +333,17 @@ impl Resize {
corner_id,
mut state,
content_ui,
sizing_pass,
} = prepared;
state.last_content_size = content_ui.min_size();
if sizing_pass {
// Remember the measured minimum so we can clamp the user's drag on subsequent frames.
// Don't touch `last_content_size`, it should keep reflecting the previously
// rendered content.
state.min_content_width = Some(content_ui.min_size().x);
} else {
state.last_content_size = content_ui.min_size();
}
// ------------------------------

View File

@@ -8,9 +8,9 @@ use emath::GuiRounding as _;
use epaint::{Color32, Direction, Margin, Shape};
use crate::{
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,
AsIdSalt, Context, CursorIcon, Id, IdSalt, 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)]
@@ -141,6 +141,51 @@ impl ScrollBarVisibility {
];
}
/// When [`ScrollArea`] should let the user scroll by dragging the content.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum DragScroll {
/// Never scroll on pointer drag.
Never,
/// Only allow drag-to-scroll when a touch screen is detected
/// (see [`crate::InputState::has_touch_screen`]). The recommended default.
#[default]
OnTouch,
/// Always allow drag-to-scroll, even with a mouse.
Always,
}
impl DragScroll {
/// Whether drag-to-scroll is currently active.
///
/// Checks if we have a touch screen (via [`crate::InputState::has_touch_screen`])
/// when `self` is [`Self::OnTouch`].
pub fn enabled(self, ctx: &Context) -> bool {
match self {
Self::Never => false,
Self::OnTouch => ctx.input(|i| i.has_touch_screen()),
Self::Always => true,
}
}
}
impl BitOr for DragScroll {
type Output = Self;
/// Combine two settings, picking the more permissive one.
/// `Always > OnTouch > Never`.
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Self::Always, _) | (_, Self::Always) => Self::Always,
(Self::OnTouch, _) | (_, Self::OnTouch) => Self::OnTouch,
(Self::Never, Self::Never) => Self::Never,
}
}
}
/// What is the source of scrolling for a [`ScrollArea`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -152,7 +197,11 @@ pub struct ScrollSource {
pub scroll_bar: bool,
/// Scroll the area by dragging the contents.
pub drag: bool,
///
/// Defaults to [`DragScroll::OnTouch`]: only active when a touch screen is
/// detected. Set to [`DragScroll::Always`] to force it on, or
/// [`DragScroll::Never`] to disable.
pub drag: DragScroll,
/// Scroll the area by scrolling (or shift scrolling) the mouse wheel with
/// the mouse cursor over the [`ScrollArea`].
@@ -160,35 +209,40 @@ pub struct ScrollSource {
}
impl Default for ScrollSource {
/// `scroll_bar` and `mouse_wheel` enabled; `drag` set to [`DragScroll::OnTouch`].
fn default() -> Self {
Self::ALL
Self {
scroll_bar: true,
drag: DragScroll::OnTouch,
mouse_wheel: true,
}
}
}
impl ScrollSource {
pub const NONE: Self = Self {
scroll_bar: false,
drag: false,
drag: DragScroll::Never,
mouse_wheel: false,
};
pub const ALL: Self = Self {
scroll_bar: true,
drag: true,
drag: DragScroll::Always,
mouse_wheel: true,
};
pub const SCROLL_BAR: Self = Self {
scroll_bar: true,
drag: false,
drag: DragScroll::Never,
mouse_wheel: false,
};
pub const DRAG: Self = Self {
scroll_bar: false,
drag: true,
drag: DragScroll::Always,
mouse_wheel: false,
};
pub const MOUSE_WHEEL: Self = Self {
scroll_bar: false,
drag: false,
drag: DragScroll::Never,
mouse_wheel: true,
};
@@ -201,13 +255,13 @@ impl ScrollSource {
/// Is anything enabled?
#[inline]
pub fn any(&self) -> bool {
self.scroll_bar | self.drag | self.mouse_wheel
self.scroll_bar || self.drag != DragScroll::Never || self.mouse_wheel
}
/// Is everything enabled?
#[inline]
pub fn is_all(&self) -> bool {
self.scroll_bar & self.drag & self.mouse_wheel
self.scroll_bar && self.drag == DragScroll::Always && self.mouse_wheel
}
}
@@ -290,7 +344,7 @@ pub struct ScrollArea {
min_scrolled_size: Vec2,
scroll_bar_visibility: ScrollBarVisibility,
scroll_bar_rect: Option<Rect>,
id_salt: Option<Id>,
id_salt: Option<IdSalt>,
offset_x: Option<f32>,
offset_y: Option<f32>,
on_hover_cursor: Option<CursorIcon>,
@@ -423,17 +477,10 @@ impl ScrollArea {
self
}
/// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`.
#[inline]
#[deprecated = "Renamed id_salt"]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
self.id_salt = Some(IdSalt::new(id_salt));
self
}
@@ -530,32 +577,6 @@ impl ScrollArea {
/// This can be used, for example, to optionally freeze scrolling while the user
/// 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.scroll_source = if enable {
ScrollSource::ALL
} else {
ScrollSource::NONE
};
self
}
/// Can the user drag the scroll area to scroll?
///
/// This is useful for touch screens.
///
/// 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.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 {
@@ -709,7 +730,7 @@ impl ScrollArea {
let ctx = ui.ctx().clone();
let id_salt = id_salt.unwrap_or_else(|| Id::new("scroll_area"));
let id_salt = id_salt.unwrap_or_else(|| IdSalt::new("scroll_area"));
let id = ui.make_persistent_id(id_salt);
ctx.check_for_id_clash(
id,
@@ -803,72 +824,74 @@ 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);
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));
let background_drag_response = if scroll_source.drag.enabled(ui.ctx())
&& ui.is_enabled()
&& state.content_is_too_large.any()
{
// Drag contents to scroll (for touch screens mostly).
// We must do this BEFORE adding content to the `ScrollArea`,
// or we will steal input from the widgets we contain.
let content_response_option = state
.interact_rect
.map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG));
if content_response_option
.as_ref()
.is_some_and(|response| response.dragged())
{
for d in 0..2 {
if direction_enabled[d] {
ui.input(|input| {
state.offset[d] -= input.pointer.delta()[d];
});
state.scroll_stuck_to_end[d] = false;
state.offset_target[d] = None;
}
}
} else {
// Apply the cursor velocity to the scroll area when the user releases the drag.
if content_response_option
.as_ref()
.is_some_and(|response| response.dragged())
.is_some_and(|response| response.drag_stopped())
{
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.
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);
}
// 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
};
content_response_option
} else {
None
};
// Scroll with an animation if we have a target offset (that hasn't been cleared by the code
// above).
@@ -1006,7 +1029,6 @@ impl ScrollArea {
let margin = self
.content_margin
.unwrap_or_else(|| ui.spacing().scroll.content_margin);
let direction_enabled = self.direction_enabled;
let mut prepared = self.begin(ui);
let id = prepared.id;
@@ -1020,17 +1042,13 @@ impl ScrollArea {
.inner;
let (content_size, state) = prepared.end(ui);
let output = ScrollAreaOutput {
ScrollAreaOutput {
inner,
id,
state,
content_size,
inner_rect,
};
paint_fade_areas(ui, &output, direction_enabled);
output
}
}
}
@@ -1063,6 +1081,8 @@ impl Prepared {
.ctx()
.pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
for d in 0..2 {
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
let mut delta = -scroll_delta.0[d];
@@ -1134,6 +1154,10 @@ impl Prepared {
ui.request_repaint();
}
}
if delta != 0.0 {
had_explicit_scroll_adjustment[d] = true;
}
}
// Restore scroll target meant for ScrollAreas up the stack (if any)
@@ -1232,11 +1256,18 @@ impl Prepared {
let scroll_style = ui.spacing().scroll;
// Reserve the scroll area before painting fades, because fade painting uses ui.min_rect().
ui.advance_cursor_after_rect(outer_rect);
paint_fade_areas_impl(ui, inner_rect, content_size, state.offset, direction_enabled);
// Paint the bars:
let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect);
for d in 0..2 {
// maybe force increase in offset to keep scroll stuck to end position
if stick_to_end[d] && state.scroll_stuck_to_end[d] {
// maybe force increase in offset to keep scroll stuck to end position,
// unless this axis had an explicit scroll adjustment.
if stick_to_end[d] && state.scroll_stuck_to_end[d] && !had_explicit_scroll_adjustment[d]
{
state.offset[d] = content_size[d] - inner_rect.size()[d];
}
@@ -1478,8 +1509,6 @@ impl Prepared {
}
}
ui.advance_cursor_after_rect(outer_rect);
if show_scroll_this_frame != state.show_scroll {
ui.request_repaint();
}
@@ -1488,16 +1517,25 @@ impl Prepared {
state.offset = state.offset.min(available_offset);
state.offset = state.offset.max(Vec2::ZERO);
let suppress_stuck_recompute = Vec2b::new(
had_explicit_scroll_adjustment[0] && state.offset_target[0].is_some(),
had_explicit_scroll_adjustment[1] && state.offset_target[1].is_some(),
);
// Is scroll handle at end of content, or is there no scrollbar
// yet (not enough content), but sticking is requested? If so, enter sticky mode.
// Only has an effect if stick_to_end is enabled but we save in
// state anyway so that entering sticky mode at an arbitrary time
// has appropriate effect.
// Keep explicit target requests from being reclassified as "still stuck" in the same
// frame, otherwise animated scroll-to requests never get a chance to pull away from the end.
state.scroll_stuck_to_end = Vec2b::new(
(state.offset[0] == available_offset[0])
|| (self.stick_to_end[0] && available_offset[0] < 0.0),
(state.offset[1] == available_offset[1])
|| (self.stick_to_end[1] && available_offset[1] < 0.0),
!suppress_stuck_recompute[0]
&& ((state.offset[0] == available_offset[0])
|| (stick_to_end[0] && available_offset[0] < 0.0)),
!suppress_stuck_recompute[1]
&& ((state.offset[1] == available_offset[1])
|| (stick_to_end[1] && available_offset[1] < 0.0)),
);
state.show_scroll = show_scroll_this_frame;
@@ -1516,7 +1554,13 @@ impl Prepared {
/// Only fades for axes where scrolling is enabled — otherwise stray overflow
/// in a non-scrollable axis would draw a permanent fade with nothing to scroll
/// away to.
fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>, direction_enabled: Vec2b) {
fn paint_fade_areas_impl(
ui: &Ui,
inner_rect: Rect,
content_size: Vec2,
offset: Vec2,
direction_enabled: Vec2b,
) {
let crate::style::ScrollFadeStyle {
strength,
size: fade_size,
@@ -1528,11 +1572,9 @@ fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>, direction_e
let bg = ui.stack().bg_color();
let offset = scroll_output.state.offset;
let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
let overflow = content_size - inner_rect.size();
let paint_rect = scroll_output
.inner_rect
let paint_rect = inner_rect
.intersect(ui.min_rect())
.expand(ui.visuals().clip_rect_margin);

View File

@@ -16,24 +16,6 @@ pub struct Tooltip<'a> {
}
impl Tooltip<'_> {
/// Show a tooltip that is always open.
#[deprecated = "Use `Tooltip::always_open` instead."]
pub fn new(
parent_widget: Id,
ctx: Context,
anchor: impl Into<PopupAnchor>,
parent_layer: LayerId,
) -> Self {
Self {
popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer)
.kind(PopupKind::Tooltip)
.gap(4.0)
.sense(Sense::hover()),
parent_layer,
parent_widget,
}
}
/// Show a tooltip that is always open.
pub fn always_open(
ctx: Context,

View File

@@ -1,16 +1,62 @@
// WARNING: the code in here is horrible. It is a behemoth that needs breaking up into simpler parts.
use std::sync::Arc;
use emath::GuiRounding as _;
use epaint::{CornerRadiusF32, RectShape};
use epaint::CornerRadiusF32;
use crate::collapsing_header::CollapsingState;
use crate::*;
use super::scroll_area::{ScrollBarVisibility, ScrollSource};
use super::scroll_area::{DragScroll, ScrollBarVisibility, ScrollSource};
use super::{Area, Frame, Resize, ScrollArea, area, resize};
/// Where the user can drag to move a [`Window`].
///
/// See [`Window::drag_area`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum WindowDrag {
/// Window cannot be moved by dragging.
///
/// [`Window::movable(false)`](Window::movable) forces this regardless of
/// what was passed to [`Window::drag_area`].
Off,
/// The user can drag the window from anywhere on its surface.
///
/// Good for touch screens, but can interfere with selecting / dragging
/// content inside the window when used with a mouse.
Anywhere,
/// Only the title bar accepts the move-drag gesture.
///
/// Windows without a title bar (see [`Window::title_bar`]) silently fall
/// back to [`Self::Anywhere`] — otherwise they'd be unmovable.
TitleBar,
/// [`Self::Anywhere`] when a touch screen is detected (see
/// [`crate::InputState::has_touch_screen`]); [`Self::TitleBar`] otherwise.
/// The recommended default.
#[default]
OnTouch,
}
impl WindowDrag {
/// Resolve [`Self::OnTouch`] to either [`Self::Anywhere`] or [`Self::TitleBar`]
/// based on whether a touch screen was detected.
fn resolve(self, ctx: &Context) -> Self {
match self {
Self::OnTouch => {
if ctx.input(|i| i.has_touch_screen()) {
Self::Anywhere
} else {
Self::TitleBar
}
}
other => other,
}
}
}
/// Builder for a floating window which can be dragged, closed, collapsed, resized and scrolled (off by default).
///
/// You can customize:
@@ -33,9 +79,9 @@ use super::{Area, Frame, Resize, ScrollArea, area, resize};
/// Note that this is NOT a native OS window.
/// To create a new native OS window, use [`crate::Context::show_viewport_deferred`].
#[must_use = "You should call .show()"]
pub struct Window<'open> {
title: WidgetText,
open: Option<&'open mut bool>,
pub struct Window<'a> {
title: Atoms<'a>,
open: Option<&'a mut bool>,
area: Area,
frame: Option<Frame>,
resize: Resize,
@@ -44,14 +90,16 @@ pub struct Window<'open> {
default_open: bool,
with_title_bar: bool,
fade_out: bool,
auto_sized: bool,
drag_area: WindowDrag,
}
impl<'open> Window<'open> {
impl<'a> Window<'a> {
/// The window title is used as a unique [`Id`] and must be unique, and should not change.
/// This is true even if you disable the title bar with `.title_bar(false)`.
/// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading);
pub fn new(title: impl IntoAtoms<'a>) -> Self {
let title: Atoms<'_> = title.into_atoms();
let area = Area::new(Id::new(title.text())).kind(UiKind::Window);
Self {
title,
@@ -61,12 +109,14 @@ impl<'open> Window<'open> {
resize: Resize::default()
.with_stroke(false)
.min_size([96.0, 32.0])
.default_size([340.0, 420.0]), // Default inner size of a window
scroll: ScrollArea::neither().auto_shrink(false),
.default_size([340.0, 420.0]), // Default outer size of a window (includes frame margins, stroke, and title bar)
scroll: ScrollArea::neither().auto_shrink(false).content_margin(0.0),
collapsible: true,
default_open: true,
with_title_bar: true,
fade_out: true,
auto_sized: false,
drag_area: WindowDrag::default(),
}
}
@@ -118,7 +168,7 @@ impl<'open> Window<'open> {
/// * If `*open == true`, the window will have a close button.
/// * If the close button is pressed, `*open` will be set to `false`.
#[inline]
pub fn open(mut self, open: &'open mut bool) -> Self {
pub fn open(mut self, open: &'a mut bool) -> Self {
self.open = Some(open);
self
}
@@ -142,12 +192,29 @@ impl<'open> Window<'open> {
}
/// If `false` the window will be immovable.
///
/// If `true`, you can move the window by dragging it.
/// Where you can drag to move the window is determined by [`Self::drag_area`].
#[inline]
pub fn movable(mut self, movable: bool) -> Self {
self.area = self.area.movable(movable);
self
}
/// Where the user can grab the window to move it.
///
/// Defaults to [`WindowDrag::OnTouch`]: drag anywhere on touch screens,
/// title bar only otherwise. See [`WindowDrag`] for details.
///
/// [`Self::movable(false)`](Self::movable) forces [`WindowDrag::Off`]
/// regardless of this setting. Windows without a title bar (see
/// [`Self::title_bar`]) fall back to [`WindowDrag::Anywhere`].
#[inline]
pub fn drag_area(mut self, drag_area: WindowDrag) -> Self {
self.drag_area = drag_area;
self
}
/// `order(Order::Foreground)` for a Window that should always be on top
#[inline]
pub fn order(mut self, order: Order) -> Self {
@@ -213,6 +280,9 @@ impl<'open> Window<'open> {
}
/// Set minimum size of the window, equivalent to calling both `min_width` and `min_height`.
///
/// The size refers to the *outer* window size, including the frame's `inner_margin`,
/// `outer_margin`, `stroke`, and the title bar.
#[inline]
pub fn min_size(mut self, min_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.min_size(min_size);
@@ -234,6 +304,9 @@ impl<'open> Window<'open> {
}
/// Set maximum size of the window, equivalent to calling both `max_width` and `max_height`.
///
/// The size refers to the *outer* window size, including the frame's `inner_margin`,
/// `outer_margin`, `stroke`, and the title bar.
#[inline]
pub fn max_size(mut self, max_size: impl Into<Vec2>) -> Self {
self.resize = self.resize.max_size(max_size);
@@ -262,7 +335,7 @@ impl<'open> Window<'open> {
self
}
/// Constrains this window to [`Context::screen_rect`].
/// Constrains this window to [`Context::content_rect`].
///
/// To change the area to constrain to, use [`Self::constrain_to`].
///
@@ -275,7 +348,7 @@ impl<'open> Window<'open> {
/// Constrain the movement of the window to the given rectangle.
///
/// For instance: `.constrain_to(ctx.screen_rect())`.
/// For instance: `.constrain_to(ctx.content_rect())`.
#[inline]
pub fn constrain_to(mut self, constrain_rect: Rect) -> Self {
self.area = self.area.constrain_to(constrain_rect);
@@ -320,6 +393,9 @@ impl<'open> Window<'open> {
}
/// Set initial size of the window.
///
/// The size refers to the *outer* window size, including frame margins, stroke,
/// and the title bar.
#[inline]
pub fn default_size(mut self, default_size: impl Into<Vec2>) -> Self {
let default_size: Vec2 = default_size.into();
@@ -345,6 +421,9 @@ impl<'open> Window<'open> {
}
/// Sets the window size and prevents it from being resized by dragging its edges.
///
/// The size refers to the *outer* window size, including the frame's `inner_margin`,
/// `outer_margin`, `stroke`, and the title bar.
#[inline]
pub fn fixed_size(mut self, size: impl Into<Vec2>) -> Self {
self.resize = self.resize.fixed_size(size);
@@ -399,6 +478,7 @@ impl<'open> Window<'open> {
pub fn auto_sized(mut self) -> Self {
self.resize = self.resize.auto_sized();
self.scroll = ScrollArea::neither();
self.auto_sized = true;
self
}
@@ -425,11 +505,13 @@ impl<'open> Window<'open> {
self
}
/// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
/// Controls scrolling the window by dragging the contents with the pointer.
///
/// See [`ScrollArea::drag_to_scroll`] for more.
/// Defaults to [`DragScroll::OnTouch`] — only active when a touch screen is detected.
///
/// See [`ScrollArea::scroll_source`] and [`DragScroll`] for more.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
pub fn drag_to_scroll(mut self, drag_to_scroll: DragScroll) -> Self {
self.scroll = self.scroll.scroll_source(ScrollSource {
drag: drag_to_scroll,
..Default::default()
@@ -473,13 +555,72 @@ impl Window<'_> {
default_open,
with_title_bar,
fade_out,
auto_sized,
drag_area: drag_area_setting,
} = self;
// `Window::movable(false)` (and `Area::movable(false)`) and
// `WindowDrag::Off` both mean "this window cannot be moved by
// dragging". Without a title bar, `TitleBar` mode would leave the
// window unmovable, so silently fall back to drag-anywhere instead.
let effective_drag = if !area.is_movable() || drag_area_setting == WindowDrag::Off {
WindowDrag::Off
} else if !with_title_bar {
WindowDrag::Anywhere
} else {
drag_area_setting.resolve(ctx)
};
// Make the area itself agree: keep its movable flag in sync with
// the resolved drag mode so resize behavior and `Area::begin`'s
// drag-from-anywhere handling don't disagree with the title-bar
// path. (Builder order shouldn't matter — `.drag_area(Off)` after
// `.movable(true)` and vice versa both end up here.)
let area = if effective_drag == WindowDrag::Off {
area.movable(false)
} else {
area
};
// Apply the previous frame's title-bar drag _before_ `Area::begin`
// loads the state. We can't apply it inside the content closure because
// `Area::end` writes the locally-captured `AreaState` back, overwriting
// any in-frame mutation.
//
// We deliberately leave `Area` with its normal `Sense::DRAG`: that way
// the area's widget still absorbs drag hit-tests over the body, so the
// resize-edge widgets aren't picked as the "closest drag" target when
// hovering anywhere in the window. The drag-from-anywhere move that
// `Area::begin` would then apply is undone right after `begin` for
// `WindowDrag::TitleBar`.
let title_drag_mode = effective_drag == WindowDrag::TitleBar;
let pivot_pos_before_begin = if title_drag_mode {
if let Some(resp) = ctx.read_response(area.id.with("__title_click"))
&& resp.dragged()
{
let delta = ctx.input(|i| i.pointer.delta());
if delta != Vec2::ZERO {
ctx.memory_mut(|mem| {
if let Some(state) = mem.areas_mut().get_mut(area.id)
&& let Some(pivot_pos) = state.pivot_pos.as_mut()
{
*pivot_pos += delta;
}
});
}
}
area::AreaState::load(ctx, area.id).and_then(|s| s.pivot_pos)
} else {
None
};
let style = ctx.global_style();
let header_color =
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 window_frame = frame.unwrap_or_else(|| Frame::window(&style));
// We apply the window margin by using the `ScrollArea::content_margin`.
let window_margin = window_frame.inner_margin;
let window_frame = window_frame.inner_margin(0.0);
let is_explicitly_closed = matches!(open, Some(false));
let is_open = !is_explicitly_closed || ctx.memory(|mem| mem.everything_is_visible());
@@ -507,64 +648,55 @@ impl Window<'_> {
let on_top = Some(area_layer_id) == ctx.top_layer_id();
let mut area = area.begin(ctx);
area.with_widget_info(|| WidgetInfo::labeled(WidgetType::Window, true, title.text()));
// Title-bar-drag mode: throw away any drag-from-anywhere movement
// `Area::begin` may have applied. The title-bar pre-begin step above
// already accounted for the title drag. We then re-run the same
// constrain+round step `Area::begin` does so the title-bar drag
// can't escape `constrain_rect` or reintroduce sub-pixel jitter.
if let Some(pre_begin_pivot) = pivot_pos_before_begin {
let constrain = area.constrain();
let constrain_rect = area.constrain_rect();
let state = area.state_mut();
state.pivot_pos = Some(pre_begin_pivot);
if constrain {
state.set_left_top_pos(
Context::constrain_window_rect_to_area(state.rect(), constrain_rect).min,
);
}
state.set_left_top_pos(area::round_area_position(ctx, state.left_top_pos()));
}
// 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 title_bar_inner_height = ctx
.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 _;
window_frame.corner_radius.ne = window_frame.corner_radius.ne.clamp(0, half_height);
window_frame.corner_radius.nw = window_frame.corner_radius.nw.clamp(0, half_height);
let title_content_spacing = if is_collapsed {
0.0
} else {
window_frame.stroke.width
};
(title_bar_inner_height, title_content_spacing)
} else {
(0.0, 0.0)
};
area.with_widget_info(|| {
WidgetInfo::labeled(
WidgetType::Window,
true,
title.text().as_deref().unwrap_or(""),
)
});
{
// Prevent window from becoming larger than the constrain rect.
// `resize.max_size` is still in outer-window coordinates here, matching `constrain_rect`.
let constrain_rect = area.constrain_rect();
let max_width = constrain_rect.width();
let max_height =
constrain_rect.height() - title_bar_height_with_margin - title_content_spacing;
let max_height = constrain_rect.height();
resize.max_size.x = resize.max_size.x.min(max_width);
resize.max_size.y = resize.max_size.y.min(max_height);
}
// First check for resize to avoid frame delay:
let last_frame_outer_rect = area.state().rect();
let resize_interaction = do_resize_interaction(
ctx,
possible,
area.id(),
area_layer_id,
last_frame_outer_rect,
window_frame,
);
// The user-supplied min/max/default sizes on `Window` refer to the *outer* window size
// (the total footprint, including frame margins, stroke, and title bar). `Resize` sizes
// the title bar + inner content area, so we subtract the extra frame margin (the part
// outside of `Resize`).
{
let margins = window_frame.total_margin().sum()
+ vec2(0.0, title_bar_height_with_margin + title_content_spacing);
resize_response(
resize_interaction,
ctx,
margins,
area_layer_id,
&mut area,
resize_id,
);
let frame_margin = window_frame.total_margin().sum();
resize.min_size = (resize.min_size - frame_margin).at_least(Vec2::ZERO);
resize.max_size = (resize.max_size - frame_margin).at_least(Vec2::ZERO);
resize.default_size = (resize.default_size - frame_margin).at_least(Vec2::ZERO);
}
let mut area_content_ui = area.content_ui(ctx);
if is_open {
// `Area` already takes care of fade-in animations,
// so we only need to handle fade-out animations here.
@@ -573,55 +705,41 @@ impl Window<'_> {
}
let content_inner = {
// BEGIN FRAME --------------------------------
let mut frame = window_frame.begin(&mut area_content_ui);
let show_close_button = open.is_some();
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)
}
})
let outer_response = window_frame.show(&mut area_content_ui, |ui| {
resize.show(ui, |ui| {
if with_title_bar {
title_ui(
ui,
title,
window_frame.inner_margin(window_margin),
&mut collapsing,
collapsible,
on_top,
open.as_deref_mut(),
auto_sized,
effective_drag == WindowDrag::TitleBar,
area_id,
);
}
collapsing
.show_body_unindented(ui, |ui| {
if scroll.is_any_scroll_enabled() {
scroll
.content_margin(window_margin)
.show(ui, add_contents)
.inner
} else {
crate::Frame::NONE
.inner_margin(window_margin)
.show(ui, add_contents)
.inner
}
})
.map(|inner| inner.inner)
})
.map_or((None, None), |ir| (Some(ir.inner), Some(ir.response)));
});
let outer_rect = frame.end(&mut area_content_ui).rect;
let outer_rect = outer_response.response.rect;
// Do resize interaction _again_, to move their widget rectangles on TOP of the rest of the window.
let resize_interaction = do_resize_interaction(
@@ -629,7 +747,7 @@ impl Window<'_> {
possible,
area.id(),
area_layer_id,
last_frame_outer_rect,
outer_rect,
window_frame,
);
@@ -641,50 +759,25 @@ impl Window<'_> {
resize_interaction,
);
// END FRAME --------------------------------
{
let margins = window_frame.total_margin().sum();
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,
resize_response(
resize_interaction,
ctx,
margins,
area_layer_id,
&mut area,
resize_id,
);
}
// END FRAME --------------------------------
collapsing.store(ctx);
paint_frame_interaction(&area_content_ui, outer_rect, resize_interaction);
content_inner
outer_response.inner
};
let full_response = area.end(ctx, area_content_ui);
@@ -992,7 +1085,7 @@ fn do_resize_interaction(
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| {
let vertical_rect = |a: Pos2, b: Pos2| {
Rect::from_min_max(a, b).expand2(vec2(side_grab_radius, -corner_grab_radius))
};
let horizontal_rect = |a: Pos2, b: Pos2| {
@@ -1009,14 +1102,14 @@ fn do_resize_interaction(
if possible.resize_right {
let response = side_response(
vetrtical_rect(rect.right_top(), rect.right_bottom()),
vertical_rect(rect.right_top(), rect.right_bottom()),
id.with("right"),
);
right |= response;
}
if possible.resize_left {
let response = side_response(
vetrtical_rect(rect.left_top(), rect.left_bottom()),
vertical_rect(rect.left_top(), rect.left_bottom()),
id.with("left"),
);
left |= response;
@@ -1177,176 +1270,175 @@ fn paint_frame_interaction(ui: &Ui, rect: Rect, interaction: ResizeInteraction)
// ----------------------------------------------------------------------------
struct TitleBar {
window_frame: Frame,
/// Show the window titlebar.
///
/// Should be placed inside a `Frame::window`. The [`Frame`] it was placed inside should be passed as
/// an arg and will be used to paint the divider line at the bottom and the highlighted background
/// when `active` is true.
#[expect(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
fn title_ui(
ui: &mut Ui,
mut title: Atoms<'_>,
frame: Frame,
collapsing: &mut CollapsingState,
collapsible: bool,
active: bool,
open: Option<&mut bool>,
auto_sized: bool,
drag_to_move: bool,
area_id: Id,
) -> Response {
let shape_idx = ui.painter().add(Shape::Noop);
/// Prepared text in the title
title_galley: Arc<Galley>,
let mut atoms = Atoms::default();
/// Size of the title bar in an expanded state. This size become known only
/// after expanding window and painting its content.
///
/// Does not include the stroke, nor the separator line between the title bar and the window contents.
inner_rect: Rect,
}
let button_size = Vec2::splat(ui.spacing().icon_width);
impl TitleBar {
fn new(
ui: &Ui,
title: WidgetText,
show_close_button: bool,
collapsible: bool,
window_frame: Frame,
title_bar_height_with_margin: f32,
) -> Self {
if false {
ui.debug_painter()
.debug_rect(ui.min_rect(), Color32::GREEN, "outer_min_rect");
}
// Since the heading height is higher than the button size, we need to allocate the buttons
// with the headers height as size, otherwise they'd look slightly off-center.
// The shrink is then used to render the buttons with the right size.
let heading_font_height =
ui.fonts_mut(|f| f.row_height(&TextStyle::Heading.resolve(ui.style())));
let button_allocation_size = Vec2::splat(heading_font_height);
let button_shrink = (button_allocation_size - button_size) / 2.0;
let inner_height = title_bar_height_with_margin - window_frame.inner_margin.sum().y;
let collapse_atom_id = Id::new("__window_collapse_button");
let close_atom_id = Id::new("__window_close_button");
let item_spacing = ui.spacing().item_spacing;
let button_size = Vec2::splat(ui.spacing().icon_width.at_most(inner_height));
let expanded = collapsing.openness(ui.ctx()) > 0.0;
let left_pad = ((inner_height - button_size.y) / 2.0).round_ui(); // calculated so that the icon is on the diagonal (if window padding is symmetrical)
if collapsible {
atoms.push_right(Atom::custom(collapse_atom_id, button_allocation_size));
}
let title_galley = title.into_galley(
ui,
Some(crate::TextWrapMode::Extend),
f32::INFINITY,
TextStyle::Heading,
);
atoms.push_right(Atom::grow());
let minimum_width = if collapsible || show_close_button {
// If at least one button is shown we make room for both buttons (since title should be centered):
2.0 * (left_pad + button_size.x + item_spacing.x) + title_galley.size().x
if !auto_sized
&& !title.any_shrink()
&& let Some(first_text) = title
.iter_mut()
.find(|a| matches!(a.kind, AtomKind::Text(..)))
{
first_text.shrink = true;
}
atoms.extend_right(title);
atoms.push_right(Atom::grow());
if open.is_some() {
atoms.push_right(Atom::custom(close_atom_id, button_allocation_size));
}
let spacing = ui.spacing().item_spacing.x;
let mut child_ui = ui.new_child(UiBuilder::new());
let mut layout = AtomLayout::new(atoms)
.gap(spacing)
.fallback_font(TextStyle::Heading)
.wrap_mode(TextWrapMode::Truncate)
.frame(Frame::NONE.inner_margin(frame.inner_margin));
let frame = frame.inner_margin(0); // Only applied to the atoms; done above.
if expanded {
let min_width = if auto_sized {
// During auto size, the resize is essentially disabled, meaning we don't get an
// available_width we can rely on. Instead, check of large the content grew last frame
// and use that for sizing the title bar. Unfortunately this adds a frame delay.
ui.response().rect.width()
} else {
left_pad + title_galley.size().x + left_pad
child_ui.available_width()
};
let min_inner_size = vec2(minimum_width, inner_height);
let min_rect = Rect::from_min_size(ui.min_rect().min, min_inner_size);
if false {
ui.debug_painter()
.debug_rect(min_rect, Color32::LIGHT_BLUE, "min_rect");
}
Self {
window_frame,
title_galley,
inner_rect: min_rect, // First estimate - will be refined later
}
layout = layout.min_size(Vec2::new(min_width, 0.0));
}
/// Finishes painting of the title bar when the window content size already known.
///
/// # Parameters
///
/// - `ui`:
/// - `outer_rect`:
/// - `content_response`: if `None`, window is collapsed at this frame, otherwise contains
/// a result of rendering the window content
/// - `open`: if `None`, no "Close" button will be rendered, otherwise renders and processes
/// the "Close" button and writes a `false` if window was closed
/// - `collapsing`: holds the current expanding state. Can be changed by double click on the
/// title if `collapsible` is `true`
/// - `collapsible`: if `true`, double click on the title bar will be handled for a change
/// of `collapsing` state
fn ui(
self,
ui: &mut Ui,
content_response: &Option<Response>,
open: Option<&mut bool>,
collapsing: &mut CollapsingState,
collapsible: bool,
) {
let window_frame = self.window_frame;
let title_inner_rect = self.inner_rect;
let layout_response = layout.show(&mut child_ui);
if false {
ui.debug_painter()
.debug_rect(self.inner_rect, Color32::RED, "TitleBar");
}
let mut title_click_rect = layout_response.response.rect + frame.total_margin();
if collapsible {
// Show collapse-button:
let button_center = Align2::LEFT_CENTER
.align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
.center();
let button_size = Vec2::splat(ui.spacing().icon_width);
let button_rect = Rect::from_center_size(button_center, button_size);
let button_rect = button_rect.round_ui();
ui.scope_builder(UiBuilder::new().max_rect(button_rect), |ui| {
collapsing.show_default_button_with_size(ui, button_size);
});
}
if let Some(open) = open {
// Add close button now that we know our full width:
if self.close_button_ui(ui).clicked() {
*open = false;
}
}
let text_pos =
emath::align::center_size_in_rect(self.title_galley.size(), title_inner_rect)
.left_top();
let text_pos = text_pos - self.title_galley.rect.min.to_vec2();
ui.painter().galley(
text_pos,
Arc::clone(&self.title_galley),
ui.visuals().text_color(),
// Collapse triangle icon
if collapsible && let Some(rect) = layout_response.rect(collapse_atom_id) {
let rect = rect.shrink2(button_shrink);
title_click_rect = title_click_rect.with_min_x(rect.max.x);
let icon_response = child_ui.interact(
rect,
child_ui.auto_id_with("collapse_button"),
Sense::click(),
);
if let Some(content_response) = &content_response {
// Paint separator between title and content:
let content_rect = content_response.rect;
if false {
ui.debug_painter()
.debug_rect(content_rect, Color32::RED, "content_rect");
}
let y = title_inner_rect.bottom() + window_frame.stroke.width / 2.0;
// To verify the sanity of this, use a very wide window stroke
ui.painter()
.hline(title_inner_rect.x_range(), y, window_frame.stroke);
icon_response.widget_info(|| {
WidgetInfo::labeled(
WidgetType::Button,
child_ui.is_enabled(),
if collapsing.is_open() { "Hide" } else { "Show" },
)
});
if icon_response.clicked() {
collapsing.toggle(&child_ui);
}
let openness = collapsing.openness(child_ui.ctx());
crate::collapsing_header::paint_default_icon(&mut child_ui, openness, &icon_response);
}
// Don't cover the close- and collapse buttons:
let double_click_rect = title_inner_rect.shrink2(vec2(32.0, 0.0));
if false {
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)
.double_clicked()
&& collapsible
{
collapsing.toggle(ui);
// Close button
if let Some(open) = open
&& let Some(rect) = layout_response.rect(close_atom_id)
{
let rect = rect.shrink2(button_shrink);
title_click_rect = title_click_rect.with_max_x(rect.min.x);
if close_button(&mut child_ui, rect).clicked() {
*open = false;
}
}
/// Paints the "Close" button at the right side of the title bar
/// and processes clicks on it.
///
/// The button is square and its size is determined by the
/// [`crate::style::Spacing::icon_width`] setting.
fn close_button_ui(&self, ui: &mut Ui) -> Response {
let button_center = Align2::RIGHT_CENTER
.align_size_within_rect(Vec2::splat(self.inner_rect.height()), self.inner_rect)
.center();
let button_size = Vec2::splat(ui.spacing().icon_width);
let button_rect = Rect::from_center_size(button_center, button_size);
let button_rect = button_rect.round_to_pixels(ui.pixels_per_point());
close_button(ui, button_rect)
if collapsible || drag_to_move {
// Single widget covers double-click-to-toggle (when collapsible) and
// drag-to-move (in title-bar-drag mode). The move itself is applied in
// `Window::show_dyn` _before_ `Area::begin` next frame, since
// `Area::end` overwrites any in-frame mutation of `AreaState`.
let sense = if drag_to_move {
Sense::click_and_drag()
} else {
Sense::click()
};
let response = child_ui.interact(title_click_rect, area_id.with("__title_click"), sense);
if collapsible && response.double_clicked() {
collapsing.toggle(&child_ui);
}
}
{
let mut header_frame = frame.shadow(Shadow::NONE);
if active {
header_frame = header_frame.fill(ui.visuals().widgets.open.weak_bg_fill);
}
if expanded {
header_frame.corner_radius.sw = 0;
header_frame.corner_radius.se = 0;
}
ui.painter()
.set(shape_idx, header_frame.paint(layout_response.rect));
}
let mut advance_rect = child_ui.min_rect();
if auto_sized {
// We may not allocate in the horizontal direction as that would break auto sizing.
// Allocate a rect with 0 width:
advance_rect = advance_rect.with_max_x(advance_rect.min.x);
}
if expanded {
// Account for the margin of the title frame + the margin of the window contents
// - the default ui spacing egui would add on this call
advance_rect.max.y += frame.total_margin().bottom + frame.inner_margin.top as f32
- child_ui.spacing().item_spacing.y;
}
ui.advance_cursor_after_rect(advance_rect);
layout_response.response
}
/// Paints the "Close" button of the window and processes clicks on it.

View File

@@ -300,7 +300,7 @@ impl RepaintCause {
struct ViewportRepaintInfo {
/// Monotonically increasing counter.
///
/// Incremented at the end of [`Context::run`].
/// Incremented at the end of [`Context::run_ui`].
/// This can be smaller than [`Self::cumulative_pass_nr`],
/// but never larger.
cumulative_frame_nr: u64,
@@ -463,7 +463,7 @@ impl ContextImpl {
let content_rect = viewport.input.content_rect();
viewport.this_pass.begin_pass(content_rect);
viewport.this_pass.begin_pass();
{
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
@@ -641,11 +641,7 @@ impl ContextImpl {
}
fn all_viewport_ids(&self) -> ViewportIdSet {
self.viewports
.keys()
.copied()
.chain([ViewportId::ROOT])
.collect()
std::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect()
}
/// The current active viewport
@@ -697,8 +693,8 @@ impl ContextImpl {
/// // Game loop:
/// loop {
/// let raw_input = egui::RawInput::default();
/// let full_output = ctx.run(raw_input, |ctx| {
/// egui::CentralPanel::default().show(&ctx, |ui| {
/// let full_output = ctx.run_ui(raw_input, |ui| {
/// egui::CentralPanel::default().show(ui, |ui| {
/// ui.label("Hello world!");
/// if ui.button("Click me").clicked() {
/// // take some action here
@@ -780,9 +776,6 @@ impl Context {
/// });
/// // handle full_output
/// ```
///
/// ## See also
/// * [`Self::run`]
#[must_use]
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
self.run_ui_dyn(new_input, &mut run_ui)
@@ -791,60 +784,28 @@ impl Context {
#[must_use]
fn run_ui_dyn(&self, new_input: RawInput, run_ui: &mut dyn FnMut(&mut Ui)) -> FullOutput {
let plugins = self.read(|ctx| ctx.plugins.ordered_plugins());
#[expect(deprecated)]
self.run(new_input, |ctx| {
let mut top_ui = Ui::new(
self.run_dyn(new_input, &mut |ctx| {
let mut root_ui = Ui::new(
ctx.clone(),
Id::new((ctx.viewport_id(), "__top_ui")),
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(ctx.available_rect()),
.max_rect(ctx.viewport_rect()),
);
{
plugins.on_begin_pass(&mut top_ui);
run_ui(&mut top_ui);
plugins.on_end_pass(&mut top_ui);
plugins.on_begin_pass(&mut root_ui);
run_ui(&mut root_ui);
plugins.on_end_pass(&mut root_ui);
}
// Inform ctx about what we actually used, so we can shrink the native window to fit.
// TODO(emilk): make better use of this somehow
ctx.pass_state_mut(|state| state.allocate_central_panel(top_ui.min_rect()));
ctx.pass_state_mut(|state| {
state.root_ui_available_rect = Some(root_ui.available_rect_before_wrap());
state.root_ui_min_rect = Some(root_ui.min_rect());
});
})
}
/// Run the ui code for one frame.
///
/// At most [`Options::max_passes`] calls will be issued to `run_ui`,
/// and only on the rare occasion that [`Context::request_discard`] is called.
/// Usually, it `run_ui` will only be called once.
///
/// Put your widgets into a [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
///
/// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`].
///
/// ```
/// // One egui context that you keep reusing:
/// let mut ctx = egui::Context::default();
///
/// // Each frame:
/// let input = egui::RawInput::default();
/// let full_output = ctx.run(input, |ctx| {
/// egui::CentralPanel::default().show(&ctx, |ui| {
/// ui.label("Hello egui!");
/// });
/// });
/// // handle full_output
/// ```
///
/// ## See also
/// * [`Self::run_ui`]
#[must_use]
#[deprecated = "Call run_ui instead"]
pub fn run(&self, new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput {
self.run_dyn(new_input, &mut run_ui)
}
#[must_use]
fn run_dyn(&self, mut new_input: RawInput, run_ui: &mut dyn FnMut(&Self)) -> FullOutput {
profiling::function_scope!();
@@ -914,10 +875,10 @@ impl Context {
output
}
/// An alternative to calling [`Self::run`].
/// An alternative to calling [`Self::run_ui`].
///
/// It is usually better to use [`Self::run`], because
/// `run` supports multi-pass layout using [`Self::request_discard`].
/// It is usually better to use [`Self::run_ui`], because
/// `run_ui` supports multi-pass layout using [`Self::request_discard`].
///
/// ```
/// // One egui context that you keep reusing:
@@ -927,9 +888,7 @@ impl Context {
/// let input = egui::RawInput::default();
/// ctx.begin_pass(input);
///
/// egui::CentralPanel::default().show(&ctx, |ui| {
/// ui.label("Hello egui!");
/// });
/// // … add panels and windows here …
///
/// let full_output = ctx.end_pass();
/// // handle full_output
@@ -942,12 +901,6 @@ impl Context {
self.write(|ctx| ctx.begin_pass(new_input));
}
/// See [`Self::begin_pass`].
#[deprecated = "Renamed begin_pass"]
pub fn begin_frame(&self, new_input: RawInput) {
self.begin_pass(new_input);
}
}
/// ## Borrows parts of [`Context`]
@@ -1048,7 +1001,7 @@ impl Context {
/// Read-only access to [`PassState`].
///
/// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
/// This is only valid during the call to [`Self::run_ui`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
#[inline]
pub(crate) fn pass_state<R>(&self, reader: impl FnOnce(&PassState) -> R) -> R {
self.write(move |ctx| reader(&ctx.viewport().this_pass))
@@ -1056,7 +1009,7 @@ impl Context {
/// Read-write access to [`PassState`].
///
/// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
/// This is only valid during the call to [`Self::run_ui`] (between [`Self::begin_pass`] and [`Self::end_pass`]).
#[inline]
pub(crate) fn pass_state_mut<R>(&self, writer: impl FnOnce(&mut PassState) -> R) -> R {
self.write(move |ctx| writer(&mut ctx.viewport().this_pass))
@@ -1072,7 +1025,7 @@ impl Context {
/// Read-only access to [`Fonts`].
///
/// Not valid until first call to [`Context::run()`].
/// Not valid until first call to [`Context::run_ui()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts<R>(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R {
@@ -1089,7 +1042,7 @@ impl Context {
/// Read-write access to [`Fonts`].
///
/// Not valid until first call to [`Context::run()`].
/// Not valid until first call to [`Context::run_ui()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
#[inline]
pub fn fonts_mut<R>(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R {
@@ -1580,6 +1533,19 @@ impl Context {
self.output_mut(|o| o.cursor_icon = cursor_icon);
}
/// Request that the integration display this RGBA bitmap as the OS
/// cursor for the next frame, instead of the standard `cursor_icon`.
/// Backends that don't support custom cursors (web, eframe with
/// non-winit integrations) silently fall back to the icon.
///
/// Pass `None` to clear and revert to `cursor_icon` selection.
///
/// The integration is expected to dedupe by `Arc` pointer identity,
/// so reusing the same `Arc<[u8]>` across frames is cheap.
pub fn set_cursor_image(&self, image: Option<crate::CustomCursorImage>) {
self.output_mut(|o| o.cursor_image = image);
}
/// Add a command to [`PlatformOutput::commands`],
/// for the integration to execute at the end of the frame.
pub fn send_cmd(&self, cmd: crate::OutputCommand) {
@@ -1665,7 +1631,7 @@ impl Context {
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
/// Starts at zero, and is incremented once at the end of each call to [`Self::run_ui`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr`].
pub fn cumulative_frame_nr(&self) -> u64 {
@@ -1674,7 +1640,7 @@ impl Context {
/// The total number of completed frames.
///
/// Starts at zero, and is incremented once at the end of each call to [`Self::run`].
/// Starts at zero, and is incremented once at the end of each call to [`Self::run_ui`].
///
/// This is always smaller or equal to [`Self::cumulative_pass_nr_for`].
pub fn cumulative_frame_nr_for(&self, id: ViewportId) -> u64 {
@@ -1694,7 +1660,7 @@ impl Context {
/// The total number of completed passes (usually there is one pass per rendered frame).
///
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once).
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once).
///
/// If you instead want to know which pass index this is within the current frame,
/// use [`Self::current_pass_index`].
@@ -1704,7 +1670,7 @@ impl Context {
/// The total number of completed passes (usually there is one pass per rendered frame).
///
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once).
/// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once).
pub fn cumulative_pass_nr_for(&self, id: ViewportId) -> u64 {
self.read(|ctx| {
ctx.viewports
@@ -2079,7 +2045,7 @@ impl Context {
self.options(|opt| opt.theme())
}
/// The [`Theme`] used to select between dark and light [`Self::style`]
/// The [`Theme`] used to select between dark and light [`Self::global_style`]
/// as the active style used by all subsequent popups, menus, etc.
///
/// Example:
@@ -2096,12 +2062,6 @@ impl Context {
self.options(|opt| Arc::clone(opt.style()))
}
/// The currently active [`Style`] used by all subsequent popups, menus, etc.
#[deprecated = "Renamed to `global_style` to avoid confusion with `ui.style()`"]
pub fn style(&self) -> Arc<Style> {
self.options(|opt| Arc::clone(opt.style()))
}
/// Mutate the currently active [`Style`] used by all subsequent popups, menus, etc.
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
@@ -2116,21 +2076,6 @@ impl Context {
self.options_mut(|opt| mutate_style(Arc::make_mut(opt.style_mut())));
}
/// Mutate the currently active [`Style`] used by all subsequent popups, menus, etc.
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
/// Example:
/// ```
/// # let mut ctx = egui::Context::default();
/// ctx.global_style_mut(|style| {
/// style.spacing.item_spacing = egui::vec2(10.0, 20.0);
/// });
/// ```
#[deprecated = "Renamed to `global_style_mut` to avoid confusion with `ui.style_mut()`"]
pub fn style_mut(&self, mutate_style: impl FnOnce(&mut Style)) {
self.options_mut(|opt| mutate_style(Arc::make_mut(opt.style_mut())));
}
/// The currently active [`Style`] used by all new popups, menus, etc.
///
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
@@ -2142,18 +2087,6 @@ impl Context {
self.options_mut(|opt| *opt.style_mut() = style.into());
}
/// The currently active [`Style`] used by all new popups, menus, etc.
///
/// Use [`Self::all_styles_mut`] to mutate both dark and light mode styles.
///
/// You can also change this using [`Self::style_mut`].
///
/// You can use [`Ui::style_mut`] to change the style of a single [`Ui`].
#[deprecated = "Renamed to `set_global_style` to avoid confusion with `ui.set_style()`"]
pub fn set_style(&self, style: impl Into<Arc<Style>>) {
self.options_mut(|opt| *opt.style_mut() = style.into());
}
/// Mutate the [`Style`]s used by all subsequent popups, menus, etc. in both dark and light mode.
///
/// Example:
@@ -2417,17 +2350,11 @@ impl Context {
output
}
/// Call at the end of each frame if you called [`Context::begin_pass`].
#[must_use]
#[deprecated = "Renamed end_pass"]
pub fn end_frame(&self) -> FullOutput {
self.end_pass()
}
/// Called at the end of the pass.
#[cfg(debug_assertions)]
fn debug_painting(&self) {
#![expect(clippy::iter_over_hash_type)] // ok to be sloppy in debug painting
use std::fmt::Write as _;
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
let rect = widget.interact_rect;
@@ -2500,13 +2427,17 @@ impl Context {
for id in contains_pointer {
let mut widget_text = format!("{id:?}");
if let Some(rect) = widget_rects.get(id) {
widget_text +=
&format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense);
write!(
widget_text,
" {:?} {:?} {:?}",
rect.layer_id, rect.rect, rect.sense
)
.ok();
}
if let Some(info) = widget_rects.info(id) {
widget_text += &format!(" {info:?}");
write!(widget_text, " {info:?}").ok();
}
debug_text += &format!("{widget_text}\n");
writeln!(debug_text, "{widget_text}").ok();
}
self.debug_text(debug_text);
}
@@ -2571,7 +2502,7 @@ impl Context {
);
self.viewport(|vp| {
for reason in &vp.output.request_discard_reasons {
warning += &format!("\n {reason}");
write!(warning, "\n {reason}").ok();
}
});
@@ -2606,6 +2537,12 @@ impl ContextImpl {
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
if self.memory.should_interrupt_ime()
&& let Some(ime) = &mut platform_output.ime
{
ime.should_interrupt_composition = true;
}
{
profiling::scope!("accesskit");
let state = viewport.this_pass.accesskit_state.take();
@@ -2837,24 +2774,14 @@ impl Context {
self.input(|i| i.viewport_rect()).round_ui()
}
/// Position and size of the egui area.
#[deprecated(
note = "screen_rect has been split into viewport_rect() and content_rect(). You likely should use content_rect()"
)]
pub fn screen_rect(&self) -> Rect {
self.input(|i| i.content_rect()).round_ui()
}
/// How much space is still available after panels have been added.
#[deprecated = "Use content_rect (or viewport_rect) instead"]
pub fn available_rect(&self) -> Rect {
self.pass_state(|s| s.available_rect()).round_ui()
}
/// How much space is used by windows and the top-level [`Ui`].
pub fn globally_used_rect(&self) -> Rect {
self.write(|ctx| {
let mut used = ctx.viewport().this_pass.used_by_panels;
let viewport = ctx.viewport();
let root_ui_min_rect =
(viewport.this_pass.root_ui_min_rect).or(viewport.prev_pass.root_ui_min_rect);
let mut used = root_ui_min_rect.unwrap_or(Rect::NOTHING);
for (_id, window) in ctx.memory.areas().visible_windows() {
used |= window.rect();
}
@@ -2862,46 +2789,33 @@ impl Context {
})
}
/// How much space is used by windows and the top-level [`Ui`].
#[deprecated = "Renamed to globally_used_rect"]
pub fn used_rect(&self) -> Rect {
self.globally_used_rect()
}
/// How much space is used by windows and the top-level [`Ui`].
///
/// You can shrink your egui area to this size and still fit all egui components.
#[deprecated = "Use globally_used_rect instead"]
pub fn used_size(&self) -> Vec2 {
(self.globally_used_rect().max - Pos2::ZERO).round_ui()
}
// ---------------------------------------------------------------------
/// Is the pointer (mouse/touch) over any egui area?
pub fn is_pointer_over_egui(&self) -> bool {
let pointer_pos = self.input(|i| i.pointer.interact_pos());
if let Some(pointer_pos) = pointer_pos {
if let Some(layer) = self.layer_id_at(pointer_pos) {
if layer.order == Order::Background {
!self.pass_state(|state| state.unused_rect.contains(pointer_pos))
} else {
true
}
let Some(pointer_pos) = pointer_pos else {
return false;
};
let Some(layer) = self.layer_id_at(pointer_pos) else {
return false;
};
if layer.order == Order::Background {
let root_ui_available_rect = self
.pass_state(|state| state.root_ui_available_rect)
.or_else(|| self.prev_pass_state(|state| state.root_ui_available_rect));
if let Some(root_ui_available_rect) = root_ui_available_rect {
// Modern `run_ui` code
!root_ui_available_rect.contains(pointer_pos)
} else {
false
true // We shouldn't get here, but who knows
}
} else {
false
true
}
}
/// Is the pointer (mouse/touch) over any egui area?
#[deprecated = "Renamed to is_pointer_over_egui"]
pub fn is_pointer_over_area(&self) -> bool {
self.is_pointer_over_egui()
}
/// True if egui is currently interested in the pointer (mouse or touch).
///
/// Could be the pointer is hovering over a [`crate::Window`] or the user is dragging a widget.
@@ -2913,17 +2827,6 @@ impl Context {
|| (self.is_pointer_over_egui() && !self.input(|i| i.pointer.any_down()))
}
/// True if egui is currently interested in the pointer (mouse or touch).
///
/// Could be the pointer is hovering over a [`crate::Window`] or the user is dragging a widget.
/// If `false`, the pointer is outside of any egui area and so
/// you may be interested in what it is doing (e.g. controlling your game).
/// Returns `false` if a drag started outside of egui and then moved over an egui area.
#[deprecated = "Renamed to egui_wants_pointer_input"]
pub fn wants_pointer_input(&self) -> bool {
self.egui_wants_pointer_input()
}
/// Is egui currently using the pointer position (e.g. dragging a slider)?
///
/// NOTE: this will return `false` if the pointer is just hovering over an egui area.
@@ -2931,25 +2834,11 @@ impl Context {
self.memory(|m| m.interaction().is_using_pointer())
}
/// Is egui currently using the pointer position (e.g. dragging a slider)?
///
/// NOTE: this will return `false` if the pointer is just hovering over an egui area.
#[deprecated = "Renamed to egui_is_using_pointer"]
pub fn is_using_pointer(&self) -> bool {
self.egui_is_using_pointer()
}
/// If `true`, egui is currently listening on text input (e.g. typing text in a [`crate::TextEdit`]).
pub fn egui_wants_keyboard_input(&self) -> bool {
self.memory(|m| m.focused().is_some())
}
/// If `true`, egui is currently listening on text input (e.g. typing text in a [`crate::TextEdit`]).
#[deprecated = "Renamed to egui_wants_keyboard_input"]
pub fn wants_keyboard_input(&self) -> bool {
self.egui_wants_keyboard_input()
}
/// Is the currently focused widget a text edit?
pub fn text_edit_focused(&self) -> bool {
if let Some(id) = self.memory(|mem| mem.focused()) {
@@ -2969,18 +2858,6 @@ impl Context {
self.pass_state_mut(|fs| fs.highlight_next_pass.insert(id));
}
/// Is an egui context menu open?
///
/// This only works with the old, deprecated [`crate::menu`] API.
#[expect(deprecated)]
#[deprecated = "Use `any_popup_open` instead"]
pub fn is_context_menu_open(&self) -> bool {
self.data(|d| {
d.get_temp::<crate::menu::BarState>(crate::menu::CONTEXT_MENU_ID_STR.into())
.is_some_and(|state| state.has_root())
})
}
/// Is a popup or (context) menu open?
///
/// Will return false for [`crate::Tooltip`]s (which are technically popups as well).
@@ -2991,18 +2868,6 @@ impl Context {
.any(|layer| !layer.open_popups.is_empty())
})
}
/// Is a popup or (context) menu open?
///
/// Will return false for [`crate::Tooltip`]s (which are technically popups as well).
#[deprecated = "Renamed to any_popup_open"]
pub fn is_popup_open(&self) -> bool {
self.pass_state_mut(|fs| {
fs.layers
.values()
.any(|layer| !layer.open_popups.is_empty())
})
}
}
// Ergonomic methods to forward some calls often used in 'if let' without holding the borrow
@@ -3617,17 +3482,6 @@ impl Context {
}
});
#[expect(deprecated)]
ui.horizontal(|ui| {
ui.label(format!(
"{} menu bars",
self.data(|d| d.count::<crate::menu::BarState>())
));
if ui.button("Reset").clicked() {
self.data_mut(|d| d.remove_by_type::<crate::menu::BarState>());
}
});
ui.horizontal(|ui| {
ui.label(format!(
"{} scroll areas",
@@ -3980,8 +3834,8 @@ impl Context {
/// When called, the integration needs to:
/// * Check if there already is a window for this viewport id, and if not open one
/// * Set the window attributes (position, size, …) based on [`ImmediateViewport::builder`].
/// * Call [`Context::run`] with [`ImmediateViewport::viewport_ui_cb`].
/// * Handle the output from [`Context::run`], including rendering
/// * Call [`Context::run_ui`] with [`ImmediateViewport::viewport_ui_cb`].
/// * Handle the output from [`Context::run_ui`], including rendering
pub fn set_immediate_viewport_renderer(
callback: impl for<'a> Fn(&Self, ImmediateViewport<'a>) + 'static,
) {
@@ -4185,7 +4039,7 @@ impl Context {
/// ## Interaction
impl Context {
/// Read you what widgets are currently being interacted with.
/// Read which widgets are currently being interacted with.
pub fn interaction_snapshot<R>(&self, reader: impl FnOnce(&InteractionSnapshot) -> R) -> R {
self.write(|w| reader(&w.viewport().interact_widgets))
}

View File

@@ -12,7 +12,7 @@ use crate::{
/// Set the values that make sense, leave the rest at their `Default::default()`.
///
/// You can check if `egui` is using the inputs using
/// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`].
/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`].
///
/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner.
///
@@ -64,8 +64,8 @@ pub struct RawInput {
/// In-order events received this frame.
///
/// There is currently no way to know if egui handles a particular event,
/// but you can check if egui is using the keyboard with [`crate::Context::wants_keyboard_input`]
/// and/or the pointer (mouse/touch) with [`crate::Context::is_using_pointer`].
/// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`]
/// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`].
pub events: Vec<Event>,
/// Dragged files hovering over egui.
@@ -376,12 +376,14 @@ impl ViewportInfo {
ui.label(opt_as_str(&visible));
ui.end_row();
#[expect(clippy::ref_option)]
fn opt_rect_as_string(v: &Option<Rect>) -> String {
v.as_ref().map_or(String::new(), |r| {
format!("Pos: {:?}, size: {:?}", r.min, r.size())
})
}
#[expect(clippy::ref_option)]
fn opt_as_str<T: std::fmt::Debug>(v: &Option<T>) -> String {
v.as_ref().map_or(String::new(), |v| format!("{v:?}"))
}
@@ -613,15 +615,22 @@ pub enum Event {
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImeEvent {
/// Notifies when the IME was enabled.
#[deprecated = "No longer used by egui"]
Enabled,
/// A new IME candidate is being suggested.
///
/// An empty preedit string indicates that the IME has been dismissed, while
/// a non-empty preedit string indicates that the IME is active.
Preedit(String),
/// IME composition ended with this final result.
///
/// The IME is considered dismissed after this event.
Commit(String),
/// Notifies when the IME was disabled.
#[deprecated = "No longer used by egui"]
Disabled,
}

View File

@@ -2,7 +2,7 @@
use crate::{OrderedViewportIdMap, RepaintCause, ViewportOutput, WidgetType};
/// What egui emits each frame from [`crate::Context::run`].
/// What egui emits each frame from [`crate::Context::run_ui`].
///
/// The backend should use this.
#[derive(Clone, Default)]
@@ -79,6 +79,9 @@ pub struct IMEOutput {
///
/// This is a very thin rectangle.
pub cursor_rect: crate::Rect,
/// Whether any ongoing IME composition should be interrupted.
pub should_interrupt_composition: bool,
}
/// Commands that the egui integration should execute at the end of a frame.
@@ -113,6 +116,16 @@ pub struct PlatformOutput {
/// Set the cursor to this icon.
pub cursor_icon: CursorIcon,
/// If set, the integration should display this RGBA image as the OS
/// cursor (via e.g. `winit::window::CustomCursor`) instead of the
/// standard `cursor_icon`. Set per frame; integrations that don't
/// support custom cursors fall back to `cursor_icon`.
///
/// Skipped from serde because the bitmap is ephemeral and shouldn't
/// roundtrip through persisted state.
#[cfg_attr(feature = "serde", serde(skip))]
pub cursor_image: Option<CustomCursorImage>,
/// Events that may be useful to e.g. a screen reader.
pub events: Vec<OutputEvent>,
@@ -123,6 +136,9 @@ pub struct PlatformOutput {
/// This is set if, and only if, the user is currently editing text.
///
/// Useful for IME.
///
/// This field should only be set by the widget that currently owns IME
/// events (see [`crate::Memory::owns_ime_events`]).
pub ime: Option<IMEOutput>,
/// The difference in the widget tree since last frame.
@@ -171,6 +187,7 @@ impl PlatformOutput {
let Self {
mut commands,
cursor_icon,
cursor_image,
mut events,
mutable_text_under_cursor,
ime,
@@ -181,6 +198,7 @@ impl PlatformOutput {
self.commands.append(&mut commands);
self.cursor_icon = cursor_icon;
self.cursor_image = cursor_image;
self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor;
self.ime = ime.or(self.ime);
@@ -192,10 +210,12 @@ impl PlatformOutput {
self.accesskit_update = accesskit_update;
}
/// Take everything ephemeral (everything except `cursor_icon` currently)
/// Take everything ephemeral (everything except `cursor_icon` and
/// `cursor_image` currently)
pub fn take(&mut self) -> Self {
let taken = std::mem::take(self);
self.cursor_icon = taken.cursor_icon; // everything else is ephemeral
self.cursor_icon = taken.cursor_icon; // sticky between frames
self.cursor_image = taken.cursor_image.clone(); // sticky between frames
taken
}
@@ -255,6 +275,39 @@ pub enum UserAttentionType {
Reset,
}
/// A bitmap cursor pushed to the integration via [`PlatformOutput::cursor_image`].
///
/// The integration is expected to upload this to the OS as a real cursor
/// (so the image is not clipped by the egui window — what `egui::Painter`
/// drawn cursors suffer from). Backends that don't support it should fall
/// back to [`PlatformOutput::cursor_icon`].
///
/// `rgba` is straight (non-premultiplied) RGBA — same encoding as
/// `winit::window::CustomCursor::from_rgba`. The buffer length must be
/// exactly `size[0] * size[1] * 4` bytes. `size` and `hotspot` use
/// `u16` to match winit's native types and avoid a lossy cast in the
/// integration layer.
///
/// `Arc<[u8]>` is used so integrations can dedupe / cache by pointer
/// identity (`Arc::ptr_eq`) and avoid re-uploading the same bitmap to
/// the OS every frame.
#[derive(Clone, PartialEq, Eq)]
pub struct CustomCursorImage {
pub rgba: std::sync::Arc<[u8]>,
pub size: [u16; 2],
pub hotspot: [u16; 2],
}
impl std::fmt::Debug for CustomCursorImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomCursorImage")
.field("size", &self.size)
.field("hotspot", &self.hotspot)
.field("rgba_len", &self.rgba.len())
.finish()
}
}
/// A mouse cursor icon.
///
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.

View File

@@ -3,8 +3,8 @@ use std::sync::Arc;
use emath::GuiRounding as _;
use crate::{
Align2, Color32, Context, Id, InnerResponse, NumExt as _, Painter, Rect, Region, Style, Ui,
UiBuilder, Vec2, vec2,
Align2, AsIdSalt, Color32, Context, Id, IdSalt, InnerResponse, NumExt as _, Painter, Rect,
Region, Style, Ui, UiBuilder, Vec2, vec2,
};
#[cfg(debug_assertions)]
@@ -312,7 +312,7 @@ impl GridLayout {
/// ```
#[must_use = "You should call .show()"]
pub struct Grid {
id_salt: Id,
id_salt: IdSalt,
num_columns: Option<usize>,
min_col_width: Option<f32>,
min_row_height: Option<f32>,
@@ -324,9 +324,9 @@ pub struct Grid {
impl Grid {
/// Create a new [`Grid`] with a locally unique identifier.
pub fn new(id_salt: impl std::hash::Hash) -> Self {
pub fn new(id_salt: impl AsIdSalt) -> Self {
Self {
id_salt: Id::new(id_salt),
id_salt: IdSalt::new(id_salt),
num_columns: None,
min_col_width: None,
min_row_height: None,

View File

@@ -2,7 +2,7 @@ use ahash::HashMap;
use emath::TSTransform;
use crate::{LayerId, Pos2, Sense, WidgetRect, WidgetRects, ahash, emath, id::IdSet};
use crate::{LayerId, Pos2, Sense, WidgetRect, WidgetRects, emath, id::IdSet};
/// Result of a hit-test against [`WidgetRects`].
///

View File

@@ -2,6 +2,16 @@
use std::num::NonZeroU64;
use crate::{AsIdSalt, IdSalt};
/// Types that can be converted to an [`Id`].
///
/// This is all types implementing `Hash` and `Debug`,
/// which includes things like string, integers, tuples of those, etc.
pub trait AsId: std::hash::Hash + std::fmt::Debug {}
impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
/// egui tracks widgets frame-to-frame using [`Id`]s.
///
/// For instance, if you start dragging a slider one frame, egui stores
@@ -43,6 +53,7 @@ impl Id {
/// though obviously it will lead to a lot of collisions if you do use it!
pub const NULL: Self = Self(NonZeroU64::MAX);
/// Create a new root [`Id`] from a high-entropy hash.
#[inline]
const fn from_hash(hash: u64) -> Self {
if let Some(nonzero) = NonZeroU64::new(hash) {
@@ -52,17 +63,17 @@ impl Id {
}
}
/// Generate a new [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl std::hash::Hash) -> Self {
/// Generate a new root [`Id`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl AsId) -> Self {
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
}
/// Generate a new [`Id`] by hashing the parent [`Id`] and the given argument.
pub fn with(self, child: impl std::hash::Hash) -> Self {
/// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
pub fn with(self, salt: impl AsIdSalt) -> Self {
use std::hash::{BuildHasher as _, Hasher as _};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.0.get());
child.hash(&mut hasher);
hasher.write_u64(self.value());
hasher.write_u64(IdSalt::new(salt).value());
Self::from_hash(hasher.finish())
}

View File

@@ -0,0 +1,59 @@
use std::num::NonZeroU64;
/// Types that can be converted to an [`IdSalt`].
///
/// This is all types implementing `Hash` and `Debug`,
/// which includes things like string, integers, tuples of those, etc.
pub trait AsIdSalt: std::hash::Hash + std::fmt::Debug {}
impl<T: std::hash::Hash + std::fmt::Debug> AsIdSalt for T {}
/// Uniquely identifies a child widget within a parent widget.
///
/// An [`IdSalt`] is only unique within a parent [`crate::Id`].
/// An [`IdSalt`] is NOT globally unique.
///
/// You combine a parent [`crate::Id`] with an [`IdSalt`] to get a child [`crate::Id`],
/// using [`crate::Id::with`].
///
/// An [`IdSalt`] is usually a string, an integer, or similar.
///
/// An [`IdSalt`] should NOT be produced from an [`crate::Id`].
///
/// This is niche-optimized to that `Option<IdSalt>` is the same size as `IdSalt`.
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct IdSalt(NonZeroU64);
impl nohash_hasher::IsEnabled for IdSalt {}
impl IdSalt {
/// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer).
pub fn new(source: impl AsIdSalt) -> Self {
Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source))
}
/// Create a new root [`IdSalt`] from a high-entropy hash.
#[inline]
const fn from_hash(hash: u64) -> Self {
if let Some(nonzero) = NonZeroU64::new(hash) {
Self(nonzero)
} else {
Self(NonZeroU64::MIN) // The hash was exactly zero
}
}
/// The inner value of the [`IdSalt`].
///
/// This is a high-entropy hash.
#[inline(always)]
pub fn value(&self) -> u64 {
self.0.get()
}
}
impl std::fmt::Debug for IdSalt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "salt_{:04X}", self.value() as u16)
}
}

View File

@@ -209,7 +209,7 @@ impl InputOptions {
/// You can access this with [`crate::Context::input`].
///
/// You can check if `egui` is using the inputs using
/// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`].
/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct InputState {
@@ -522,14 +522,6 @@ impl InputState {
self.viewport_rect
}
/// Position and size of the egui area.
#[deprecated(
note = "screen_rect has been split into viewport_rect() and content_rect(). You likely should use content_rect()"
)]
pub fn screen_rect(&self) -> Rect {
self.content_rect()
}
/// Get the safe area insets.
///
/// This represents the area of the screen covered by status bars, navigation controls, notches,

View File

@@ -233,20 +233,14 @@ pub(crate) fn interact(
// );
// }
let contains_pointer: IdSet = hits
.contains_pointer
.iter()
.chain(&hits.click)
.chain(&hits.drag)
.map(|w| w.id)
.collect();
let contains_pointer: IdSet =
itertools::chain!(&hits.contains_pointer, &hits.click, &hits.drag)
.map(|w| w.id)
.collect();
let hovered = if clicked.is_some() || dragged.is_some() || long_touched.is_some() {
// If currently clicking or dragging, only that and nothing else is hovered.
clicked
.iter()
.chain(&dragged)
.chain(&long_touched)
itertools::chain!(&clicked, &dragged, &long_touched)
.copied()
.collect()
} else {
@@ -269,7 +263,9 @@ pub(crate) fn interact(
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);
let top_interactive_order = click_order.max(drag_order);
let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect();
let mut hovered: IdSet = std::iter::chain(&hits.click, &hits.drag)
.map(|w| w.id)
.collect();
for w in &hits.contains_pointer {
let is_interactive = w.sense.senses_click() || w.sense.senses_drag();

View File

@@ -1,7 +1,7 @@
//! Handles paint layers, i.e. how things
//! are sometimes painted behind or in front of other things.
use crate::{Id, IdMap, Rect, ahash, epaint};
use crate::{Id, IdMap, Rect, epaint};
use epaint::{ClippedShape, Shape, emath::TSTransform};
/// Different layer categories
@@ -86,12 +86,6 @@ impl LayerId {
}
}
#[inline(always)]
#[deprecated = "Use `Memory::allows_interaction` instead"]
pub fn allow_interaction(&self) -> bool {
self.order.allow_interaction()
}
/// Short and readable summary
pub fn short_debug_format(&self) -> String {
format!(

View File

@@ -112,8 +112,8 @@
//! loop {
//! let raw_input: egui::RawInput = gather_input();
//!
//! let full_output = ctx.run(raw_input, |ctx| {
//! egui::CentralPanel::default().show(&ctx, |ui| {
//! let full_output = ctx.run_ui(raw_input, |ui| {
//! egui::CentralPanel::default().show(ui, |ui| {
//! ui.label("Hello world!");
//! if ui.button("Click me").clicked() {
//! // take some action here
@@ -400,6 +400,7 @@ pub(crate) mod grid;
pub mod gui_zoom;
mod hit_test;
mod id;
mod id_salt;
mod input_state;
mod interaction;
pub mod introspection;
@@ -407,8 +408,6 @@ pub mod layers;
mod layout;
pub mod load;
mod memory;
#[deprecated = "Use `egui::containers::menu` instead"]
pub mod menu;
pub mod os;
mod painter;
mod pass_state;
@@ -434,9 +433,6 @@ mod callstack;
pub use accesskit;
#[deprecated = "Use the ahash crate directly."]
pub use ahash;
pub use epaint;
pub use epaint::ecolor;
pub use epaint::emath;
@@ -458,8 +454,8 @@ pub use epaint::{
pub mod text {
pub use crate::text_selection::CCursorRange;
pub use epaint::text::{
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TAB_SIZE,
TextFormat, TextWrapping, cursor::CCursor,
FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat,
TextWrapping, cursor::CCursor,
};
}
@@ -471,14 +467,15 @@ pub use self::{
Key, UserData,
input::*,
output::{
self, CursorIcon, FullOutput, OpenUrl, OutputCommand, PlatformOutput,
UserAttentionType, WidgetInfo,
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand,
PlatformOutput, UserAttentionType, WidgetInfo,
},
},
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap, IdSet},
id::{AsId, Id, IdMap, IdSet},
id_salt::{AsIdSalt, IdSalt},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState, SurrenderFocusOn},
layers::{LayerId, Order},
layout::*,
@@ -491,7 +488,7 @@ pub use self::{
style::{FontSelection, Spacing, Style, TextStyle, Visuals},
text::{Galley, TextFormat},
ui::Ui,
ui_builder::UiBuilder,
ui_builder::{IdSource, UiBuilder},
ui_stack::*,
viewport::*,
widget_rect::{InteractOptions, WidgetRect, WidgetRects},
@@ -499,9 +496,6 @@ pub use self::{
widgets::*,
};
#[deprecated = "Renamed to CornerRadius"]
pub type Rounding = CornerRadius;
// ----------------------------------------------------------------------------
/// Helper function that adds a label when compiling with debug assertions enabled.

View File

@@ -72,7 +72,7 @@ use crate::Context;
pub use self::{bytes_loader::DefaultBytesLoader, texture_loader::DefaultTextureLoader};
/// Represents a failed attempt at loading an image.
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LoadError {
/// Programmer error: There are no image loaders installed.
NoImageLoaders,

View File

@@ -1,5 +1,3 @@
use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
use emath::Vec2;
use super::{
@@ -17,7 +15,7 @@ struct PrimaryKey {
type Bucket = HashMap<Option<SizeHint>, Entry>;
struct Entry {
last_used: AtomicU64,
last_used: u64,
/// Size of the original SVG, if any, or the texel size of the image if not an SVG.
source_size: Vec2,
@@ -25,10 +23,15 @@ struct Entry {
handle: TextureHandle,
}
#[derive(Default)]
struct State {
pass_index: u64,
cache: HashMap<PrimaryKey, Bucket>,
}
#[derive(Default)]
pub struct DefaultTextureLoader {
pass_index: AtomicU64,
cache: Mutex<HashMap<PrimaryKey, Bucket>>,
state: Mutex<State>,
}
impl TextureLoader for DefaultTextureLoader {
@@ -55,7 +58,9 @@ impl TextureLoader for DefaultTextureLoader {
None
};
let mut cache = self.cache.lock();
let mut state = self.state.lock();
let State { pass_index, cache } = &mut *state;
let bucket = cache
.entry(PrimaryKey {
uri: uri.to_owned(),
@@ -63,10 +68,8 @@ impl TextureLoader for DefaultTextureLoader {
})
.or_default();
if let Some(texture) = bucket.get(&svg_size_hint) {
texture
.last_used
.store(self.pass_index.load(Relaxed), Relaxed);
if let Some(texture) = bucket.get_mut(&svg_size_hint) {
texture.last_used = *pass_index;
let texture = SizedTexture::new(texture.handle.id(), texture.source_size);
Ok(TexturePoll::Ready { texture })
} else {
@@ -79,7 +82,7 @@ impl TextureLoader for DefaultTextureLoader {
bucket.insert(
svg_size_hint,
Entry {
last_used: AtomicU64::new(self.pass_index.load(Relaxed)),
last_used: *pass_index,
source_size,
handle,
},
@@ -104,33 +107,37 @@ impl TextureLoader for DefaultTextureLoader {
fn forget(&self, uri: &str) {
log::trace!("forget {uri:?}");
self.cache.lock().retain(|key, _value| key.uri != uri);
self.state.lock().cache.retain(|key, _value| key.uri != uri);
}
fn forget_all(&self) {
log::trace!("forget all");
self.cache.lock().clear();
self.state.lock().cache.clear();
}
fn end_pass(&self, pass_index: u64) {
self.pass_index.store(pass_index, Relaxed);
let mut cache = self.cache.lock();
let mut state = self.state.lock();
state.pass_index = pass_index;
let State { pass_index, cache } = &mut *state;
cache.retain(|_key, bucket| {
if 2 <= bucket.len() {
// There are multiple textures of the same URI (e.g. SVGs of different scales).
// This could be because someone has an SVG in a resizable container,
// and so we get a lot of different sizes of it.
// This could wast VRAM, so we remove the ones that are not used in this frame.
bucket.retain(|_, texture| pass_index <= texture.last_used.load(Relaxed) + 1);
bucket.retain(|_, texture| *pass_index <= texture.last_used + 1);
}
!bucket.is_empty()
});
}
fn byte_size(&self) -> usize {
self.cache
self.state
.lock()
.cache
.values()
.map(|bucket| {
bucket

View File

@@ -116,6 +116,11 @@ pub struct Memory {
/// (e.g. relative to some other widget).
#[cfg_attr(feature = "persistence", serde(skip))]
popups: ViewportIdMap<OpenPopup>,
/// Whether to inform the backend to interrupt any ongoing IME composition
/// this pass.
#[cfg_attr(feature = "persistence", serde(skip))]
requested_interrupt_ime: bool,
}
impl Default for Memory {
@@ -133,6 +138,7 @@ impl Default for Memory {
popups: Default::default(),
everything_is_visible: Default::default(),
add_fonts: Default::default(),
requested_interrupt_ime: Default::default(),
};
slf.interactions.entry(slf.viewport_id).or_default();
slf.areas.entry(slf.viewport_id).or_default();
@@ -193,7 +199,7 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))]
pub light_style: std::sync::Arc<Style>,
/// Preference for selection between dark and light [`crate::Context::style`]
/// Preference for selection between dark and light [`crate::Context::global_style`]
/// as the active style used by all subsequent windows, panels, etc.
///
/// Default: `ThemePreference::System`.
@@ -266,7 +272,7 @@ pub struct Options {
///
/// If this is `1`, [`crate::Context::request_discard`] will be ignored.
///
/// Multi-pass is supported by [`crate::Context::run`].
/// Multi-pass is supported by [`crate::Context::run_ui`].
///
/// See [`crate::Context::request_discard`] for more.
pub max_passes: NonZeroUsize,
@@ -761,6 +767,8 @@ impl Memory {
self.areas.entry(self.viewport_id).or_default();
self.requested_interrupt_ime = false;
// self.interactions is handled elsewhere
self.options.begin_pass(new_raw_input);
@@ -812,12 +820,6 @@ impl Memory {
}
}
/// The currently set transform of a layer.
#[deprecated = "Use `Context::layer_transform_to_global` instead"]
pub fn layer_transforms(&self, layer_id: LayerId) -> Option<TSTransform> {
self.to_global.get(&layer_id).copied()
}
/// An iterator over all layers. Back-to-front, top is last.
pub fn layer_ids(&self) -> impl ExactSizeIterator<Item = LayerId> + '_ {
self.areas().order().iter().copied()
@@ -875,9 +877,12 @@ impl Memory {
/// Give keyboard focus to a specific widget.
/// See also [`crate::Response::request_focus`].
///
/// Calling this will interrupt IME composition.
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.focus_mut().focused_widget = Some(FocusWidget::new(id));
self.interrupt_ime();
}
/// Surrender keyboard focus for a specific widget.
@@ -993,6 +998,28 @@ impl Memory {
pub(crate) fn focus_mut(&mut self) -> &mut Focus {
self.focus.entry(self.viewport_id).or_default()
}
/// Check if the widget owns IME events.
///
/// A widget should only consume IME events if this returns `true`. At most
/// one widget can own IME events for each frame.
#[inline(always)]
pub fn owns_ime_events(&self, id: Id) -> bool {
// Note: Even if the IME is being interrupted in the current frame, we
// should not return `false` here, since we still need
// `PlatformOutput::ime` to be set in such cases.
self.has_focus(id)
}
/// Interrupt the current IME composition, if any.
pub fn interrupt_ime(&mut self) {
self.requested_interrupt_ime = true;
}
pub(crate) fn should_interrupt_ime(&self) -> bool {
self.requested_interrupt_ime
}
}
/// State of an open popup.
@@ -1019,40 +1046,27 @@ impl OpenPopup {
}
}
/// ## Deprecated popup API
/// Use [`crate::Popup`] instead.
/// ## Popup state (internal API)
///
/// Used by [`crate::Popup`].
impl Memory {
/// Is the given popup open?
#[deprecated = "Use Popup::is_id_open instead"]
pub fn is_popup_open(&self, popup_id: Id) -> bool {
pub(crate) fn is_popup_open(&self, popup_id: Id) -> bool {
self.popups
.get(&self.viewport_id)
.is_some_and(|state| state.id == popup_id)
|| self.everything_is_visible()
}
/// Is any popup open?
#[deprecated = "Use Popup::is_any_open instead"]
pub fn any_popup_open(&self) -> bool {
pub(crate) fn any_popup_open(&self) -> bool {
self.popups.contains_key(&self.viewport_id) || self.everything_is_visible()
}
/// Open the given popup and close all others.
///
/// Note that you must call `keep_popup_open` on subsequent frames as long as the popup is open.
#[deprecated = "Use Popup::open_id instead"]
pub fn open_popup(&mut self, popup_id: Id) {
pub(crate) fn open_popup(&mut self, popup_id: Id) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, None));
}
/// Popups must call this every frame while open.
///
/// This is needed because in some cases popups can go away without `close_popup` being
/// called. For example, when a context menu is open and the underlying widget stops
/// being rendered.
#[deprecated = "Use Popup::show instead"]
pub fn keep_popup_open(&mut self, popup_id: Id) {
pub(crate) fn keep_popup_open(&mut self, popup_id: Id) {
if let Some(state) = self.popups.get_mut(&self.viewport_id)
&& state.id == popup_id
{
@@ -1060,43 +1074,27 @@ impl Memory {
}
}
/// Open the popup and remember its position.
#[deprecated = "Use Popup with PopupAnchor::Position instead"]
pub fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
pub(crate) fn open_popup_at(&mut self, popup_id: Id, pos: impl Into<Option<Pos2>>) {
self.popups
.insert(self.viewport_id, OpenPopup::new(popup_id, pos.into()));
}
/// Get the position for this popup.
#[deprecated = "Use Popup::position_of_id instead"]
pub fn popup_position(&self, id: Id) -> Option<Pos2> {
pub(crate) fn popup_position(&self, id: Id) -> Option<Pos2> {
let state = self.popups.get(&self.viewport_id)?;
if state.id == id { state.pos } else { None }
}
/// Close any currently open popup.
#[deprecated = "Use Popup::close_all instead"]
pub fn close_all_popups(&mut self) {
pub(crate) fn close_all_popups(&mut self) {
self.popups.clear();
}
/// Close the given popup, if it is open.
///
/// See also [`Self::close_all_popups`] if you want to close any / all currently open popups.
#[deprecated = "Use Popup::close_id instead"]
pub fn close_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
pub(crate) fn close_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.popups.remove(&self.viewport_id);
}
}
/// Toggle the given popup between closed and open.
///
/// Note: At most, only one popup can be open at a time.
#[deprecated = "Use Popup::toggle_id instead"]
pub fn toggle_popup(&mut self, popup_id: Id) {
#[expect(deprecated)]
pub(crate) fn toggle_popup(&mut self, popup_id: Id) {
if self.is_popup_open(popup_id) {
self.close_popup(popup_id);
} else {
@@ -1173,6 +1171,10 @@ impl Areas {
self.areas.get(&id)
}
pub(crate) fn get_mut(&mut self, id: Id) -> Option<&mut area::AreaState> {
self.areas.get_mut(&id)
}
/// All layers back-to-front, top is last.
pub(crate) fn order(&self) -> &[LayerId] {
&self.order
@@ -1234,11 +1236,12 @@ impl Areas {
}
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
self.visible_areas_last_frame
.iter()
.copied()
.chain(self.visible_areas_current_frame.iter().copied())
.collect()
std::iter::chain(
&self.visible_areas_last_frame,
&self.visible_areas_current_frame,
)
.copied()
.collect()
}
pub(crate) fn visible_windows(&self) -> impl Iterator<Item = (LayerId, &area::AreaState)> {

View File

@@ -1,781 +0,0 @@
#![expect(deprecated)]
//! Deprecated menu API - Use [`crate::containers::menu`] instead.
//!
//! Usage:
//! ```
//! fn show_menu(ui: &mut egui::Ui) {
//! use egui::{menu, Button};
//!
//! menu::bar(ui, |ui| {
//! ui.menu_button("File", |ui| {
//! if ui.button("Open").clicked() {
//! // …
//! }
//! });
//! });
//! }
//! ```
use super::{
Align, Context, Id, InnerResponse, PointerState, Pos2, Rect, Response, Sense, TextStyle, Ui,
Vec2, style::WidgetVisuals,
};
use crate::{
Align2, Area, Color32, Frame, Key, LayerId, Layout, NumExt as _, Order, Stroke, Style,
TextWrapMode, UiKind, WidgetText, epaint, vec2,
widgets::{Button, ImageButton},
};
use epaint::mutex::RwLock;
use std::sync::Arc;
/// What is saved between frames.
#[derive(Clone, Default)]
pub struct BarState {
open_menu: MenuRootManager,
}
impl BarState {
pub fn load(ctx: &Context, bar_id: Id) -> Self {
ctx.data_mut(|d| d.get_temp::<Self>(bar_id).unwrap_or_default())
}
pub fn store(self, ctx: &Context, bar_id: Id) {
ctx.data_mut(|d| d.insert_temp(bar_id, self));
}
/// Show a menu at pointer if primary-clicked response.
///
/// Should be called from [`Context`] on a [`Response`]
pub fn bar_menu<R>(
&mut self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
MenuRoot::stationary_click_interaction(button, &mut self.open_menu);
self.open_menu.show(button, add_contents)
}
pub(crate) fn has_root(&self) -> bool {
self.open_menu.inner.is_some()
}
}
impl std::ops::Deref for BarState {
type Target = MenuRootManager;
fn deref(&self) -> &Self::Target {
&self.open_menu
}
}
impl std::ops::DerefMut for BarState {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.open_menu
}
}
fn set_menu_style(style: &mut Style) {
if style.compact_menu_style {
style.spacing.button_padding = vec2(2.0, 0.0);
style.visuals.widgets.active.bg_stroke = Stroke::NONE;
style.visuals.widgets.hovered.bg_stroke = Stroke::NONE;
style.visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
style.visuals.widgets.inactive.bg_stroke = Stroke::NONE;
}
}
/// The menu bar goes well in a [`crate::Panel::top`],
/// but can also be placed in a [`crate::Window`].
/// In the latter case you may want to wrap it in [`Frame`].
#[deprecated = "Use `egui::MenuBar::new().ui(` instead"]
pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
ui.horizontal(|ui| {
set_menu_style(ui.style_mut());
// Take full width and fixed height:
let height = ui.spacing().interact_size.y;
ui.set_min_size(vec2(ui.available_width(), height));
add_contents(ui)
})
}
/// Construct a top level menu in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_button<R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_impl(ui, title, Box::new(add_contents))
}
/// Construct a top level menu with a custom button in a menu bar.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
pub fn menu_custom_button<R>(
ui: &mut Ui,
button: Button<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_button_impl(ui, button, Box::new(add_contents))
}
/// Construct a top level menu with an image in a menu bar. This would be e.g. "File", "Edit" etc.
///
/// Responds to primary clicks.
///
/// Returns `None` if the menu is not open.
#[deprecated = "Use `menu_custom_button` instead"]
pub fn menu_image_button<R>(
ui: &mut Ui,
image_button: ImageButton<'_>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
stationary_menu_button_impl(
ui,
Button::image(image_button.image),
Box::new(add_contents),
)
}
/// Construct a nested sub menu in another menu.
///
/// Opens on hover.
///
/// Returns `None` if the menu is not open.
pub fn submenu_button<R>(
ui: &mut Ui,
parent_state: Arc<RwLock<MenuState>>,
title: impl Into<WidgetText>,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
SubMenu::new(parent_state, title).show(ui, add_contents)
}
/// wrapper for the contents of every menu.
fn menu_popup<'c, R>(
ctx: &Context,
parent_layer: LayerId,
menu_state_arc: &Arc<RwLock<MenuState>>,
menu_id: Id,
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> {
let pos = {
let mut menu_state = menu_state_arc.write();
menu_state.entry_count = 0;
menu_state.rect.min
};
let area_id = menu_id.with("__menu");
ctx.pass_state_mut(|fs| {
fs.layers
.entry(parent_layer)
.or_default()
.open_popups
.insert(area_id)
});
let area = Area::new(area_id)
.kind(UiKind::Menu)
.order(Order::Foreground)
.fixed_pos(pos)
.default_width(ctx.global_style().spacing.menu_width)
.sense(Sense::hover());
let mut sizing_pass = false;
let area_response = area.show(ctx, |ui| {
sizing_pass = ui.is_sizing_pass();
set_menu_style(ui.style_mut());
Frame::menu(ui.style())
.show(ui, |ui| {
ui.set_menu_state(Some(Arc::clone(menu_state_arc)));
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
.inner
})
.inner
});
let area_rect = area_response.response.rect;
menu_state_arc.write().rect = if sizing_pass {
// During the sizing pass we didn't know the size yet,
// so we might have just constrained the position unnecessarily.
// Therefore keep the original=desired position until the next frame.
Rect::from_min_size(pos, area_rect.size())
} else {
// We knew the size, and this is where it ended up (potentially constrained to screen).
// Remember it for the future:
area_rect
};
area_response
}
/// Build a top level menu with a button.
///
/// Responds to primary clicks.
fn stationary_menu_impl<'c, R>(
ui: &mut Ui,
title: impl Into<WidgetText>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let title = title.into();
let bar_id = ui.id();
let menu_id = bar_id.with(title.text());
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let mut button = Button::new(title);
if bar_state.open_menu.is_menu_open(menu_id) {
button = button.fill(ui.visuals().widgets.open.weak_bg_fill);
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
}
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
/// Build a top level menu with an image button.
///
/// Responds to primary clicks.
fn stationary_menu_button_impl<'c, R>(
ui: &mut Ui,
button: Button<'_>,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<Option<R>> {
let bar_id = ui.id();
let mut bar_state = BarState::load(ui.ctx(), bar_id);
let button_response = ui.add(button);
let inner = bar_state.bar_menu(&button_response, add_contents);
bar_state.store(ui.ctx(), bar_id);
InnerResponse::new(inner.map(|r| r.inner), button_response)
}
pub(crate) const CONTEXT_MENU_ID_STR: &str = "__egui::context_menu";
/// Response to secondary clicks (right-clicks) by showing the given menu.
pub fn context_menu(
response: &Response,
add_contents: impl FnOnce(&mut Ui),
) -> Option<InnerResponse<()>> {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let mut bar_state = BarState::load(&response.ctx, menu_id);
MenuRoot::context_click_interaction(response, &mut bar_state);
let inner_response = bar_state.show(response, add_contents);
bar_state.store(&response.ctx, menu_id);
inner_response
}
/// Returns `true` if the context menu is opened for this widget.
pub fn context_menu_opened(response: &Response) -> bool {
let menu_id = Id::new(CONTEXT_MENU_ID_STR);
let bar_state = BarState::load(&response.ctx, menu_id);
bar_state.is_menu_open(response.id)
}
/// Stores the state for the context menu.
#[derive(Clone, Default)]
pub struct MenuRootManager {
inner: Option<MenuRoot>,
}
impl MenuRootManager {
/// Show a menu at pointer if right-clicked response.
///
/// Should be called from [`Context`] on a [`Response`]
pub fn show<R>(
&mut self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
if let Some(root) = self.inner.as_mut() {
let (menu_response, inner_response) = root.show(button, add_contents);
if menu_response.is_close() {
self.inner = None;
}
inner_response
} else {
None
}
}
fn is_menu_open(&self, id: Id) -> bool {
self.inner.as_ref().map(|m| m.id) == Some(id)
}
}
impl std::ops::Deref for MenuRootManager {
type Target = Option<MenuRoot>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for MenuRootManager {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
/// Menu root associated with an Id from a Response
#[derive(Clone)]
pub struct MenuRoot {
pub menu_state: Arc<RwLock<MenuState>>,
pub id: Id,
}
impl MenuRoot {
pub fn new(position: Pos2, id: Id) -> Self {
Self {
menu_state: Arc::new(RwLock::new(MenuState::new(position))),
id,
}
}
pub fn show<R>(
&self,
button: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> (MenuResponse, Option<InnerResponse<R>>) {
if self.id == button.id {
let inner_response = menu_popup(
&button.ctx,
button.layer_id,
&self.menu_state,
self.id,
add_contents,
);
let menu_state = self.menu_state.read();
let escape_pressed = button.ctx.input(|i| i.key_pressed(Key::Escape));
if menu_state.response.is_close()
|| escape_pressed
|| inner_response.response.should_close()
{
return (MenuResponse::Close, Some(inner_response));
}
}
(MenuResponse::Stay, None)
}
/// Interaction with a stationary menu, i.e. fixed in another Ui.
///
/// Responds to primary clicks.
fn stationary_interaction(button: &Response, root: &mut MenuRootManager) -> MenuResponse {
let id = button.id;
if (button.clicked() && root.is_menu_open(id))
|| button.ctx.input(|i| i.key_pressed(Key::Escape))
{
// menu open and button clicked or esc pressed
return MenuResponse::Close;
} else if (button.clicked() && !root.is_menu_open(id))
|| (button.hovered() && root.is_some())
{
// menu not open and button clicked
// or button hovered while other menu is open
let mut pos = button.rect.left_bottom();
let menu_frame = Frame::menu(&button.ctx.global_style());
pos.x -= menu_frame.total_margin().left; // Make fist button in menu align with the parent button
pos.y += button.ctx.global_style().spacing.menu_spacing;
if let Some(root) = root.inner.as_mut() {
let menu_rect = root.menu_state.read().rect;
let content_rect = button.ctx.input(|i| i.content_rect());
if pos.y + menu_rect.height() > content_rect.max.y {
pos.y = content_rect.max.y - menu_rect.height() - button.rect.height();
}
if pos.x + menu_rect.width() > content_rect.max.x {
pos.x = content_rect.max.x - menu_rect.width();
}
}
if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) {
pos = to_global * pos;
}
return MenuResponse::Create(pos, id);
} else if button
.ctx
.input(|i| i.pointer.any_pressed() && i.pointer.primary_down())
&& let Some(pos) = button.ctx.input(|i| i.pointer.interact_pos())
&& let Some(root) = root.inner.as_mut()
&& root.id == id
{
// pressed somewhere while this menu is open
let in_menu = root.menu_state.read().area_contains(pos);
if !in_menu {
return MenuResponse::Close;
}
}
MenuResponse::Stay
}
/// Interaction with a context menu (secondary click).
pub fn context_interaction(response: &Response, root: &mut Option<Self>) -> MenuResponse {
let response = response.interact(Sense::click());
let hovered = response.hovered();
let secondary_clicked = response.secondary_clicked();
response.ctx.input(|input| {
let pointer = &input.pointer;
if let Some(pos) = pointer.interact_pos() {
let (in_old_menu, destroy) = if let Some(root) = root {
let in_old_menu = root.menu_state.read().area_contains(pos);
let destroy = !in_old_menu && pointer.any_pressed() && root.id == response.id;
(in_old_menu, destroy)
} else {
(false, false)
};
if !in_old_menu {
if hovered && secondary_clicked {
return MenuResponse::Create(pos, response.id);
} else if destroy || hovered && pointer.primary_down() {
return MenuResponse::Close;
}
}
}
MenuResponse::Stay
})
}
pub fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
match menu_response {
MenuResponse::Create(pos, id) => {
root.inner = Some(Self::new(pos, id));
}
MenuResponse::Close => root.inner = None,
MenuResponse::Stay => {}
}
}
/// Respond to secondary (right) clicks.
pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager) {
let menu_response = Self::context_interaction(response, root);
Self::handle_menu_response(root, menu_response);
}
// Responds to primary clicks.
pub fn stationary_click_interaction(button: &Response, root: &mut MenuRootManager) {
let menu_response = Self::stationary_interaction(button, root);
Self::handle_menu_response(root, menu_response);
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum MenuResponse {
Close,
Stay,
Create(Pos2, Id),
}
impl MenuResponse {
pub fn is_close(&self) -> bool {
*self == Self::Close
}
}
pub struct SubMenuButton {
text: WidgetText,
icon: WidgetText,
index: usize,
}
impl SubMenuButton {
/// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label
fn new(text: impl Into<WidgetText>, icon: impl Into<WidgetText>, index: usize) -> Self {
Self {
text: text.into(),
icon: icon.into(),
index,
}
}
fn visuals<'a>(
ui: &'a Ui,
response: &Response,
menu_state: &MenuState,
sub_id: Id,
) -> &'a WidgetVisuals {
if menu_state.is_open(sub_id) && !response.hovered() {
&ui.style().visuals.widgets.open
} else {
ui.style().interact(response)
}
}
#[inline]
pub fn icon(mut self, icon: impl Into<WidgetText>) -> Self {
self.icon = icon.into();
self
}
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
let Self { text, icon, .. } = self;
let text_style = TextStyle::Button;
let sense = Sense::click();
let text_icon_gap = ui.spacing().item_spacing.x;
let button_padding = ui.spacing().button_padding;
let total_extra = button_padding + button_padding;
let text_available_width = ui.available_width() - total_extra.x;
let text_galley = text.into_galley(
ui,
Some(TextWrapMode::Wrap),
text_available_width,
text_style.clone(),
);
let icon_available_width = text_available_width - text_galley.size().x;
let icon_galley = icon.into_galley(
ui,
Some(TextWrapMode::Wrap),
icon_available_width,
text_style,
);
let text_and_icon_size = Vec2::new(
text_galley.size().x + text_icon_gap + icon_galley.size().x,
text_galley.size().y.max(icon_galley.size().y),
);
let mut desired_size = text_and_icon_size + 2.0 * button_padding;
desired_size.y = desired_size.y.at_least(ui.spacing().interact_size.y);
let (rect, response) = ui.allocate_at_least(desired_size, sense);
response.widget_info(|| {
crate::WidgetInfo::labeled(
crate::WidgetType::Button,
ui.is_enabled(),
text_galley.text(),
)
});
if ui.is_rect_visible(rect) {
let visuals = Self::visuals(ui, &response, menu_state, sub_id);
let text_pos = Align2::LEFT_CENTER
.align_size_within_rect(text_galley.size(), rect.shrink2(button_padding))
.min;
let icon_pos = Align2::RIGHT_CENTER
.align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
.min;
if ui.visuals().button_frame {
ui.painter().rect_filled(
rect.expand(visuals.expansion),
visuals.corner_radius,
visuals.weak_bg_fill,
);
}
let text_color = visuals.text_color();
ui.painter().galley(text_pos, text_galley, text_color);
ui.painter().galley(icon_pos, icon_galley, text_color);
}
response
}
}
pub struct SubMenu {
button: SubMenuButton,
parent_state: Arc<RwLock<MenuState>>,
}
impl SubMenu {
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl Into<WidgetText>) -> Self {
let index = parent_state.write().next_entry_index();
Self {
button: SubMenuButton::new(text, "", index),
parent_state,
}
}
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let sub_id = ui.id().with(self.button.index);
let response = self.button.show(ui, &self.parent_state.read(), sub_id);
self.parent_state
.write()
.submenu_button_interaction(ui, sub_id, &response);
let inner =
self.parent_state
.write()
.show_submenu(ui.ctx(), ui.layer_id(), sub_id, add_contents);
InnerResponse::new(inner, response)
}
}
/// Components of menu state, public for advanced usage.
///
/// Usually you don't need to use it directly.
pub struct MenuState {
/// The opened sub-menu and its [`Id`]
sub_menu: Option<(Id, Arc<RwLock<Self>>)>,
/// Bounding box of this menu (without the sub-menu),
/// including the frame and everything.
pub rect: Rect,
/// Used to check if any menu in the tree wants to close
pub response: MenuResponse,
/// Used to hash different [`Id`]s for sub-menus
entry_count: usize,
}
impl MenuState {
pub fn new(position: Pos2) -> Self {
Self {
rect: Rect::from_min_size(position, Vec2::ZERO),
sub_menu: None,
response: MenuResponse::Stay,
entry_count: 0,
}
}
/// Close menu hierarchy.
pub fn close(&mut self) {
self.response = MenuResponse::Close;
}
fn show_submenu<R>(
&mut self,
ctx: &Context,
parent_layer: LayerId,
id: Id,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<R> {
let (sub_response, response) = self.submenu(id).map(|sub| {
let inner_response = menu_popup(ctx, parent_layer, sub, id, add_contents);
if inner_response.response.should_close() {
sub.write().close();
}
(sub.read().response, inner_response.inner)
})?;
self.cascade_close_response(sub_response);
Some(response)
}
/// Check if position is in the menu hierarchy's area.
pub fn area_contains(&self, pos: Pos2) -> bool {
self.rect.contains(pos)
|| self
.sub_menu
.as_ref()
.is_some_and(|(_, sub)| sub.read().area_contains(pos))
}
fn next_entry_index(&mut self) -> usize {
self.entry_count += 1;
self.entry_count - 1
}
/// Sense button interaction opening and closing submenu.
fn submenu_button_interaction(&mut self, ui: &Ui, sub_id: Id, button: &Response) {
let pointer = ui.input(|i| i.pointer.clone());
let open = self.is_open(sub_id);
if self.moving_towards_current_submenu(&pointer) {
// We don't close the submenu if the pointer is on its way to hover it.
// ensure to repaint once even when pointer is not moving
ui.request_repaint();
} else if !open && button.hovered() {
// TODO(emilk): open menu to the left if there isn't enough space to the right
let mut pos = button.rect.right_top();
pos.x = self.rect.right() + ui.spacing().menu_spacing;
pos.y -= Frame::menu(ui.style()).total_margin().top; // align the first button in the submenu with the parent button
self.open_submenu(sub_id, pos);
} else if open
&& ui.response().contains_pointer()
&& !button.hovered()
&& !self.hovering_current_submenu(&pointer)
{
// We are hovering something else in the menu, so close the submenu.
self.close_submenu();
}
}
/// Check if pointer is moving towards current submenu.
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
if pointer.is_still() {
return false;
}
if let Some(sub_menu) = self.current_submenu()
&& let Some(pos) = pointer.hover_pos()
{
let rect = sub_menu.read().rect;
return rect.intersects_ray(pos, pointer.direction().normalized());
}
false
}
/// Check if pointer is hovering current submenu.
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
if let Some(sub_menu) = self.current_submenu()
&& let Some(pos) = pointer.hover_pos()
{
return sub_menu.read().area_contains(pos);
}
false
}
/// Cascade close response to menu root.
fn cascade_close_response(&mut self, response: MenuResponse) {
if response.is_close() {
self.response = response;
}
}
fn is_open(&self, id: Id) -> bool {
self.sub_id() == Some(id)
}
fn sub_id(&self) -> Option<Id> {
self.sub_menu.as_ref().map(|(id, _)| *id)
}
fn current_submenu(&self) -> Option<&Arc<RwLock<Self>>> {
self.sub_menu.as_ref().map(|(_, sub)| sub)
}
fn submenu(&self, id: Id) -> Option<&Arc<RwLock<Self>>> {
let (k, sub) = self.sub_menu.as_ref()?;
if id == *k { Some(sub) } else { None }
}
/// Open submenu at position, if not already open.
fn open_submenu(&mut self, id: Id, pos: Pos2) {
if !self.is_open(id) {
self.sub_menu = Some((id, Arc::new(RwLock::new(Self::new(pos)))));
}
}
fn close_submenu(&mut self) {
self.sub_menu = None;
}
}

View File

@@ -82,12 +82,6 @@ impl Painter {
self.layer_id = layer_id;
}
/// If set, colors will be modified to look like this
#[deprecated = "Use `multiply_opacity` instead"]
pub fn set_fade_to_color(&mut self, fade_to_color: Option<Color32>) {
self.fade_to_color = fade_to_color;
}
/// Set the opacity (alpha multiplier) of everything painted by this painter from this point forward.
///
/// `opacity` must be between 0.0 and 1.0, where 0.0 means fully transparent (i.e., invisible)
@@ -195,41 +189,6 @@ impl Painter {
pub fn round_to_pixel_center(&self, point: f32) -> f32 {
point.round_to_pixel_center(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering of lines that are one pixel wide (or any odd number of pixels).
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 {
pos.round_to_pixel_center(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering of filled shapes.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_to_pixel(&self, point: f32) -> f32 {
point.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
vec.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
pos.round_to_pixels(self.pixels_per_point())
}
/// Useful for pixel-perfect rendering.
#[deprecated = "Use `emath::GuiRounding` with `painter.pixels_per_point()` instead"]
#[inline]
pub fn round_rect_to_pixels(&self, rect: Rect) -> Rect {
rect.round_to_pixels(self.pixels_per_point())
}
}
/// ## Low level

View File

@@ -199,16 +199,15 @@ pub struct PassState {
pub tooltips: TooltipPassState,
/// Starts off as the `screen_rect`, shrinks as panels are added.
/// The [`crate::CentralPanel`] does not change this.
pub available_rect: Rect,
/// What the root UI had available at the end of the previous pass.
///
/// Only set if [`crate::Context::run_ui`] has been called.
pub root_ui_available_rect: Option<Rect>,
/// Starts off as the `screen_rect`, shrinks as panels are added.
/// The [`crate::CentralPanel`] retracts from this.
pub unused_rect: Rect,
/// How much space is used by panels.
pub used_by_panels: Rect,
/// What the root UI had used at the end of the previous pass.
///
/// Only set if [`crate::Context::run_ui`] has been called.
pub root_ui_min_rect: Option<Rect>,
/// The current scroll area should scroll to this range (horizontal, vertical).
pub scroll_target: [Option<ScrollTarget>; 2],
@@ -240,9 +239,8 @@ impl Default for PassState {
widgets: Default::default(),
layers: Default::default(),
tooltips: Default::default(),
available_rect: Rect::NAN,
unused_rect: Rect::NAN,
used_by_panels: Rect::NAN,
root_ui_available_rect: None,
root_ui_min_rect: None,
scroll_target: [None, None],
scroll_delta: (Vec2::default(), style::ScrollAnimation::none()),
accesskit_state: None,
@@ -255,16 +253,15 @@ impl Default for PassState {
}
impl PassState {
pub(crate) fn begin_pass(&mut self, content_rect: Rect) {
pub(crate) fn begin_pass(&mut self) {
profiling::function_scope!();
let Self {
used_ids,
widgets,
tooltips,
layers,
available_rect,
unused_rect,
used_by_panels,
root_ui_available_rect,
root_ui_min_rect,
scroll_target,
scroll_delta,
accesskit_state,
@@ -278,9 +275,8 @@ impl PassState {
widgets.clear();
tooltips.clear();
layers.clear();
*available_rect = content_rect;
*unused_rect = content_rect;
*used_by_panels = Rect::NOTHING;
*root_ui_available_rect = None;
*root_ui_min_rect = None;
*scroll_target = [None, None];
*scroll_delta = Default::default();
@@ -293,64 +289,4 @@ impl PassState {
highlight_next_pass.clear();
}
/// How much space is still available after panels has been added.
pub(crate) fn available_rect(&self) -> Rect {
debug_assert!(
self.available_rect.is_finite(),
"Called `available_rect()` before `Context::run()`"
);
self.available_rect
}
/// Shrink `available_rect`.
pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
"Mismatching left panel. You must not create a panel from within another panel."
);
self.available_rect.min.x = panel_rect.max.x;
self.unused_rect.min.x = panel_rect.max.x;
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
"Mismatching right panel. You must not create a panel from within another panel."
);
self.available_rect.max.x = panel_rect.min.x;
self.unused_rect.max.x = panel_rect.min.x;
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.min.distance(self.available_rect.min) < 0.1,
"Mismatching top panel. You must not create a panel from within another panel."
);
self.available_rect.min.y = panel_rect.max.y;
self.unused_rect.min.y = panel_rect.max.y;
self.used_by_panels |= panel_rect;
}
/// Shrink `available_rect`.
pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) {
debug_assert!(
panel_rect.max.distance(self.available_rect.max) < 0.1,
"Mismatching bottom panel. You must not create a panel from within another panel."
);
self.available_rect.max.y = panel_rect.min.y;
self.unused_rect.max.y = panel_rect.min.y;
self.used_by_panels |= panel_rect;
}
pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) {
// Note: we do not shrink `available_rect`, because
// we allow windows to cover the CentralPanel.
self.unused_rect = Rect::NOTHING; // Nothing left unused after this
self.used_by_panels |= panel_rect;
}
}

View File

@@ -272,10 +272,10 @@ impl Response {
false
} else if let Some(pos) = pointer_interact_pos {
let layer_under_pointer = self.ctx.layer_id_at(pos);
if layer_under_pointer != Some(self.layer_id) {
true
} else {
if layer_under_pointer == Some(self.layer_id) {
!self.interact_rect.contains(pos)
} else {
true
}
} else {
false // clicked without a pointer, weird

View File

@@ -2,7 +2,7 @@
use emath::Align;
use epaint::{
AlphaFromCoverage, CornerRadius, Shadow, Stroke, TextOptions,
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
mutex::Mutex,
text::{FontTweak, Tag},
};
@@ -297,17 +297,6 @@ pub struct Style {
#[cfg_attr(feature = "serde", serde(skip))]
pub number_formatter: NumberFormatter,
/// If set, labels, buttons, etc. will use this to determine whether to wrap the text at the
/// right edge of the [`Ui`] they are in. By default, this is `None`.
///
/// **Note**: this API is deprecated, use `wrap_mode` instead.
///
/// * `None`: use `wrap_mode` instead
/// * `Some(true)`: wrap mode defaults to [`crate::TextWrapMode::Wrap`]
/// * `Some(false)`: wrap mode defaults to [`crate::TextWrapMode::Extend`]
#[deprecated = "Use wrap_mode instead"]
pub wrap: Option<bool>,
/// If set, labels, buttons, etc. will use this to determine whether to wrap or truncate the
/// text at the right edge of the [`Ui`] they are in, or to extend it. By default, this is
/// `None`.
@@ -1105,7 +1094,10 @@ pub struct Visuals {
/// How the text cursor acts.
pub text_cursor: TextCursorStyle,
/// Allow child widgets to be just on the border and still have a stroke with some thickness
/// Allow widgets to paint this much outside the scroll area rect.
///
/// Legacy. Should not be used anymore.
/// Use [`crate::ScrollArea::content_margin`] instead.
pub clip_rect_margin: f32,
/// Show a background behind buttons.
@@ -1186,13 +1178,6 @@ impl Visuals {
self.window_stroke
}
/// When fading out things, we fade the colors towards this.
#[inline(always)]
#[deprecated = "Use disabled_alpha(). Fading is now handled by modifying the alpha channel."]
pub fn fade_out_to_color(&self) -> Color32 {
self.widgets.noninteractive.weak_bg_fill
}
/// Disabled widgets have their alpha modified by this.
#[inline(always)]
pub fn disabled_alpha(&self) -> f32 {
@@ -1323,11 +1308,6 @@ impl WidgetVisuals {
pub fn text_color(&self) -> Color32 {
self.fg_stroke.color
}
#[deprecated = "Renamed to corner_radius"]
pub fn rounding(&self) -> CornerRadius {
self.corner_radius
}
}
/// Options for help debug egui by adding extra visualization
@@ -1430,7 +1410,6 @@ pub fn default_text_styles() -> BTreeMap<TextStyle, FontId> {
impl Default for Style {
fn default() -> Self {
#[expect(deprecated)]
Self {
override_font_id: None,
override_text_style: None,
@@ -1438,12 +1417,11 @@ impl Default for Style {
text_styles: default_text_styles(),
drag_value_text_style: TextStyle::Button,
number_formatter: NumberFormatter(Arc::new(emath::format_with_decimals_in_range)),
wrap: None,
wrap_mode: None,
spacing: Spacing::default(),
interaction: Interaction::default(),
visuals: Visuals::default(),
animation_time: 6.0 / 60.0, // If we make this too slow, it will be too obvious that our panel animations look like shit :(
animation_time: 0.2,
#[cfg(debug_assertions)]
debug: Default::default(),
explanation_tooltips: false,
@@ -1503,7 +1481,7 @@ impl Visuals {
Self {
dark_mode: true,
text_options: TextOptions {
alpha_from_coverage: AlphaFromCoverage::DARK_MODE_DEFAULT,
color_transfer_function: FontColorTransferFunction::DARK_MODE_DEFAULT,
..Default::default()
},
override_text_color: None,
@@ -1549,7 +1527,7 @@ impl Visuals {
text_cursor: Default::default(),
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
clip_rect_margin: 0.0,
button_frame: true,
collapsing_header_frame: false,
indent_has_left_vline: true,
@@ -1573,7 +1551,7 @@ impl Visuals {
Self {
dark_mode: false,
text_options: TextOptions {
alpha_from_coverage: AlphaFromCoverage::LIGHT_MODE_DEFAULT,
color_transfer_function: FontColorTransferFunction::LIGHT_MODE_DEFAULT,
..Default::default()
},
widgets: Widgets::light(),
@@ -1748,7 +1726,6 @@ use crate::{
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
#[expect(deprecated)]
let Self {
override_font_id,
override_text_style,
@@ -1756,7 +1733,6 @@ impl Style {
text_styles,
drag_value_text_style,
number_formatter: _, // can't change callbacks in the UI
wrap: _,
wrap_mode,
spacing,
interaction,
@@ -2369,13 +2345,15 @@ impl Visuals {
let TextOptions {
max_texture_side: _,
alpha_from_coverage,
color_transfer_function,
font_hinting,
subpixel_binning,
} = text_options;
text_alpha_from_coverage_ui(ui, alpha_from_coverage);
color_transfer_function_ui(ui, color_transfer_function);
ui.checkbox(font_hinting, "Enable font hinting");
ui.checkbox(font_hinting, "Font hinting (sharper text)");
ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)");
});
ui.collapsing("Text cursor", |ui| {
@@ -2498,23 +2476,29 @@ impl Visuals {
}
}
fn text_alpha_from_coverage_ui(ui: &mut Ui, alpha_from_coverage: &mut AlphaFromCoverage) {
let mut dark_mode_special =
*alpha_from_coverage == AlphaFromCoverage::TwoCoverageMinusCoverageSq;
fn color_transfer_function_ui(
ui: &mut Ui,
color_transfer_function: &mut FontColorTransferFunction,
) {
ui.horizontal(|ui| {
ui.label("Text rendering:");
ui.label("Opacity tweaking:");
ui.checkbox(&mut dark_mode_special, "Dark-mode special");
ui.radio_value(
color_transfer_function,
FontColorTransferFunction::Off,
"Off",
);
ui.radio_value(
color_transfer_function,
FontColorTransferFunction::DARK_MODE_DEFAULT,
"Dark-mode special",
);
if dark_mode_special {
*alpha_from_coverage = AlphaFromCoverage::DARK_MODE_DEFAULT;
} else {
let mut gamma = match alpha_from_coverage {
AlphaFromCoverage::Linear => 1.0,
AlphaFromCoverage::Gamma(gamma) => *gamma,
AlphaFromCoverage::TwoCoverageMinusCoverageSq => 0.5, // approximately the same
};
let mut use_gamma = matches!(color_transfer_function, FontColorTransferFunction::Gamma(_));
ui.radio_value(&mut use_gamma, true, "Gamma function");
if use_gamma {
let mut gamma = color_transfer_function.to_gamma();
ui.add(
DragValue::new(&mut gamma)
@@ -2523,11 +2507,7 @@ fn text_alpha_from_coverage_ui(ui: &mut Ui, alpha_from_coverage: &mut AlphaFromC
.prefix("Gamma: "),
);
if gamma == 1.0 {
*alpha_from_coverage = AlphaFromCoverage::Linear;
} else {
*alpha_from_coverage = AlphaFromCoverage::Gamma(gamma);
}
*color_transfer_function = FontColorTransferFunction::Gamma(gamma);
}
});
}
@@ -2952,8 +2932,11 @@ impl Widget for &mut FontTweak {
scale,
y_offset_factor,
y_offset,
hinting_override,
hinting,
coords,
thin_space_width,
tab_size,
subpixel_binning,
} = self;
ui.label("Scale");
@@ -2969,18 +2952,20 @@ impl Widget for &mut FontTweak {
ui.add(DragValue::new(y_offset).speed(-0.02));
ui.end_row();
ui.label("hinting_override");
ComboBox::from_id_salt("hinting_override")
.selected_text(match hinting_override {
None => "None",
Some(true) => "Enable",
Some(false) => "Disable",
})
.show_ui(ui, |ui| {
ui.selectable_value(hinting_override, None, "None");
ui.selectable_value(hinting_override, Some(true), "Enable");
ui.selectable_value(hinting_override, Some(false), "Disable");
});
ui.label("hinting");
ui.horizontal(|ui| {
ui.radio_value(hinting, Some(true), "on");
ui.radio_value(hinting, Some(false), "off");
ui.radio_value(hinting, None, "default");
});
ui.end_row();
ui.label("subpixel_binning");
ui.horizontal(|ui| {
ui.radio_value(subpixel_binning, Some(true), "on");
ui.radio_value(subpixel_binning, Some(false), "off");
ui.radio_value(subpixel_binning, None, "default");
});
ui.end_row();
ui.label("coords");
@@ -3026,6 +3011,21 @@ impl Widget for &mut FontTweak {
}
ui.end_row();
ui.label("thin_space_width");
ui.horizontal(|ui| {
ui.add(
DragValue::new(thin_space_width)
.range(0.0..=1.0)
.speed(0.01),
);
ui.label("1\u{2009}234\u{2009}567\u{2009}890");
});
ui.end_row();
ui.label("tab_size");
ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1));
ui.end_row();
if ui.button("Reset").clicked() {
*self = Default::default();
}

View File

@@ -97,12 +97,6 @@ impl CCursorRange {
}
}
#[inline]
#[deprecated = "Use `self.sorted_cursors` instead."]
pub fn sorted(&self) -> [CCursor; 2] {
self.sorted_cursors()
}
pub fn slice_str<'s>(&self, text: &'s str) -> &'s str {
let [min, max] = self.sorted_cursors();
slice_char_range(text, min.index..max.index)

View File

@@ -49,10 +49,15 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
impl std::fmt::Debug for WidgetTextCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
widget_id,
ccursor,
pos: _,
} = self;
f.debug_struct("WidgetTextCursor")
.field("widget_id", &self.widget_id.short_debug_format())
.field("ccursor", &self.ccursor.index)
.finish()
.field("widget_id", &widget_id.short_debug_format())
.field("ccursor", &ccursor.index)
.finish_non_exhaustive()
}
}

View File

@@ -106,38 +106,26 @@ impl TextCursorState {
}
fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange {
if ccursor.index == 0 {
CCursorRange::two(ccursor, ccursor_next_word(text, ccursor))
} else {
let it = text.chars();
let mut it = it.skip(ccursor.index - 1);
if let Some(char_before_cursor) = it.next() {
if let Some(char_after_cursor) = it.next() {
if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
let min = ccursor_previous_word(text, ccursor + 1);
let max = ccursor_next_word(text, min);
CCursorRange::two(min, max)
} else if is_word_char(char_before_cursor) {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, min);
CCursorRange::two(min, max)
} else if is_word_char(char_after_cursor) {
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(ccursor, max)
} else {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(min, max)
}
} else {
let min = ccursor_previous_word(text, ccursor);
CCursorRange::two(min, ccursor)
}
} else {
let max = ccursor_next_word(text, ccursor);
CCursorRange::two(ccursor, max)
}
if text.is_empty() {
return CCursorRange::one(ccursor);
}
let line_start = find_line_start(text, ccursor);
let line_end = ccursor_next_line(text, line_start);
let line_range = line_start.index..line_end.index;
let current_line_text = slice_char_range(text, line_range.clone());
let relative_idx = ccursor.index - line_start.index;
let relative_ccursor = CCursor::new(relative_idx);
let min = ccursor_previous_word(current_line_text, relative_ccursor);
let max = ccursor_next_word(current_line_text, relative_ccursor);
CCursorRange::two(
CCursor::new(line_start.index + min.index),
CCursor::new(line_start.index + max.index),
)
}
fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
@@ -156,13 +144,13 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange {
let min = ccursor_previous_line(text, ccursor);
let max = ccursor_next_line(text, min);
CCursorRange::two(min, max)
} else if !is_linebreak(char_after_cursor) {
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(ccursor, max)
} else {
} else if is_linebreak(char_after_cursor) {
let min = ccursor_previous_line(text, ccursor);
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(min, max)
} else {
let max = ccursor_next_line(text, ccursor);
CCursorRange::two(ccursor, max)
}
} else {
let min = ccursor_previous_line(text, ccursor);
@@ -209,16 +197,20 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor {
}
fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize {
for (word_byte_index, word) in text.split_word_bound_indices() {
let word_ci = char_index_from_byte_index(text, word_byte_index);
let mut current_char_idx = 0;
for (_word_byte_index, word) in text.split_word_bound_indices() {
let word_ci = current_char_idx;
// We consider `.` a word boundary.
// At least that's how Mac works when navigating something like `www.example.com`.
for (dot_ci_offset, chr) in word.chars().enumerate() {
let dot_ci = word_ci + dot_ci_offset;
let mut word_char_count = 0;
for chr in word.chars() {
let dot_ci = word_ci + word_char_count;
if chr == '.' && cursor_ci < dot_ci {
return dot_ci;
}
word_char_count += 1;
}
// Splitting considers contiguous whitespace as one word, such words must be skipped,
@@ -228,9 +220,11 @@ fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize {
if cursor_ci < word_ci && !all_word_chars(word) {
return word_ci;
}
current_char_idx += word_char_count;
}
char_index_from_byte_index(text, text.len())
current_char_idx
}
fn all_word_chars(text: &str) -> bool {
@@ -265,22 +259,14 @@ fn is_linebreak(c: char) -> bool {
/// Accepts and returns character offset (NOT byte offset!).
pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor {
// We know that new lines, '\n', are a single byte char, but we have to
// work with char offsets because before the new line there may be any
// number of multi byte chars.
// We need to know the char index to be able to correctly set the cursor
// later.
let chars_count = text.chars().count();
let byte_idx = byte_index_from_char_index(text, current_index.index);
let text_before = &text[..byte_idx];
let position = text
.chars()
.rev()
.skip(chars_count - current_index.index)
.position(|x| x == '\n');
match position {
Some(pos) => CCursor::new(current_index.index - pos),
None => CCursor::new(0),
if let Some(last_newline_byte) = text_before.rfind('\n') {
let char_idx = char_index_from_byte_index(text, last_newline_byte + 1);
CCursor::new(char_idx)
} else {
CCursor::new(0)
}
}
@@ -329,7 +315,7 @@ pub fn cursor_rect(galley: &Galley, cursor: &CCursor, row_height: f32) -> Rect {
#[cfg(test)]
mod test {
use crate::text_selection::text_cursor_state::next_word_boundary_char_index;
use super::*;
#[test]
fn test_next_word_boundary_char_index() {
@@ -366,4 +352,117 @@ mod test {
assert_eq!(next_word_boundary_char_index(text, 19), 20);
assert_eq!(next_word_boundary_char_index(text, 20), 21);
}
#[test]
fn test_previous_word() {
let text = "abc def ghi";
assert_eq!(ccursor_previous_word(text, CCursor::new(7)).index, 4);
assert_eq!(ccursor_previous_word(text, CCursor::new(5)).index, 4);
assert_eq!(ccursor_previous_word(text, CCursor::new(4)).index, 0);
assert_eq!(ccursor_previous_word(text, CCursor::new(0)).index, 0);
}
#[test]
fn test_next_word() {
let text = "abc def ghi";
assert_eq!(ccursor_next_word(text, CCursor::new(0)).index, 3);
assert_eq!(ccursor_next_word(text, CCursor::new(3)).index, 7);
assert_eq!(ccursor_next_word(text, CCursor::new(7)).index, 11);
assert_eq!(ccursor_next_word(text, CCursor::new(11)).index, 11);
}
#[test]
fn test_select_word_at() {
// CCursorRange::two(min, max) sets primary=max, secondary=min
let text = "hello world";
let range = select_word_at(text, CCursor::new(2));
let (lo, hi) = (
range.primary.index.min(range.secondary.index),
range.primary.index.max(range.secondary.index),
);
assert_eq!(lo, 0);
assert_eq!(hi, 5);
let range = select_word_at(text, CCursor::new(8));
let (lo, hi) = (
range.primary.index.min(range.secondary.index),
range.primary.index.max(range.secondary.index),
);
assert_eq!(lo, 6);
assert_eq!(hi, 11);
}
#[test]
fn test_word_boundary_large_text_performance() {
// Before the O(n²) → O(n) fix, this would take minutes on large text.
let large_text = "word ".repeat(200_000); // ~1MB
let len = large_text.chars().count();
let start = std::time::Instant::now();
let next = ccursor_next_word(&large_text, CCursor::new(len - 10));
assert!(next.index <= len);
let prev = ccursor_previous_word(&large_text, CCursor::new(len - 10));
assert!(prev.index < len);
let range = select_word_at(&large_text, CCursor::new(len - 3));
let lo = range.primary.index.min(range.secondary.index);
let hi = range.primary.index.max(range.secondary.index);
assert!(lo < hi, "Expected a non-empty word selection");
let elapsed = start.elapsed();
assert!(
elapsed.as_secs() < 5,
"Word boundary operations on 1MB text took {elapsed:?}, expected < 5s"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_previous_word_graphemes() {
let cases = [
("", 0, 0),
("hello", 0, 0),
("hello", "hello".chars().count(), 0),
("hello world", 6, 0),
("hello world", 8, 6),
("hello world", "hello world".chars().count(), 6),
("hello world ", "hello world ".chars().count(), 6),
("hello world", "hello world".chars().count(), 8),
(" ", " ".chars().count(), 0),
("hello, world", "hello, world".chars().count(), 7),
("www.example.com", "www.example.com".chars().count(), 12),
("안녕! 😊 세상", 8, 6),
("❤️👍 skvělá knihovna 👍❤️", 18, 11),
(
"a e\u{301} b",
"a e\u{301} b".chars().count(),
"a e\u{301} ".chars().count(),
),
(
"hi 🙂 world",
"hi 🙂 world".chars().count(),
"hi 🙂 ".chars().count(),
),
(
"hi 👨‍👩‍👧‍👦 world",
"hi 👨‍👩‍👧‍👦 world".chars().count(),
"hi 👨‍👩‍👧‍👦 ".chars().count(),
),
];
for (text, cursor, expected) in cases {
let result = ccursor_previous_word(text, CCursor::new(cursor));
assert_eq!(
result.index, expected,
"text={text:?}, cursor={cursor}, got={}, expected={expected}",
result.index
);
}
}
}

View File

@@ -61,7 +61,7 @@ pub fn paint_text_selection(
let last_glyph_index = if ri == max.row {
max.column
} else {
row.glyphs.len() - 1
row.glyphs.len()
};
let first_vertex_index = row

View File

@@ -1,13 +1,13 @@
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
#![expect(clippy::use_self)]
use std::{any::Any, hash::Hash, ops::Deref, sync::Arc};
use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use std::{any::Any, ops::Deref, sync::Arc};
use crate::containers::menu;
use crate::{containers::*, ecolor::*, layout::*, placer::Placer, widgets::*, *};
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
use crate::{IdSource, containers::*, ecolor::*, layout::*, placer::Placer, widgets::*, *};
use emath::GuiRounding as _;
// ----------------------------------------------------------------------------
/// This is what you use to place widgets.
@@ -75,10 +75,6 @@ pub struct Ui {
/// where we size up the contents of the Ui, without actually showing it.
sizing_pass: bool,
/// Indicates whether this Ui belongs to a Menu.
#[expect(deprecated)]
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
/// The [`UiStack`] for this [`Ui`].
stack: Arc<UiStack>,
@@ -111,8 +107,7 @@ impl Ui {
/// [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`].
pub fn new(ctx: Context, id: Id, ui_builder: UiBuilder) -> Self {
let UiBuilder {
id_salt,
global_scope: _,
id_source,
ui_stack_info,
layer_id,
max_rect,
@@ -123,13 +118,14 @@ impl Ui {
style,
sense,
accessibility_parent,
classes,
} = ui_builder;
let layer_id = layer_id.unwrap_or_else(LayerId::background);
debug_assert!(
id_salt.is_none(),
"Top-level Ui:s should not have an id_salt"
id_source.is_none(),
"Top-level Ui:s should not have an UiBuilder::id_source"
);
let max_rect = max_rect.unwrap_or_else(|| ctx.content_rect());
@@ -138,6 +134,7 @@ impl Ui {
let disabled = disabled || invisible;
let style = style.unwrap_or_else(|| ctx.global_style());
let sense = sense.unwrap_or_else(Sense::hover);
let classes = classes.with_class(ROOT_CLASS);
let placer = Placer::new(max_rect, layout);
let ui_stack = UiStack {
@@ -147,7 +144,9 @@ impl Ui {
parent: None,
min_rect: placer.min_rect(),
max_rect: placer.max_rect(),
classes,
};
let mut ui = Ui {
id,
unique_id: id,
@@ -157,7 +156,6 @@ impl Ui {
placer,
enabled: true,
sizing_pass,
menu_state: None,
stack: Arc::new(ui_stack),
sense,
min_rect_already_remembered: false,
@@ -198,47 +196,6 @@ impl Ui {
ui
}
/// Create a new [`Ui`] at a specific region.
///
/// Note: calling this function twice from the same [`Ui`] will create a conflict of id. Use
/// [`Self::scope`] if needed.
///
/// When in doubt, use `None` for the `UiStackInfo` argument.
#[deprecated = "Use ui.new_child() instead"]
pub fn child_ui(
&mut self,
max_rect: Rect,
layout: Layout,
ui_stack_info: Option<UiStackInfo>,
) -> Self {
self.new_child(
UiBuilder::new()
.max_rect(max_rect)
.layout(layout)
.ui_stack_info(ui_stack_info.unwrap_or_default()),
)
}
/// Create a new [`Ui`] at a specific region with a specific id.
///
/// When in doubt, use `None` for the `UiStackInfo` argument.
#[deprecated = "Use ui.new_child() instead"]
pub fn child_ui_with_id_source(
&mut self,
max_rect: Rect,
layout: Layout,
id_salt: impl Hash,
ui_stack_info: Option<UiStackInfo>,
) -> Self {
self.new_child(
UiBuilder::new()
.id_salt(id_salt)
.max_rect(max_rect)
.layout(layout)
.ui_stack_info(ui_stack_info.unwrap_or_default()),
)
}
/// Create a child `Ui` with the properties of the given builder.
///
/// This is a very low-level function.
@@ -250,8 +207,7 @@ impl Ui {
/// [`Ui::advance_cursor_after_rect`].
pub fn new_child(&mut self, ui_builder: UiBuilder) -> Self {
let UiBuilder {
id_salt,
global_scope,
id_source,
ui_stack_info,
layer_id,
max_rect,
@@ -262,11 +218,11 @@ impl Ui {
style,
sense,
accessibility_parent,
classes,
} = ui_builder;
let mut painter = self.painter.clone();
let id_salt = id_salt.unwrap_or_else(|| Id::from("child"));
let max_rect = max_rect.unwrap_or_else(|| self.available_rect_before_wrap());
let mut layout = layout.unwrap_or_else(|| *self.layout());
let enabled = self.enabled && !disabled && !invisible;
@@ -290,13 +246,15 @@ impl Ui {
}
debug_assert!(!max_rect.any_nan(), "max_rect is NaN: {max_rect:?}");
let (stable_id, unique_id) = if global_scope {
(id_salt, id_salt)
} else {
let stable_id = self.id.with(id_salt);
let unique_id = stable_id.with(self.next_auto_id_salt);
(stable_id, unique_id)
let id_source = id_source.unwrap_or_else(|| IdSource::Child(IdSalt::new("child")));
let (stable_id, unique_id) = match id_source {
IdSource::Explicit(id) => (id, id),
IdSource::Child(id_salt) => {
let stable_id = self.id.with(id_salt);
let unique_id = stable_id.with(self.next_auto_id_salt);
(stable_id, unique_id)
}
};
let next_auto_id_salt = unique_id.value().wrapping_add(1);
@@ -310,7 +268,9 @@ impl Ui {
parent: Some(Arc::clone(&self.stack)),
min_rect: placer.min_rect(),
max_rect: placer.max_rect(),
classes,
};
let mut child_ui = Ui {
id: stable_id,
unique_id,
@@ -320,7 +280,6 @@ impl Ui {
placer,
enabled,
sizing_pass,
menu_state: self.menu_state.clone(),
stack: Arc::new(ui_stack),
sense,
min_rect_already_remembered: false,
@@ -362,18 +321,6 @@ impl Ui {
// -------------------------------------------------
/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
///
/// This will also turn the Ui invisible.
/// Should be called right after [`Self::new`], if at all.
#[inline]
#[deprecated = "Use UiBuilder.sizing_pass().invisible()"]
pub fn set_sizing_pass(&mut self) {
self.sizing_pass = true;
self.set_invisible();
}
/// Set to true in special cases where we do one frame
/// where we size up the contents of the Ui, without actually showing it.
#[inline]
@@ -412,7 +359,7 @@ impl Ui {
/// Style options for this [`Ui`] and its children.
///
/// Note that this may be a different [`Style`] than that of [`Context::style`].
/// Note that this may be a different [`Style`] than that of [`Context::global_style`].
#[inline]
pub fn style(&self) -> &Arc<Style> {
&self.style
@@ -554,33 +501,6 @@ impl Ui {
}
}
/// Calling `set_enabled(false)` will cause the [`Ui`] to deny all future interaction
/// and all the widgets will draw with a gray look.
///
/// Usually it is more convenient to use [`Self::add_enabled_ui`] or [`Self::add_enabled`].
///
/// Calling `set_enabled(true)` has no effect - it will NOT re-enable the [`Ui`] once disabled.
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut enabled = true;
/// ui.group(|ui| {
/// ui.checkbox(&mut enabled, "Enable subsection");
/// ui.set_enabled(enabled);
/// if ui.button("Button that is not always clickable").clicked() {
/// /* … */
/// }
/// });
/// # });
/// ```
#[deprecated = "Use disable(), add_enabled_ui(), or add_enabled() instead"]
pub fn set_enabled(&mut self, enabled: bool) {
if !enabled {
self.disable();
}
}
/// If `false`, any widgets added to the [`Ui`] will be invisible and non-interactive.
///
/// This is `false` if any parent had [`UiBuilder::invisible`]
@@ -597,7 +517,7 @@ impl Ui {
///
/// Once invisible, there is no way to make the [`Ui`] visible again.
///
/// Usually it is more convenient to use [`Self::add_visible_ui`] or [`Self::add_visible`].
/// Usually it is more convenient to use [`Self::add_visible`].
///
/// ### Example
/// ```
@@ -619,34 +539,6 @@ impl Ui {
self.disable();
}
/// Calling `set_visible(false)` will cause all further widgets to be invisible,
/// yet still allocate space.
///
/// The widgets will not be interactive (`set_visible(false)` implies `set_enabled(false)`).
///
/// Calling `set_visible(true)` has no effect.
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut visible = true;
/// ui.group(|ui| {
/// ui.checkbox(&mut visible, "Show subsection");
/// ui.set_visible(visible);
/// if ui.button("Button that is not always shown").clicked() {
/// /* … */
/// }
/// });
/// # });
/// ```
#[deprecated = "Use set_invisible(), add_visible_ui(), or add_visible() instead"]
pub fn set_visible(&mut self, visible: bool) {
if !visible {
self.painter.set_invisible();
self.disable();
}
}
/// Make the widget in this [`Ui`] semi-transparent.
///
/// `opacity` must be between 0.0 and 1.0, where 0.0 means fully transparent (i.e., invisible)
@@ -694,17 +586,8 @@ impl Ui {
///
/// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`].
pub fn wrap_mode(&self) -> TextWrapMode {
#[expect(deprecated)]
if let Some(wrap_mode) = self.style.wrap_mode {
wrap_mode
}
// `wrap` handling for backward compatibility
else if let Some(wrap) = self.style.wrap {
if wrap {
TextWrapMode::Wrap
} else {
TextWrapMode::Extend
}
} else if let Some(grid) = self.placer.grid() {
if grid.wrap_text() {
TextWrapMode::Wrap
@@ -721,14 +604,6 @@ impl Ui {
}
}
/// Should text wrap in this [`Ui`]?
///
/// This is determined first by [`Style::wrap_mode`], and then by the layout of this [`Ui`].
#[deprecated = "Use `wrap_mode` instead"]
pub fn wrap_text(&self) -> bool {
self.wrap_mode() == TextWrapMode::Wrap
}
/// How to vertically align text
#[inline]
pub fn text_valign(&self) -> Align {
@@ -1005,11 +880,8 @@ impl Ui {
/// # [`Id`] creation
impl Ui {
/// Use this to generate widget ids for widgets that have persistent state in [`Memory`].
pub fn make_persistent_id<IdSource>(&self, id_salt: IdSource) -> Id
where
IdSource: Hash,
{
self.id.with(&id_salt)
pub fn make_persistent_id(&self, id_salt: impl AsIdSalt) -> Id {
self.id.with(id_salt)
}
/// This is the `Id` that will be assigned to the next widget added to this `Ui`.
@@ -1018,10 +890,7 @@ impl Ui {
}
/// Same as `ui.next_auto_id().with(id_salt)`
pub fn auto_id_with<IdSource>(&self, id_salt: IdSource) -> Id
where
IdSource: Hash,
{
pub fn auto_id_with(&self, id_salt: impl AsIdSalt) -> Id {
Id::new(self.next_auto_id_salt).with(id_salt)
}
@@ -1063,18 +932,6 @@ impl Ui {
)
}
/// Deprecated: use [`Self::interact`] instead.
#[deprecated = "The contains_pointer argument is ignored. Use `ui.interact` instead."]
pub fn interact_with_hovered(
&self,
rect: Rect,
_contains_pointer: bool,
id: Id,
sense: Sense,
) -> Response {
self.interact(rect, id, sense)
}
/// Read the [`Ui`]'s background [`Response`].
/// Its [`Sense`] will be based on the [`UiBuilder::sense`] used to create this [`Ui`].
///
@@ -1086,7 +943,7 @@ impl Ui {
pub fn response(&self) -> Response {
// This is the inverse of Context::read_response. We prefer a response
// based on last frame's widget rect since the one from this frame is Rect::NOTHING until
// Ui::interact_bg is called or the Ui is dropped.
// Ui::remember_min_rect is called or the Ui is dropped.
let mut response = self
.ctx()
.viewport(|viewport| {
@@ -1137,16 +994,6 @@ impl Ui {
response
}
/// Interact with the background of this [`Ui`],
/// i.e. behind all the widgets.
///
/// The rectangle of the [`Response`] (and interactive area) will be [`Self::min_rect`].
#[deprecated = "Use UiBuilder::sense with Ui::response instead"]
pub fn interact_bg(&self, sense: Sense) -> Response {
// This will update the WidgetRect that was first created in `Ui::new`.
self.interact(self.min_rect(), self.unique_id, sense)
}
/// Is the pointer (mouse/touch) above this rectangle in this [`Ui`]?
///
/// The `clip_rect` and layer of this [`Ui`] will be respected, so, for instance,
@@ -1499,34 +1346,6 @@ impl Ui {
)
}
/// Allocated the given rectangle and then adds content to that rectangle.
///
/// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated.
/// So you can request a lot of space and then use less.
#[deprecated = "Use `allocate_new_ui` instead"]
pub fn allocate_ui_at_rect<R>(
&mut self,
max_rect: Rect,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
self.scope_builder(UiBuilder::new().max_rect(max_rect), add_contents)
}
/// Allocated space (`UiBuilder::max_rect`) and then add content to it.
///
/// If the contents overflow, more space will be allocated.
/// When finished, the amount of space actually used (`min_rect`) will be allocated in the parent.
/// So you can request a lot of space and then use less.
#[deprecated = "Use `scope_builder` instead"]
pub fn allocate_new_ui<R>(
&mut self,
ui_builder: UiBuilder,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
self.scope_dyn(ui_builder, Box::new(add_contents))
}
/// Convenience function to get a region to paint on.
///
/// Note that egui uses screen coordinates for everything.
@@ -1817,7 +1636,7 @@ impl Ui {
/// If you call `add_visible` from within an already invisible [`Ui`],
/// the widget will always be invisible, even if the `visible` argument is true.
///
/// See also [`Self::add_visible_ui`], [`Self::set_visible`] and [`Self::is_visible`].
/// See also [`Self::set_invisible`] and [`Self::is_visible`].
///
/// ```
/// # egui::__run_test_ui(|ui| {
@@ -1842,38 +1661,6 @@ impl Ui {
}
}
/// Add a section that is possibly invisible, i.e. greyed out and non-interactive.
///
/// An invisible ui still takes up the same space as if it were visible.
///
/// If you call `add_visible_ui` from within an already invisible [`Ui`],
/// the result will always be invisible, even if the `visible` argument is true.
///
/// See also [`Self::add_visible`], [`Self::set_visible`] and [`Self::is_visible`].
///
/// ### Example
/// ```
/// # egui::__run_test_ui(|ui| {
/// # let mut visible = true;
/// ui.checkbox(&mut visible, "Show subsection");
/// ui.add_visible_ui(visible, |ui| {
/// ui.label("Maybe you see this, maybe you don't!");
/// });
/// # });
/// ```
#[deprecated = "Use 'ui.scope_builder' instead"]
pub fn add_visible_ui<R>(
&mut self,
visible: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
let mut ui_builder = UiBuilder::new();
if !visible {
ui_builder = ui_builder.invisible();
}
self.scope_builder(ui_builder, add_contents)
}
/// Add extra space before the next widget.
///
/// The direction is dependent on the layout.
@@ -2065,10 +1852,10 @@ impl Ui {
///
/// Usage: `if ui.small_button("Click me").clicked() { … }`
///
/// Shortcut for `add(Button::new(text).small())`
/// Shortcut for `add(Button::new(atoms).small())`
#[must_use = "You should check if the user clicked this with `if ui.small_button(…).clicked() { … } "]
pub fn small_button(&mut self, text: impl Into<WidgetText>) -> Response {
Button::new(text).small().ui(self)
pub fn small_button<'a>(&mut self, atoms: impl IntoAtoms<'a>) -> Response {
Button::new(atoms).small().ui(self)
}
/// Show a checkbox.
@@ -2375,27 +2162,12 @@ impl Ui {
/// ```
pub fn push_id<R>(
&mut self,
id_salt: impl Hash,
id_salt: impl AsIdSalt,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.scope_dyn(UiBuilder::new().id_salt(id_salt), Box::new(add_contents))
}
/// Push another level onto the [`UiStack`].
///
/// You can use this, for instance, to tag a group of widgets.
#[deprecated = "Use 'ui.scope_builder' instead"]
pub fn push_stack_info<R>(
&mut self,
ui_stack_info: UiStackInfo,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.scope_dyn(
UiBuilder::new().ui_stack_info(ui_stack_info),
Box::new(add_contents),
)
}
/// Create a scoped child ui.
///
/// You can use this to temporarily change the [`Style`] of a sub-region, for instance:
@@ -2408,11 +2180,16 @@ impl Ui {
/// });
/// # });
/// ```
///
/// See also [`Self::scope_builder`] for more options.
pub fn scope<R>(&mut self, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
self.scope_dyn(UiBuilder::new(), Box::new(add_contents))
}
/// Create a child, add content to it, and then allocate only what was used in the parent `Ui`.
/// Create a scoped child ui, inheriting properties from the parent as specified by the [`UiBuilder`].
/// In contrast to [`Self::new_child`], this allocates the space used by the child.
///
/// See also [`Self::scope`] and [`Self::scope_dyn`].
pub fn scope_builder<R>(
&mut self,
ui_builder: UiBuilder,
@@ -2421,7 +2198,7 @@ impl Ui {
self.scope_dyn(ui_builder, Box::new(add_contents))
}
/// Create a child, add content to it, and then allocate only what was used in the parent `Ui`.
/// [`Self::scope_builder`] but with dynamic dispatch.
pub fn scope_dyn<'c, R>(
&mut self,
ui_builder: UiBuilder,
@@ -2436,26 +2213,6 @@ impl Ui {
InnerResponse::new(ret, response)
}
/// Redirect shapes to another paint layer.
///
/// ```
/// # use egui::{LayerId, Order, Id};
/// # egui::__run_test_ui(|ui| {
/// let layer_id = LayerId::new(Order::Tooltip, Id::new("my_floating_ui"));
/// ui.with_layer_id(layer_id, |ui| {
/// ui.label("This is now in a different layer");
/// });
/// # });
/// ```
#[deprecated = "Use ui.scope_builder(UiBuilder::new().layer_id(…), …) instead"]
pub fn with_layer_id<R>(
&mut self,
layer_id: LayerId,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R> {
self.scope_builder(UiBuilder::new().layer_id(layer_id), add_contents)
}
/// A [`CollapsingHeader`] that starts out collapsed.
///
/// The name must be unique within the current parent,
@@ -2475,7 +2232,7 @@ impl Ui {
#[inline]
pub fn indent<R>(
&mut self,
id_salt: impl Hash,
id_salt: impl AsIdSalt,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.indent_dyn(id_salt, Box::new(add_contents))
@@ -2483,7 +2240,7 @@ impl Ui {
fn indent_dyn<'c, R>(
&mut self,
id_salt: impl Hash,
id_salt: impl AsIdSalt,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
assert!(
@@ -3011,22 +2768,6 @@ impl Ui {
/// # Menus
impl Ui {
/// Close the menu we are in (including submenus), if any.
///
/// See also: [`Self::menu_button`] and [`Response::context_menu`].
#[deprecated = "Use `ui.close()` or `ui.close_kind(UiKind::Menu)` instead"]
pub fn close_menu(&self) {
self.close_kind(UiKind::Menu);
}
#[expect(deprecated)]
pub(crate) fn set_menu_state(
&mut self,
menu_state: Option<Arc<RwLock<crate::menu::MenuState>>>,
) {
self.menu_state = menu_state;
}
#[inline]
/// Create a menu button that when clicked will show the given menu.
///

View File

@@ -1,20 +1,23 @@
use std::{hash::Hash, sync::Arc};
use std::sync::Arc;
use crate::ClosableTag;
#[expect(unused_imports)] // Used for doclinks
use crate::Ui;
use crate::{Id, LayerId, Layout, Rect, Sense, Style, UiStackInfo};
use crate::{
AsIdSalt, ClosableTag, Id, IdSalt, LayerId, Layout, Rect, Sense, Style, UiStackInfo,
widget_style::{Classes, HasClasses},
};
/// Build a [`Ui`] as the child of another [`Ui`].
/// The properties specified when creating a top-level or child [`Ui`].
///
/// By default, everything is inherited from the parent,
/// except for `max_rect` which by default is set to
/// the parent [`Ui::available_rect_before_wrap`].
///
/// See also [`Ui::new`] and [`Ui::new_child`] for uses.
#[must_use]
#[derive(Clone, Default)]
pub struct UiBuilder {
pub id_salt: Option<Id>,
pub global_scope: bool,
pub id_source: Option<IdSource>,
pub ui_stack_info: UiStackInfo,
pub layer_id: Option<LayerId>,
pub max_rect: Option<Rect>,
@@ -25,6 +28,17 @@ pub struct UiBuilder {
pub style: Option<Arc<Style>>,
pub sense: Option<Sense>,
pub accessibility_parent: Option<Id>,
pub classes: Classes,
}
/// Is this [`Ui`] a root or a child of another [`Ui`]?
#[derive(Clone)]
pub enum IdSource {
/// Explicitly use this [`Id`]
Explicit(Id),
/// Salt the parent [`Id`] with this.
Child(IdSalt),
}
impl UiBuilder {
@@ -39,8 +53,8 @@ impl UiBuilder {
/// You should give each [`Ui`] an `id_salt` that is unique
/// within the parent, or give it none at all.
#[inline]
pub fn id_salt(mut self, id_salt: impl Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
self.id_source = Some(IdSource::Child(IdSalt::new(id_salt)));
self
}
@@ -55,20 +69,7 @@ impl UiBuilder {
/// This is a shortcut for `.id_salt(my_id).global_scope(true)`.
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id_salt = Some(id);
self.global_scope = true;
self
}
/// Make the new `Ui` child ids independent of the parent `Ui`.
/// This way child widgets can be moved in the ui tree without losing state.
/// You have to ensure that in a frame the child widgets do not get rendered in multiple places.
///
/// You should set the same globally unique `id_salt` at every place in the ui tree where you want the
/// child widgets to share state.
#[inline]
pub fn global_scope(mut self, global_scope: bool) -> Self {
self.global_scope = global_scope;
self.id_source = Some(IdSource::Explicit(id));
self
}
@@ -192,3 +193,13 @@ impl UiBuilder {
self
}
}
impl HasClasses for UiBuilder {
fn classes(&self) -> &Classes {
&self.classes
}
fn classes_mut(&mut self) -> &mut Classes {
&mut self.classes
}
}

View File

@@ -1,6 +1,7 @@
use std::sync::Arc;
use std::{any::Any, iter::FusedIterator};
use crate::widget_style::Classes;
use epaint::Color32;
use crate::{Direction, Frame, Id, Rect};
@@ -212,6 +213,7 @@ pub struct UiStack {
pub min_rect: Rect,
pub max_rect: Rect,
pub parent: Option<Arc<Self>>,
pub classes: Classes,
}
// these methods act on this specific node

View File

@@ -8,7 +8,3 @@ pub use id_type_map::IdTypeMap;
pub use epaint::emath::History;
pub use epaint::util::{hash, hash_with};
/// Deprecated alias for [`crate::cache`].
#[deprecated = "Use egui::cache instead"]
pub use crate::cache;

View File

@@ -73,7 +73,7 @@ use std::sync::Arc;
use epaint::{Pos2, Vec2};
use crate::{Context, Id, Ui};
use crate::{AsId, Context, Id, Ui};
// ----------------------------------------------------------------------------
@@ -150,7 +150,7 @@ impl ViewportId {
pub const ROOT: Self = Self(Id::NULL);
#[inline]
pub fn from_hash_of(source: impl std::hash::Hash) -> Self {
pub fn from_hash_of(source: impl AsId) -> Self {
Self(Id::new(source))
}
}
@@ -333,6 +333,19 @@ pub struct ViewportBuilder {
// X11
pub window_type: Option<X11WindowType>,
pub override_redirect: Option<bool>,
/// Target monitor index for borderless fullscreen.
///
/// When set, the window is placed in borderless fullscreen on the monitor at
/// the given index in `available_monitors()` order (same order returned by
/// winit). Works on Windows, macOS, and Linux (X11 + Wayland).
///
/// If the index is out of range, it is ignored and a warning is logged.
///
/// Takes precedence over [`Self::with_position`] / [`Self::with_fullscreen`]
/// for monitor selection: if both are set, the window will be fullscreen on
/// the chosen monitor.
pub monitor: Option<usize>,
}
impl ViewportBuilder {
@@ -680,6 +693,20 @@ impl ViewportBuilder {
self
}
/// Place the window in borderless fullscreen on the monitor at `index`.
///
/// The index refers to the order returned by winit's `available_monitors()`.
/// Works cross-platform (Windows, macOS, Linux X11 + Wayland). On Wayland
/// this is the only reliable way to target a specific output, since
/// absolute window positions are not exposed.
///
/// If the index is out of range, the flag is ignored at window creation time.
#[inline]
pub fn with_monitor(mut self, index: usize) -> Self {
self.monitor = Some(index);
self
}
/// Update this `ViewportBuilder` with a delta,
/// returning a list of commands and a bool indicating if the window needs to be recreated.
#[must_use]
@@ -717,6 +744,7 @@ impl ViewportBuilder {
taskbar: new_taskbar,
window_type: new_window_type,
override_redirect: new_override_redirect,
monitor: new_monitor,
} = new_vp_builder;
let mut commands = Vec::new();
@@ -919,6 +947,13 @@ impl ViewportBuilder {
recreate_window = true;
}
if let Some(new_monitor) = new_monitor
&& Some(new_monitor) != self.monitor
{
self.monitor = Some(new_monitor);
commands.push(ViewportCommand::SetMonitor(new_monitor));
}
(commands, recreate_window)
}
}
@@ -1105,6 +1140,12 @@ pub enum ViewportCommand {
/// Turn borderless fullscreen on/off.
Fullscreen(bool),
/// Move the window to borderless fullscreen on the monitor at the given index.
///
/// Index refers to winit's `available_monitors()` order. If out of range, the
/// command is ignored (logged as a warning).
SetMonitor(usize),
/// Show window decorations, i.e. the chrome around the content
/// with the title bar, close buttons, resize handles, etc.
Decorations(bool),
@@ -1201,7 +1242,7 @@ impl ViewportCommand {
/// Describes a viewport, i.e. a native window.
///
/// This is returned by [`crate::Context::run`] on each frame, and should be applied
/// This is returned by [`crate::Context::run_ui`] on each frame, and should be applied
/// by the integration.
#[derive(Clone)]
pub struct ViewportOutput {

View File

@@ -1,8 +1,11 @@
use std::{borrow::Cow, fmt};
use emath::Vec2;
use epaint::{Color32, FontId, Shadow, Stroke, text::TextWrapMode};
use smallvec::SmallVec;
use crate::{
Frame, Response, Style, TextStyle,
Frame, Response, Style, TextBuffer as _, TextStyle,
style::{WidgetVisuals, Widgets},
};
@@ -28,11 +31,13 @@ pub struct WidgetStyle {
pub stroke: Stroke,
}
/// Dedicated button style
pub struct ButtonStyle {
pub frame: Frame,
pub text_style: TextVisuals,
}
/// Dedicated checkbox style
pub struct CheckboxStyle {
/// Frame around
pub frame: Frame,
@@ -53,6 +58,7 @@ pub struct CheckboxStyle {
pub check_stroke: Stroke,
}
/// Dedicated label style
pub struct LabelStyle {
/// Frame around
pub frame: Frame,
@@ -64,6 +70,7 @@ pub struct LabelStyle {
pub wrap_mode: TextWrapMode,
}
/// Dedicated separator style
pub struct SeparatorStyle {
/// How much space is allocated in the layout direction
pub spacing: f32,
@@ -72,6 +79,7 @@ pub struct SeparatorStyle {
pub stroke: Stroke,
}
/// The different state of a widget can be
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
pub enum WidgetState {
Noninteractive,
@@ -82,6 +90,7 @@ pub enum WidgetState {
}
impl Widgets {
/// The widget visuals according to the state
pub fn state(&self, state: WidgetState) -> &WidgetVisuals {
match state {
WidgetState::Noninteractive => &self.noninteractive,
@@ -107,7 +116,8 @@ impl Response {
}
impl Style {
pub fn widget_style(&self, state: WidgetState) -> WidgetStyle {
/// The general widget style. The style is computed according to the classes and state of the widget.
pub fn widget_style(&self, _classes: &Classes, state: WidgetState) -> WidgetStyle {
let visuals = self.visuals.widgets.state(state);
let font_id = self.override_font_id.clone();
WidgetStyle {
@@ -131,11 +141,13 @@ impl Style {
}
}
pub fn button_style(&self, state: WidgetState, selected: bool) -> ButtonStyle {
/// The dedicated button style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn button_style(&self, classes: &Classes, state: WidgetState) -> ButtonStyle {
let mut visuals = *self.visuals.widgets.state(state);
let mut ws = self.widget_style(state);
let mut ws = self.widget_style(classes, state);
if selected {
if classes.has(SELECTED_CLASS) {
visuals.weak_bg_fill = self.visuals.selection.bg_fill;
visuals.bg_fill = self.visuals.selection.bg_fill;
visuals.fg_stroke = self.visuals.selection.stroke;
@@ -157,9 +169,11 @@ impl Style {
}
}
pub fn checkbox_style(&self, state: WidgetState) -> CheckboxStyle {
/// The dedicated checkbox style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn checkbox_style(&self, classes: &Classes, state: WidgetState) -> CheckboxStyle {
let visuals = self.visuals.widgets.state(state);
let ws = self.widget_style(state);
let ws = self.widget_style(classes, state);
CheckboxStyle {
frame: Frame::new(),
checkbox_size: self.spacing.icon_width,
@@ -168,8 +182,6 @@ impl Style {
fill: visuals.bg_fill,
corner_radius: visuals.corner_radius,
stroke: visuals.bg_stroke,
// Use the inner_margin for the expansion
inner_margin: visuals.expansion.into(),
..Default::default()
},
text_style: ws.text,
@@ -177,8 +189,10 @@ impl Style {
}
}
pub fn label_style(&self, state: WidgetState) -> LabelStyle {
let ws = self.widget_style(state);
/// The dedicated label style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn label_style(&self, classes: &Classes, state: WidgetState) -> LabelStyle {
let ws = self.widget_style(classes, state);
LabelStyle {
frame: Frame {
fill: ws.frame.fill,
@@ -193,7 +207,9 @@ impl Style {
}
}
pub fn separator_style(&self, _state: WidgetState) -> SeparatorStyle {
/// The dedicated separator style. The style is computed according to the classes and state of the widget.
/// It depend on the general widget style.
pub fn separator_style(&self, _classes: &Classes, _state: WidgetState) -> SeparatorStyle {
let visuals = self.visuals.noninteractive();
SeparatorStyle {
spacing: 6.0,
@@ -201,3 +217,102 @@ impl Style {
}
}
}
/// The root class is a special class present on every top-level [`crate::Ui`].
pub const ROOT_CLASS: &str = "root";
/// The selected class is a special class present on selected [`crate::Button`].
pub const SELECTED_CLASS: &str = "selected";
/// A class is a static string identifier.
pub type ClassName = Cow<'static, str>;
/// Classes are string identifier that can be set on widget/Ui.
///
/// This can be used by styling engine to compute a different style
/// based on the set of classes present on the widget/Ui.
#[derive(Debug, Default, Clone)]
pub struct Classes {
classes: SmallVec<[ClassName; 5]>,
}
impl Classes {
/// Add a class to the list if the condition is true
#[inline]
fn add_if(&mut self, class: impl Into<ClassName>, condition: bool) {
if condition {
self.classes.push(class.into());
}
}
}
impl HasClasses for Classes {
fn classes(&self) -> &Classes {
self
}
fn classes_mut(&mut self) -> &mut Classes {
self
}
}
impl std::fmt::Display for Classes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.classes.iter().for_each(|class| {
let _ = f.write_str(class.as_str());
});
f.write_str("")
}
}
/// Any widgets supporting [`Classes`] must implement this trait
pub trait HasClasses {
fn classes(&self) -> &Classes;
fn classes_mut(&mut self) -> &mut Classes;
/// Add the given class by consuming [`self`]
#[inline]
fn with_class(mut self, class: impl Into<ClassName>) -> Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), true);
self
}
/// Add the given class by consuming [`self`] if the condition is true
#[inline]
fn with_class_if(mut self, class: impl Into<ClassName>, condition: bool) -> Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), condition);
self
}
/// Add the given class in-place
#[inline]
fn add_class(&mut self, class: impl Into<ClassName>) -> &mut Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), true);
self
}
/// Add the given class in-place if the condition is true
#[inline]
fn add_class_if(&mut self, class: impl Into<ClassName>, condition: bool) -> &mut Self
where
Self: Sized,
{
self.classes_mut().add_if(class.into(), condition);
self
}
/// True if the class is present
fn has(&self, class: impl Into<ClassName>) -> bool {
self.classes().classes.contains(&class.into())
}
}

View File

@@ -1,4 +1,3 @@
use emath::GuiRounding as _;
use epaint::text::{IntoTag, TextFormat, VariationCoords};
use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc};
@@ -156,7 +155,7 @@ impl RichText {
/// Default: 0.0.
///
/// For even text it is recommended you round this to an even number of _pixels_,
/// e.g. using [`crate::Painter::round_to_pixel`].
/// e.g. using [`emath::GuiRounding`].
#[inline]
pub fn extra_letter_spacing(mut self, extra_letter_spacing: f32) -> Self {
self.extra_letter_spacing = extra_letter_spacing;
@@ -170,7 +169,7 @@ impl RichText {
/// If `None` (the default), the line height is determined by the font.
///
/// For even text it is recommended you round this to an even number of _pixels_,
/// e.g. using [`crate::Painter::round_to_pixel`].
/// e.g. using [`emath::GuiRounding`].
#[inline]
pub fn line_height(mut self, line_height: Option<f32>) -> Self {
self.line_height = line_height;
@@ -692,22 +691,6 @@ impl WidgetText {
self.map_rich_text(|text| text.background_color(background_color))
}
/// Returns a value rounded to [`emath::GUI_ROUNDING`].
pub(crate) fn font_height(&self, fonts: &mut epaint::FontsView<'_>, style: &Style) -> f32 {
match self {
Self::Text(_) => fonts.row_height(&FontSelection::Default.resolve(style)),
Self::RichText(text) => text.font_height(fonts, style),
Self::LayoutJob(job) => job.font_height(fonts),
Self::Galley(galley) => {
if let Some(placed_row) = galley.rows.first() {
placed_row.height().round_ui()
} else {
galley.size().y.round_ui()
}
}
}
}
pub fn into_layout_job(
self,
style: &Style,

View File

@@ -4,7 +4,7 @@ use crate::{
Atom, AtomExt as _, AtomKind, AtomLayout, AtomLayoutResponse, Color32, CornerRadius, Frame,
Image, IntoAtoms, NumExt as _, Response, Sense, Stroke, TextStyle, TextWrapMode, Ui, Vec2,
Widget, WidgetInfo, WidgetText, WidgetType,
widget_style::{ButtonStyle, WidgetState},
widget_style::{ButtonStyle, Classes, HasClasses, SELECTED_CLASS, WidgetState},
};
/// Clickable button with text.
@@ -38,6 +38,7 @@ pub struct Button<'a> {
selected: bool,
image_tint_follows_text_color: bool,
limit_image_size: bool,
classes: Classes,
}
impl<'a> Button<'a> {
@@ -56,6 +57,7 @@ impl<'a> Button<'a> {
selected: false,
image_tint_follows_text_color: false,
limit_image_size: false,
classes: Classes::default(),
}
}
@@ -200,12 +202,6 @@ impl<'a> Button<'a> {
self
}
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
/// If true, the tint of the image is multiplied by the widget text color.
///
/// This makes sense for images that are white, that should have the same color as the text color.
@@ -292,6 +288,7 @@ impl<'a> Button<'a> {
selected,
image_tint_follows_text_color,
limit_image_size,
mut classes,
} = self;
// Min size height always equal or greater than interact size if not small
@@ -317,7 +314,9 @@ impl<'a> Button<'a> {
let response: Option<Response> = ui.ctx().read_response(id);
let state = response.map(|r| r.widget_state()).unwrap_or_default();
let ButtonStyle { frame, text_style } = ui.style().button_style(state, selected);
classes.add_class_if(SELECTED_CLASS, selected);
let ButtonStyle { frame, text_style } = ui.style().button_style(&classes, state);
let mut button_padding = if has_frame_margin {
frame.inner_margin
@@ -394,3 +393,13 @@ impl Widget for Button<'_> {
self.atom_ui(ui).response
}
}
impl HasClasses for Button<'_> {
fn classes(&self) -> &Classes {
&self.classes
}
fn classes_mut(&mut self) -> &mut Classes {
&mut self.classes
}
}

View File

@@ -2,7 +2,8 @@ use emath::Rect;
use crate::{
Atom, AtomLayout, Atoms, Id, IntoAtoms, NumExt as _, Response, Sense, Shape, Ui, Vec2, Widget,
WidgetInfo, WidgetType, epaint, pos2, widget_style::CheckboxStyle,
WidgetInfo, WidgetType, epaint, pos2,
widget_style::{CheckboxStyle, Classes, HasClasses},
};
// TODO(emilk): allow checkbox without a text label
@@ -23,6 +24,7 @@ pub struct Checkbox<'a> {
checked: &'a mut bool,
atoms: Atoms<'a>,
indeterminate: bool,
classes: Classes,
}
impl<'a> Checkbox<'a> {
@@ -31,6 +33,7 @@ impl<'a> Checkbox<'a> {
checked,
atoms: atoms.into_atoms(),
indeterminate: false,
classes: Classes::default(),
}
}
@@ -55,6 +58,7 @@ impl Widget for Checkbox<'_> {
checked,
mut atoms,
indeterminate,
classes,
} = self;
// Get the widget style by reading the response from the previous pass
@@ -69,7 +73,7 @@ impl Widget for Checkbox<'_> {
frame,
check_stroke,
text_style,
} = ui.style().checkbox_style(state);
} = ui.style().checkbox_style(&classes, state);
let mut min_size = Vec2::splat(ui.spacing().interact_size.y);
min_size.y = min_size.y.at_least(checkbox_size);
@@ -153,3 +157,13 @@ impl Widget for Checkbox<'_> {
}
}
}
impl HasClasses for Checkbox<'_> {
fn classes(&self) -> &Classes {
&self.classes
}
fn classes_mut(&mut self) -> &mut Classes {
&mut self.classes
}
}

View File

@@ -91,16 +91,6 @@ impl<'a> DragValue<'a> {
self
}
/// Sets valid range for the value.
///
/// By default all values are clamped to this range, even when not interacted with.
/// You can change this behavior by passing `false` to [`Self::clamp_existing_to_range`].
#[deprecated = "Use `range` instead"]
#[inline]
pub fn clamp_range<Num: emath::Numeric>(self, range: RangeInclusive<Num>) -> Self {
self.range(range)
}
/// Sets valid range for dragging the value.
///
/// By default all values are clamped to this range, even when not interacted with.
@@ -157,12 +147,6 @@ impl<'a> DragValue<'a> {
self
}
#[inline]
#[deprecated = "Renamed clamp_existing_to_range"]
pub fn clamp_to_range(self, clamp_to_range: bool) -> Self {
self.clamp_existing_to_range(clamp_to_range)
}
/// Show a prefix before the number, e.g. "x: "
#[inline]
pub fn prefix(mut self, prefix: impl IntoAtoms<'a>) -> Self {
@@ -551,7 +535,7 @@ impl Widget for DragValue<'_> {
if let Some(value_text) = value_text {
// We were editing the value as text last frame, but lost focus.
// Make sure we applied the last text value:
let parsed_value = parse(&custom_parser, &value_text);
let parsed_value = parse(custom_parser.as_ref(), &value_text);
if let Some(mut parsed_value) = parsed_value {
// User edits always clamps:
parsed_value = clamp_value_to_range(parsed_value, range.clone());
@@ -591,7 +575,7 @@ impl Widget for DragValue<'_> {
response.lost_focus() && !ui.input(|i| i.key_pressed(Key::Escape))
};
if update {
let parsed_value = parse(&custom_parser, &value_text);
let parsed_value = parse(custom_parser.as_ref(), &value_text);
if let Some(mut parsed_value) = parsed_value {
// User edits always clamps:
parsed_value = clamp_value_to_range(parsed_value, range.clone());
@@ -733,8 +717,8 @@ impl Widget for DragValue<'_> {
}
}
fn parse(custom_parser: &Option<NumParser<'_>>, value_text: &str) -> Option<f64> {
match &custom_parser {
fn parse(custom_parser: Option<&NumParser<'_>>, value_text: &str) -> Option<f64> {
match custom_parser {
Some(parser) => parser(value_text),
None => default_parser(value_text),
}

View File

@@ -256,18 +256,6 @@ impl<'a> Image<'a> {
self
}
/// Round the corners of the image.
///
/// The default is no rounding ([`CornerRadius::ZERO`]).
///
/// Due to limitations in the current implementation,
/// this will turn off any rotation of the image.
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
/// Show a spinner when the image is loading.
///
/// By default this uses the value of [`crate::Visuals::image_loading_spinners`].

View File

@@ -1,164 +0,0 @@
use crate::{
Color32, CornerRadius, Image, Rect, Response, Sense, Ui, Vec2, Widget, WidgetInfo, WidgetType,
widgets,
};
/// A clickable image within a frame.
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
#[derive(Clone, Debug)]
#[deprecated(since = "0.33.0", note = "Use egui::Button::image instead")]
pub struct ImageButton<'a> {
pub(crate) image: Image<'a>,
sense: Sense,
frame: bool,
selected: bool,
alt_text: Option<String>,
}
#[expect(deprecated, reason = "Deprecated in egui 0.33.0")]
impl<'a> ImageButton<'a> {
pub fn new(image: impl Into<Image<'a>>) -> Self {
Self {
image: image.into(),
sense: Sense::click(),
frame: true,
selected: false,
alt_text: None,
}
}
/// Select UV range. Default is (0,0) in top-left, (1,1) bottom right.
#[inline]
pub fn uv(mut self, uv: impl Into<Rect>) -> Self {
self.image = self.image.uv(uv);
self
}
/// Multiply image color with this. Default is WHITE (no tint).
#[inline]
pub fn tint(mut self, tint: impl Into<Color32>) -> Self {
self.image = self.image.tint(tint);
self
}
/// If `true`, mark this button as "selected".
#[inline]
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
/// Turn off the frame
#[inline]
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
self
}
/// By default, buttons senses clicks.
/// Change this to a drag-button with `Sense::drag()`.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
/// Set rounding for the `ImageButton`.
///
/// If the underlying image already has rounding, this
/// will override that value.
#[inline]
pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
self.image = self.image.corner_radius(corner_radius.into());
self
}
/// Set rounding for the `ImageButton`.
///
/// If the underlying image already has rounding, this
/// will override that value.
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
}
#[expect(deprecated, reason = "Deprecated in egui 0.33.0")]
impl Widget for ImageButton<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let padding = if self.frame {
// so we can see that it is a button:
Vec2::splat(ui.spacing().button_padding.x)
} else {
Vec2::ZERO
};
let available_size_for_image = ui.available_size() - 2.0 * padding;
let tlr = self.image.load_for_size(ui.ctx(), available_size_for_image);
let image_source_size = tlr.as_ref().ok().and_then(|t| t.size());
let image_size = self
.image
.calc_size(available_size_for_image, image_source_size);
let padded_size = image_size + 2.0 * padding;
let (rect, response) = ui.allocate_exact_size(padded_size, self.sense);
response.widget_info(|| {
let mut info = WidgetInfo::new(WidgetType::Button);
info.label = self.alt_text.clone();
info
});
if ui.is_rect_visible(rect) {
let (expansion, rounding, fill, stroke) = if self.selected {
let selection = ui.visuals().selection;
(
Vec2::ZERO,
self.image.image_options().corner_radius,
selection.bg_fill,
selection.stroke,
)
} else if self.frame {
let visuals = ui.style().interact(&response);
let expansion = Vec2::splat(visuals.expansion);
(
expansion,
self.image.image_options().corner_radius,
visuals.weak_bg_fill,
visuals.bg_stroke,
)
} else {
Default::default()
};
// Draw frame background (for transparent images):
ui.painter()
.rect_filled(rect.expand2(expansion), rounding, fill);
let image_rect = ui
.layout()
.align_size_within_rect(image_size, rect.shrink2(padding));
// let image_rect = image_rect.expand2(expansion); // can make it blurry, so let's not
let image_options = self.image.image_options().clone();
widgets::image::paint_texture_load_result(
ui,
&tlr,
image_rect,
None,
&image_options,
self.alt_text.as_deref(),
);
// Draw frame outline:
ui.painter().rect_stroke(
rect.expand2(expansion),
rounding,
stroke,
epaint::StrokeKind::Inside,
);
}
widgets::image::texture_load_result_response(&self.image.source(ui.ctx()), &tlr, response)
}
}

View File

@@ -4,7 +4,7 @@
//! * `ui.add(Label::new("Text").text_color(color::red));`
//! * `if ui.add(Button::new("Click me")).clicked() { … }`
use crate::{Response, Ui, epaint};
use crate::{Response, Ui};
mod button;
mod checkbox;
@@ -12,19 +12,14 @@ pub mod color_picker;
pub(crate) mod drag_value;
mod hyperlink;
mod image;
mod image_button;
mod label;
mod progress_bar;
mod radio_button;
mod selected_label;
mod separator;
mod slider;
mod spinner;
pub mod text_edit;
#[expect(deprecated)]
pub use self::selected_label::SelectableLabel;
#[expect(deprecated, reason = "Deprecated in egui 0.33.0")]
pub use self::{
button::Button,
checkbox::Checkbox,
@@ -34,7 +29,6 @@ pub use self::{
FrameDurations, Image, ImageFit, ImageOptions, ImageSize, ImageSource,
decode_animated_image_uri, has_gif_magic_header, has_webp_header, paint_texture_at,
},
image_button::ImageButton,
label::Label,
progress_bar::ProgressBar,
radio_button::RadioButton,
@@ -126,14 +120,6 @@ pub fn reset_button_with<T: PartialEq>(ui: &mut Ui, value: &mut T, text: &str, r
// ----------------------------------------------------------------------------
#[deprecated = "Use `ui.add(&mut stroke)` instead"]
pub fn stroke_ui(ui: &mut crate::Ui, stroke: &mut epaint::Stroke, text: &str) {
ui.horizontal(|ui| {
ui.label(text);
ui.add(stroke);
});
}
/// Show a small button to switch to/from dark/light mode (globally).
pub fn global_theme_preference_switch(ui: &mut Ui) {
if let Some(new_theme) = ui.ctx().theme().small_toggle_button(ui) {
@@ -147,15 +133,3 @@ pub fn global_theme_preference_buttons(ui: &mut Ui) {
theme_preference.radio_buttons(ui);
ui.ctx().set_theme(theme_preference);
}
/// Show a small button to switch to/from dark/light mode (globally).
#[deprecated = "Use global_theme_preference_switch instead"]
pub fn global_dark_light_mode_switch(ui: &mut Ui) {
global_theme_preference_switch(ui);
}
/// Show larger buttons for switching between light and dark mode (globally).
#[deprecated = "Use global_theme_preference_buttons instead"]
pub fn global_dark_light_mode_buttons(ui: &mut Ui) {
global_theme_preference_buttons(ui);
}

View File

@@ -94,12 +94,6 @@ impl ProgressBar {
self.corner_radius = Some(corner_radius.into());
self
}
#[inline]
#[deprecated = "Renamed to `corner_radius`"]
pub fn rounding(self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius(corner_radius)
}
}
impl Widget for ProgressBar {

View File

@@ -1,13 +0,0 @@
#![expect(deprecated, clippy::new_ret_no_self)]
use crate::WidgetText;
#[deprecated = "Use `Button::selectable()` instead"]
pub struct SelectableLabel {}
impl SelectableLabel {
#[deprecated = "Use `Button::selectable()` instead"]
pub fn new(selected: bool, text: impl Into<WidgetText>) -> super::Button<'static> {
crate::Button::selectable(selected, text)
}
}

View File

@@ -1,4 +1,7 @@
use crate::{Response, Sense, Ui, Vec2, Widget, vec2, widget_style::SeparatorStyle};
use crate::{
Response, Sense, Ui, Vec2, Widget, vec2,
widget_style::{Classes, HasClasses, SeparatorStyle},
};
/// A visual separator. A horizontal or vertical line (depending on [`crate::Layout`]).
///
@@ -16,6 +19,7 @@ pub struct Separator {
spacing: Option<f32>,
grow: f32,
is_horizontal_line: Option<bool>,
classes: Classes,
}
impl Default for Separator {
@@ -24,6 +28,7 @@ impl Default for Separator {
spacing: None,
grow: 0.0,
is_horizontal_line: None,
classes: Classes::default(),
}
}
}
@@ -91,6 +96,7 @@ impl Widget for Separator {
spacing,
grow,
is_horizontal_line,
classes,
} = self;
// Get the widget style by reading the response from the previous pass
@@ -100,7 +106,7 @@ impl Widget for Separator {
let SeparatorStyle {
spacing: spacing_style,
stroke,
} = ui.style().separator_style(state);
} = ui.style().separator_style(&classes, state);
// override the spacing if not set
let spacing = spacing.unwrap_or(spacing_style);
@@ -142,3 +148,13 @@ impl Widget for Separator {
response
}
}
impl HasClasses for Separator {
fn classes(&self) -> &Classes {
&self.classes
}
fn classes_mut(&mut self) -> &mut Classes {
&mut self.classes
}
}

View File

@@ -79,7 +79,7 @@ pub enum SliderClamping {
///
/// The slider range defines the values you get when pulling the slider to the far edges.
/// By default all values are clamped to this range, even when not interacted with.
/// You can change this behavior by passing `false` to [`Slider::clamp_to_range`].
/// You can change this behavior by passing `false` to [`Slider::clamping`].
///
/// The range can include any numbers, and go from low-to-high or from high-to-low.
///
@@ -288,16 +288,6 @@ impl<'a> Slider<'a> {
self
}
#[inline]
#[deprecated = "Use `slider.clamping(…) instead"]
pub fn clamp_to_range(self, clamp_to_range: bool) -> Self {
self.clamping(if clamp_to_range {
SliderClamping::Always
} else {
SliderClamping::Never
})
}
/// Turn smart aim on/off. Default is ON.
/// There is almost no point in turning this off.
#[inline]
@@ -314,7 +304,7 @@ impl<'a> Slider<'a> {
/// Default: `0.0` (disabled).
#[inline]
pub fn step_by(mut self, step: f64) -> Self {
self.step = if step != 0.0 { Some(step) } else { None };
self.step = if step == 0.0 { None } else { Some(step) };
self
}

View File

@@ -4,14 +4,17 @@ use emath::{Rect, TSTransform};
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
use crate::{
Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon,
Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key,
KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer,
TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
Align, Align2, AsIdSalt, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context,
CursorIcon, Event, EventFilter, FontSelection, Frame, Id, IdSalt, ImeEvent, IntoAtoms,
IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense,
SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint,
os::OperatingSystem,
output::OutputEvent,
response, text_selection,
text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection},
response,
text_edit::state::TextEditCursorPurpose,
text_selection::{
self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection,
},
vec2,
};
@@ -69,7 +72,7 @@ pub struct TextEdit<'t> {
suffix: Atoms<'static>,
hint_text: Atoms<'static>,
id: Option<Id>,
id_salt: Option<Id>,
id_salt: Option<IdSalt>,
font_selection: FontSelection,
text_color: Option<Color32>,
layouter: Option<LayouterFn<'t>>,
@@ -168,14 +171,14 @@ impl<'t> TextEdit<'t> {
/// A source for the unique [`Id`], e.g. `.id_source("second_text_edit_field")` or `.id_source(loop_index)`.
#[inline]
pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self {
pub fn id_source(self, id_salt: impl AsIdSalt) -> Self {
self.id_salt(id_salt)
}
/// A source for the unique [`Id`], e.g. `.id_salt("second_text_edit_field")` or `.id_salt(loop_index)`.
#[inline]
pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self {
self.id_salt = Some(Id::new(id_salt));
pub fn id_salt(mut self, id_salt: impl AsIdSalt) -> Self {
self.id_salt = Some(IdSalt::new(id_salt));
self
}
@@ -477,11 +480,14 @@ impl TextEdit<'_> {
let font_id_clone = font_id.clone();
let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| {
let text = mask_if_password(password, text.as_str());
let layout_job = if multiline {
let mut layout_job = if multiline {
LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width)
} else {
LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color)
};
layout_job.halign = align.x();
// We want to keep the trailing whitespace, since hiding it feels really weird when typing
layout_job.keep_trailing_whitespace = true;
ui.fonts_mut(|f| f.layout_job(layout_job))
};
@@ -588,6 +594,7 @@ impl TextEdit<'_> {
if !shrunk && matches!(atom.kind, AtomKind::Text(_)) {
// elide the hint_text if needed
atom = atom.atom_shrink(true);
atom = atom.atom_grow(true);
shrunk = true;
}
@@ -616,6 +623,11 @@ impl TextEdit<'_> {
get_galley = Some(galley);
} else {
// We need to shrink when clip_text, so that we don't exceed the available size
// and thus clip. We also need to shrink in multi line text edits, so text can
// wrap appropriately.
let should_shrink = clip_text || multiline;
// We need a closure here, so we can calculate the galley based on the available
// width (after adding suffix and prefix), for correct wrapping in multi line text
// edits
@@ -642,16 +654,13 @@ impl TextEdit<'_> {
sized: SizedAtomKind::Empty { size: Some(size) },
}
})
.atom_grow(true)
.atom_align(self.align)
.atom_id(inner_rect_id)
.atom_shrink(clip_text),
.atom_shrink(should_shrink),
);
}
// Ensure the suffix is always right-aligned
if !suffix.is_empty() {
atoms.push_right(Atom::grow());
}
// TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have
// smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues.
for atom in suffix {
@@ -676,11 +685,13 @@ impl TextEdit<'_> {
.max_width(allocate_width)
.sense(sense)
.frame(frame)
.align2(Align2::LEFT_TOP)
.align2(align)
.wrap_mode(wrap_mode)
.allocate(ui);
allocated.frame = if !custom_frame {
allocated.frame = if custom_frame {
allocated.frame
} else {
let visuals = ui.style().interact(&allocated.response);
let background_color =
background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
@@ -713,8 +724,6 @@ impl TextEdit<'_> {
)
.outer_margin(Margin::same(-(visuals.expansion as i8)))
.stroke(stroke)
} else {
allocated.frame
};
allocated.paint(ui)
@@ -737,16 +746,18 @@ impl TextEdit<'_> {
// TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac)
let cursor_at_pointer =
galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset);
let cursor_at_pointer = galley.cursor_from_pos(
pointer_pos - inner_rect.min + state.text_offset + vec2(galley.rect.left(), 0.0),
);
if ui.visuals().text_cursor.preview
&& response.hovered()
&& ui.input(|i| i.pointer.is_moving())
{
// text cursor preview:
let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2())
* cursor_rect(&galley, &cursor_at_pointer, row_height);
let cursor_rect = TSTransform::from_translation(
inner_rect.min.to_vec2() - vec2(galley.rect.left(), 0.0),
) * cursor_rect(&galley, &cursor_at_pointer, row_height);
text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect);
}
@@ -832,7 +843,7 @@ impl TextEdit<'_> {
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height)
.translate(galley_pos.to_vec2());
.translate(galley_pos.to_vec2() - vec2(galley.rect.left(), 0.0));
if response.changed() || selection_changed {
// Scroll to keep primary cursor in view:
@@ -858,33 +869,24 @@ impl TextEdit<'_> {
now - state.last_interaction_time,
);
}
// Set IME output (in screen coords) when text is editable and visible
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * inner_rect,
cursor_rect: to_global * primary_cursor_rect,
if ui.memory(|mem| mem.owns_ime_events(id)) {
// Set IME output (in screen coords) when text is editable and visible
let to_global = ui
.ctx()
.layer_transform_to_global(ui.layer_id())
.unwrap_or_default();
ui.output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * inner_rect,
cursor_rect: to_global * primary_cursor_rect,
should_interrupt_composition: false,
});
});
});
}
}
}
}
// Ensures correct IME behavior when the text input area gains or loses focus.
if state.ime_enabled && (response.gained_focus() || response.lost_focus()) {
state.ime_enabled = false;
if let Some(mut ccursor_range) = state.cursor.char_range() {
ccursor_range.secondary.index = ccursor_range.primary.index;
state.cursor.set_char_range(Some(ccursor_range));
}
ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_))));
}
state.clone().store(ui.ctx(), id);
if response.changed() {
@@ -999,6 +1001,11 @@ fn events(
let events = ui.input(|i| i.filtered_events(&event_filter));
let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id));
if !owns_ime_events {
state.cursor_purpose = TextEditCursorPurpose::Selection;
}
for event in &events {
let did_mutate_text = match event {
// First handle events that only changes the selection cursor, not the text:
@@ -1019,7 +1026,9 @@ fn events(
}
}
Event::Paste(text_to_insert) => {
if !text_to_insert.is_empty() {
if text_to_insert.is_empty() {
None
} else {
let mut ccursor = text.delete_selected(&cursor_range);
if multiline {
text.insert_text_at(&mut ccursor, text_to_insert, char_limit);
@@ -1029,8 +1038,6 @@ fn events(
}
Some(CCursorRange::one(ccursor))
} else {
None
}
}
Event::Text(text_to_insert) => {
@@ -1126,7 +1133,7 @@ fn events(
..
} => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key),
Event::Ime(ime_event) => {
Event::Ime(ime_event) if owns_ime_events => {
/// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")`
/// might be emitted from different integrations to signify that
/// the current IME composition should be cleared.
@@ -1160,46 +1167,58 @@ fn events(
}
match ime_event {
ImeEvent::Enabled => {
state.ime_enabled = true;
state.ime_cursor_range = cursor_range;
#[expect(deprecated)]
ImeEvent::Enabled | ImeEvent::Disabled => None,
// Ignore `Preedit`/`Commit` events with empty text when
// there is no active IME composition.
//
// Some integrations may emit these events when there is no
// active IME composition (e.g. when `set_ime_allowed` or
// `set_ime_cursor_area` is called on `winit`'s `Window` on
// Wayland). Without this guard, they would clear any
// selected text.
//
// TODO(umajho): Ideally this would be handled by the
// integration, but since this guard is harmless for well-
// behaved integrations and also fixes the issue described
// above, it is good enough for now.
ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text)
if composition_text.is_empty()
&& !matches!(
state.cursor_purpose,
TextEditCursorPurpose::ImeComposition
) =>
{
None
}
ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text)
if composition_text == "\n" || composition_text == "\r" =>
{
None
}
ImeEvent::Preedit(preedit_text) => {
if preedit_text == "\n" || preedit_text == "\r" {
None
state.cursor_purpose = if preedit_text.is_empty() {
TextEditCursorPurpose::Selection
} else {
let mut ccursor = clear_preedit_text(text, &cursor_range);
TextEditCursorPurpose::ImeComposition
};
let mut ccursor = clear_preedit_text(text, &cursor_range);
let start_cursor = ccursor;
if !preedit_text.is_empty() {
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
}
state.ime_cursor_range = cursor_range;
Some(CCursorRange::two(start_cursor, ccursor))
let start_cursor = ccursor;
if !preedit_text.is_empty() {
text.insert_text_at(&mut ccursor, preedit_text, char_limit);
}
Some(CCursorRange::two(start_cursor, ccursor))
}
ImeEvent::Commit(commit_text) => {
if commit_text == "\n" || commit_text == "\r" {
None
} else {
state.ime_enabled = false;
state.cursor_purpose = TextEditCursorPurpose::Selection;
let mut ccursor = clear_preedit_text(text, &cursor_range);
let mut ccursor = clear_preedit_text(text, &cursor_range);
if !commit_text.is_empty()
&& cursor_range.secondary.index
== state.ime_cursor_range.secondary.index
{
text.insert_text_at(&mut ccursor, commit_text, char_limit);
}
Some(CCursorRange::one(ccursor))
if !commit_text.is_empty() {
text.insert_text_at(&mut ccursor, commit_text, char_limit);
}
}
ImeEvent::Disabled => {
state.ime_enabled = false;
None
Some(CCursorRange::one(ccursor))
}
}
}

View File

@@ -37,18 +37,14 @@ pub struct TextEditState {
/// Controls the text selection.
pub cursor: TextCursorState,
/// The purpose of the cursor.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) cursor_purpose: TextEditCursorPurpose,
/// Wrapped in Arc for cheaper clones.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) undoer: Arc<Mutex<TextEditUndoer>>,
// If IME candidate window is shown on this text edit.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) ime_enabled: bool,
// cursor range for IME candidate.
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) ime_cursor_range: CCursorRange,
// Text offset within the widget area.
// Used for sensing and singleline text clipping.
#[cfg_attr(feature = "serde", serde(skip))]
@@ -82,3 +78,13 @@ impl TextEditState {
self.set_undoer(TextEditUndoer::default());
}
}
#[derive(Clone, Default)]
pub(crate) enum TextEditCursorPurpose {
/// The cursor is used for text selection.
#[default]
Selection,
/// The cursor is used for IME composition.
ImeComposition,
}

View File

@@ -1,9 +1,9 @@
use std::{borrow::Cow, ops::Range};
use epaint::{
Galley,
text::{TAB_SIZE, cursor::CCursor},
};
use epaint::{Galley, text::cursor::CCursor};
/// One `\t` character is this many spaces wide (for indentation purposes).
const TAB_SIZE: usize = 4;
use crate::{
text::CCursorRange,

View File

@@ -8,8 +8,8 @@ rust-version.workspace = true
publish = false
default-run = "egui_demo_app"
[package.metadata.cargo-machete]
ignored = ["profiling"]
[package.metadata.cargo-shear]
ignored = ["image", "profiling", "wasm-bindgen-futures"]
[lints]
workspace = true
@@ -23,7 +23,7 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["wgpu", "persistence"]
default = ["wgpu", "persistence", "wayland", "x11"]
web_app = ["http", "persistence"]

View File

@@ -87,13 +87,13 @@ impl egui::Plugin for AccessibilityInspectorPlugin {
ui.enable_accesskit();
Panel::right(Self::id()).show_inside(ui, |ui| {
Panel::right(Self::id()).show(ui, |ui| {
ui.heading("🔎 AccessKit Inspector");
if let Some(selected_node) = self.selected_node {
Panel::bottom(Self::id().with("details_panel"))
.frame(Frame::new())
.show_separator_line(false)
.show_inside(ui, |ui| {
.show(ui, |ui| {
self.selection_ui(ui, selected_node);
});
}

View File

@@ -24,8 +24,8 @@ impl Custom3d {
impl crate::DemoApp for Custom3d {
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
// TODO(emilk): Use `ScrollArea::inner_margin`
egui::CentralPanel::default().show_inside(ui, |ui| {
// TODO(emilk): Use `ScrollArea::content_margin`
egui::CentralPanel::default().show(ui, |ui| {
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;

View File

@@ -102,8 +102,8 @@ impl Custom3d {
impl crate::DemoApp for Custom3d {
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
// TODO(emilk): Use `ScrollArea::inner_margin`
egui::CentralPanel::default().show_inside(ui, |ui| {
// TODO(emilk): Use `ScrollArea::content_margin`
egui::CentralPanel::default().show(ui, |ui| {
egui::ScrollArea::both().auto_shrink(false).show(ui, |ui| {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;

View File

@@ -61,14 +61,14 @@ impl Default for HttpApp {
impl crate::DemoApp for HttpApp {
fn demo_ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
egui::Panel::bottom("http_bottom").show_inside(ui, |ui| {
egui::Panel::bottom("http_bottom").show(ui, |ui| {
let layout = egui::Layout::top_down(egui::Align::Center).with_main_justify(true);
ui.allocate_ui_with_layout(ui.available_size(), layout, |ui| {
ui.add(egui_demo_lib::egui_github_link_file!())
})
});
egui::CentralPanel::default().show_inside(ui, |ui| {
egui::CentralPanel::default().show(ui, |ui| {
let prev_url = self.url.clone();
let trigger_fetch = ui_url(ui, frame, &mut self.url);

View File

@@ -50,7 +50,7 @@ impl Default for ImageViewer {
impl crate::DemoApp for ImageViewer {
fn demo_ui(&mut self, ui: &mut egui::Ui, _: &mut eframe::Frame) {
egui::Panel::top("url bar").show_inside(ui, |ui| {
egui::Panel::top("url bar").show(ui, |ui| {
ui.horizontal_centered(|ui| {
let label = ui.label("URI:");
ui.text_edit_singleline(&mut self.uri_edit_text)
@@ -71,7 +71,7 @@ impl crate::DemoApp for ImageViewer {
});
});
egui::Panel::left("controls").show_inside(ui, |ui| {
egui::Panel::left("controls").show(ui, |ui| {
// uv
ui.label("UV");
ui.add(Slider::new(&mut self.image_options.uv.min.x, 0.0..=1.0).text("min x"));
@@ -197,7 +197,7 @@ impl crate::DemoApp for ImageViewer {
}
});
egui::CentralPanel::default().show_inside(ui, |ui| {
egui::CentralPanel::default().show(ui, |ui| {
egui::ScrollArea::both().show(ui, |ui| {
let mut image = egui::Image::from_uri(&self.current_uri);
image = image.uv(self.image_options.uv);

View File

@@ -310,6 +310,11 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
ui.end_row();
}
});
if let Some(mut cfg) = _frame.wgpu_surface_config() {
wgpu_surface_config_ui(ui, &mut cfg);
_frame.set_wgpu_surface_config(cfg);
}
}
#[cfg(not(target_arch = "wasm32"))]
@@ -357,6 +362,52 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
}
}
#[cfg(feature = "wgpu")]
fn wgpu_surface_config_ui(ui: &mut egui::Ui, cfg: &mut eframe::SurfaceConfig) {
use eframe::wgpu::PresentMode;
egui::Grid::new("wgpu_surface_config")
.num_columns(2)
.show(ui, |ui| {
ui.label("Present mode:");
egui::ComboBox::from_id_salt("wgpu_present_mode")
.selected_text(format!("{:?}", cfg.present_mode))
.show_ui(ui, |ui| {
for mode in [
PresentMode::AutoVsync,
PresentMode::AutoNoVsync,
PresentMode::Fifo,
PresentMode::FifoRelaxed,
PresentMode::Immediate,
PresentMode::Mailbox,
] {
ui.selectable_value(&mut cfg.present_mode, mode, format!("{mode:?}"));
}
});
ui.end_row();
ui.label("Desired max frame latency:");
egui::ComboBox::from_id_salt("wgpu_desired_max_frame_latency")
.selected_text(match cfg.desired_maximum_frame_latency {
None => "Default".to_owned(),
Some(n) => n.to_string(),
})
.show_ui(ui, |ui| {
ui.weak("Lower value = lower latency");
ui.selectable_value(&mut cfg.desired_maximum_frame_latency, None, "Default");
for n in [0_u32, 1, 2, 3] {
ui.selectable_value(
&mut cfg.desired_maximum_frame_latency,
Some(n),
n.to_string(),
);
}
ui.weak("Higher value = higher throughput/FPS");
});
ui.end_row();
});
}
// ----------------------------------------------------------------------------
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]

View File

@@ -38,7 +38,8 @@ fn main() {
});
for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] {
if !rust_log.contains(&format!("{loud_crate}=")) {
rust_log += &format!(",{loud_crate}=warn");
use std::fmt::Write as _;
write!(rust_log, ",{loud_crate}=warn").ok();
}
}

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