1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Merge branch 'main' into style_modifier

# Conflicts:
#	crates/egui/src/ui_stack.rs
This commit is contained in:
lucasmerlin
2026-05-07 17:06:28 +02:00
344 changed files with 6081 additions and 4883 deletions

View File

@@ -6,6 +6,18 @@ 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.2 - 2026-05-04
Nothing new
## 0.34.1 - 2026-03-27
Nothing new
## 0.34.0 - 2026-03-26
Nothing new
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui"
categories = ["mathematics", "encoding"]
keywords = ["gui", "color", "conversion", "gamedev", "images"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true

View File

@@ -7,6 +7,39 @@ 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.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)
## 0.34.0 - 2026-03-26
### ⭐ Added
* Add feature `wgpu_no_default_features` [#7700](https://github.com/emilk/egui/pull/7700) by [@emilk](https://github.com/emilk)
* Add `ViewportInfo::occluded` and `visible` [#7948](https://github.com/emilk/egui/pull/7948) by [@emilk](https://github.com/emilk)
* Add `eframe::WindowChromeMetrics` (macOS only) [#8015](https://github.com/emilk/egui/pull/8015) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Replace `App::update` with `fn logic` and `fn ui` [#7775](https://github.com/emilk/egui/pull/7775) by [@emilk](https://github.com/emilk)
* Make `wgpu` the default renderer for `eframe` and egui.rs [#7615](https://github.com/emilk/egui/pull/7615) by [@emilk](https://github.com/emilk)
* Roll out new egui icon and logo [#7995](https://github.com/emilk/egui/pull/7995) by [@emilk](https://github.com/emilk)
* Update wasm-bindgen to 0.2.108, and ehttp to 0.7.1 [#7996](https://github.com/emilk/egui/pull/7996) by [@emilk](https://github.com/emilk)
* Update to `wgpu` 29 [#7990](https://github.com/emilk/egui/pull/7990) by [@cwfitzgerald](https://github.com/cwfitzgerald)
* Allow fallback from smithay to arboard when getting clipboard [#7976](https://github.com/emilk/egui/pull/7976) by [@wizzeh](https://github.com/wizzeh)
### 🐛 Fixed
* Fix: update get_proc_address to use Arc for better ownership management [#7922](https://github.com/emilk/egui/pull/7922) by [@Wybxc](https://github.com/Wybxc)
* Much improved IME [#7967](https://github.com/emilk/egui/pull/7967) by [@umajho](https://github.com/umajho)
* Improve behavior of invisible windows [#7905](https://github.com/emilk/egui/pull/7905) by [@gcailly](https://github.com/gcailly)
### 🚀 Performance
* Avoid repaints on device mouse motion outside window [#7866](https://github.com/emilk/egui/pull/7866) by [@inktomi](https://github.com/inktomi)
* Only run `App::ui` if the application is visible [#7950](https://github.com/emilk/egui/pull/7950) by [@emilk](https://github.com/emilk)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/eframe"
categories = ["gui", "game-development"]
keywords = ["egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"]
[package.metadata.docs.rs]
all-features = true
@@ -89,7 +89,7 @@ web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtteranc
## See <https://github.com/emilk/egui/issues/5889> for more details.
##
## By default, eframe will prefer WebGPU over WebGL, but
## you can configure this at run-time with [`NativeOptions::wgpu_options`].
## you can configure this at run-time with `WebOptions::wgpu_options`.
wgpu = ["wgpu_no_default_features", "egui-wgpu/default"]
## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`.
@@ -155,7 +155,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe
"wgl",
] }
home = { workspace = true, optional = true }
wgpu = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
@@ -169,10 +168,17 @@ objc2-foundation = { workspace = true, default-features = false, features = [
objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
"NSBitmapImageRep",
"NSButton",
"NSControl",
"NSGraphics",
"NSImage",
"NSImageRep",
"NSMenu",
"NSMenuItem",
"NSResponder",
"NSView",
"NSWindow",
] }
# windows:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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.
@@ -161,22 +161,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 +240,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 +259,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 +282,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 +303,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 +339,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 +346,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 +394,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 +416,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 +431,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 +473,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 +521,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(),
@@ -776,7 +740,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 +769,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 +868,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
@@ -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, wgpu};
pub use {egui_wgpu, egui_wgpu::SurfaceConfig, egui_wgpu::WgpuConfiguration, egui_wgpu::wgpu};
mod epi;
@@ -190,6 +190,9 @@ pub use web::{WebLogger, WebRunner};
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
mod native;
#[cfg(target_os = "macos")]
pub use native::macos::WindowChromeMetrics;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframeWinitApplication;
@@ -254,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);
@@ -264,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)
}
}
}
@@ -443,67 +465,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

@@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS
use crate::icon_data::IconDataExt as _;
profiling::function_scope!();
use objc2::ClassType as _;
use objc2::AnyThread as _;
use objc2_app_kit::{NSApplication, NSImage};
use objc2_foundation::NSString;

View File

@@ -214,15 +214,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 +261,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 +289,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

@@ -34,7 +34,7 @@ use egui_winit::accesskit_winit;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::epi_integration::EpiIntegration,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use super::{
@@ -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,
@@ -761,9 +770,11 @@ impl GlowWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), Some(&window));
if window.is_minimized() == Some(true) {
if is_invisible_or_minimized(&window) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -950,12 +961,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

@@ -0,0 +1,76 @@
use egui::Vec2;
use objc2_app_kit::{NSView, NSWindow, NSWindowButton};
use raw_window_handle::{AppKitWindowHandle, RawWindowHandle};
/// Size of the "traffic lights" (red/yellow/green close/minimize/maximize buttons)
/// on the native macOS window.
///
/// This is very useful together with [`egui::ViewportBuilder::with_fullsize_content_view`].
#[derive(Debug)]
pub struct WindowChromeMetrics {
/// Size of the "traffic lights" (red/yellow/green close/minimize/maximize buttons),
/// including margins.
///
/// The unit here is in "native scale", which means it needs to be divided by [`egui::Context::zoom_factor`]
/// to get the size in egui points.
pub traffic_lights_size: Vec2,
}
impl WindowChromeMetrics {
/// Get the window chrome metrics for a given window handle.
pub fn from_window_handle(window_handle: &RawWindowHandle) -> Option<Self> {
window_chrome_metrics(window_handle)
}
}
fn window_chrome_metrics(window_handle: &RawWindowHandle) -> Option<WindowChromeMetrics> {
let RawWindowHandle::AppKit(appkit_handle) = window_handle else {
return None;
};
let ns_view = ns_view_from_handle(appkit_handle)?;
let ns_window = ns_view.window()?;
Some(WindowChromeMetrics {
traffic_lights_size: traffic_lights_metrics(&ns_window)?,
})
}
fn traffic_lights_metrics(ns_window: &NSWindow) -> Option<Vec2> {
// Button order is CloseButton, MiniaturizeButton, ZoomButton:
let close_button = ns_window
.standardWindowButton(NSWindowButton::CloseButton)?
.frame();
let zoom_button = ns_window
.standardWindowButton(NSWindowButton::ZoomButton)?
.frame();
let left_margin = close_button.origin.x;
let right_margin = left_margin; // for symmetry
let total_width = zoom_button.origin.x + zoom_button.size.width + right_margin;
let top_margin = close_button.origin.y;
let bottom_margin = top_margin; // Usually symmetric
let total_height = top_margin + close_button.size.height + bottom_margin;
Some(Vec2::new(total_width as f32, total_height as f32))
}
fn ns_view_from_handle(handle: &AppKitWindowHandle) -> Option<&NSView> {
let ns_view_ptr = handle.ns_view.as_ptr().cast::<NSView>();
// Validate the pointer is non-null
if ns_view_ptr.is_null() {
None
} else {
// SAFETY:
// - We've verified the pointer is non-null
// - The pointer comes from the windowing system, so it should be valid
// - NSView pointers from AppKit are expected to remain valid for the window lifetime
#[expect(unsafe_code)]
unsafe {
ns_view_ptr.as_ref()
}
}
}

View File

@@ -3,6 +3,9 @@ mod epi_integration;
mod event_loop_context;
pub mod run;
#[cfg(target_os = "macos")]
pub(crate) mod macos;
/// File storage which can be used by native backends.
#[cfg(feature = "persistence")]
pub mod file_storage;

View File

@@ -1,4 +1,4 @@
use std::time::Instant;
use std::time::{Duration, Instant};
use winit::{
application::ApplicationHandler,
@@ -11,9 +11,20 @@ use ahash::HashMap;
use super::winit_integration::{UserEvent, WinitApp};
use crate::{
Result, epi,
native::{event_loop_context, winit_integration::EventResult},
native::{
event_loop_context,
winit_integration::{EventResult, is_invisible_or_minimized},
},
};
/// Minimum interval between repaints for invisible windows.
///
/// On Windows, invisible windows don't receive `RedrawRequested` events,
/// so we throttle their repaints to avoid busy-looping while still
/// processing viewport commands like `Visible(true)`.
/// See <https://github.com/emilk/egui/issues/7776>.
const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100);
// ----------------------------------------------------------------------------
fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoop<UserEvent>> {
#[cfg(target_os = "android")]
@@ -177,23 +188,54 @@ impl<T: WinitApp> WinitAppWrapper<T> {
fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) {
let now = Instant::now();
let mut invisible_window_ids = Vec::new();
self.windows_next_repaint_times
.retain(|window_id, repaint_time| {
if now < *repaint_time {
return true; // not yet ready
}
event_loop.set_control_flow(ControlFlow::Poll);
if let Some(window) = self.winit_app.window(*window_id) {
log::trace!("request_redraw for {window_id:?}");
window.request_redraw();
// On Windows, invisible windows don't receive RedrawRequested
// events, so pending viewport commands (e.g. Visible(true)) would
// never be processed. We collect these windows to paint them
// directly below.
// See: https://github.com/emilk/egui/issues/5229
if is_invisible_or_minimized(&window) {
invisible_window_ids.push(*window_id);
} else {
log::trace!("request_redraw for {window_id:?}");
event_loop.set_control_flow(ControlFlow::Poll);
window.request_redraw();
}
} else {
log::trace!("No window found for {window_id:?}");
}
false
});
// Paint invisible windows directly, since they won't receive
// RedrawRequested events on Windows. This ensures that viewport
// commands like Visible(true) are still processed.
for window_id in &invisible_window_ids {
let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id);
self.handle_event_result(event_loop, event_result);
}
// Throttle any already-scheduled repaints for invisible windows
// to avoid busy-looping. If no repaint was requested by the app,
// the window will simply sleep.
// See: https://github.com/emilk/egui/issues/7776
if !invisible_window_ids.is_empty() {
let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL;
for window_id in &invisible_window_ids {
self.windows_next_repaint_times
.entry(*window_id)
.and_modify(|t| *t = (*t).min(next_paint));
}
}
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
@@ -270,6 +312,16 @@ impl<T: WinitApp> ApplicationHandler<UserEvent> for WinitAppWrapper<T> {
if let Some(window_id) =
self.winit_app.window_id_from_viewport_id(viewport_id)
{
// Throttle repaints for invisible windows to prevent
// high CPU usage on Windows.
// See: https://github.com/emilk/egui/issues/7776
let when = if let Some(window) = self.winit_app.window(window_id)
&& is_invisible_or_minimized(&window)
{
when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL)
} else {
when
};
Ok(EventResult::RepaintAt(window_id, when))
} else {
Ok(EventResult::Wait)
@@ -347,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;
@@ -354,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)
}
@@ -373,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)
}
@@ -383,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;
@@ -390,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)
}
@@ -409,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

@@ -27,7 +27,10 @@ use winit_integration::UserEvent;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::EventResult},
native::{
epi_integration::EpiIntegration,
winit_integration::{EventResult, is_invisible_or_minimized},
},
};
use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp};
@@ -45,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
@@ -102,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!();
@@ -118,6 +126,7 @@ impl<'app> WgpuWinitApp<'app> {
native_options,
running: None,
app_creator: Some(app_creator),
egui_ctx,
}
}
@@ -184,9 +193,17 @@ impl<'app> WgpuWinitApp<'app> {
builder: ViewportBuilder,
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
// Inject the display handle into the wgpu setup so that wgpu can create
// surfaces on platforms that require it (e.g. GLES on Wayland).
let mut wgpu_options = self.native_options.wgpu_options.clone();
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
&& create_new.display_handle.is_none()
{
create_new.display_handle = Some(Box::new(event_loop.owned_display_handle()));
}
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
wgpu_options,
self.native_options.viewport.transparent.unwrap_or(false),
egui_wgpu::RendererOptions {
msaa_samples: self.native_options.multisampling as _,
@@ -417,7 +434,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,
@@ -711,6 +731,7 @@ impl WgpuWinitRunning<'_> {
&clipped_primitives,
&textures_delta,
screenshot_commands,
window,
);
for action in viewport.actions_requested.drain(..) {
@@ -770,10 +791,12 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window
&& window.is_minimized() == Some(true)
&& is_invisible_or_minimized(window)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
@@ -1098,6 +1121,7 @@ fn render_immediate_viewport(
&clipped_primitives,
&textures_delta,
vec![],
window,
);
egui_winit.handle_platform_output(window, platform_output);

View File

@@ -9,6 +9,14 @@ use egui::ViewportId;
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
/// Returns `true` if the window is invisible or minimized.
///
/// These windows don't receive `RedrawRequested` events on Windows,
/// so they need special handling to keep processing viewport commands.
pub fn is_invisible_or_minimized(window: &Window) -> bool {
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
}
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!();

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);
}
});
@@ -393,7 +390,7 @@ impl AppRunner {
}
}
super::set_cursor_icon(cursor_icon);
super::set_cursor_icon(self.canvas(), cursor_icon);
if self.has_focus() {
// The eframe app has focus.

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

@@ -38,6 +38,7 @@ mod web_painter_wgpu;
pub use backend::*;
use egui::Theme;
use js_sys::Object;
use wasm_bindgen::prelude::*;
use web_sys::{Document, MediaQueryList, Node};
@@ -177,10 +178,8 @@ fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Contex
// ----------------------------------------------------------------------------
/// Set the cursor icon.
fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
let document = web_sys::window()?.document()?;
document
.body()?
fn set_cursor_icon(canvas: &web_sys::HtmlCanvasElement, cursor: egui::CursorIcon) -> Option<()> {
canvas
.style()
.set_property("cursor", cursor_web_name(cursor))
.ok()
@@ -370,5 +369,5 @@ pub fn percent_decode(s: &str) -> String {
/// Are we running inside the Safari browser?
pub fn is_safari_browser() -> bool {
web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari")))
web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari")))
}

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

@@ -15,13 +15,32 @@ pub(crate) struct WebPainterWgpu {
surface: wgpu::Surface<'static>,
surface_configuration: wgpu::SurfaceConfiguration,
render_state: Option<RenderState>,
on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction>,
on_surface_status: Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction>,
depth_stencil_format: Option<wgpu::TextureFormat>,
depth_texture_view: Option<wgpu::TextureView>,
screen_capture_state: Option<CaptureState>,
capture_tx: CaptureSender,
capture_rx: CaptureReceiver,
ctx: egui::Context,
needs_reconfigure: bool,
}
/// Owned web display handle that is `Send + Sync`.
///
/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum
/// contains platform variants with raw pointers. On web the handle is always empty,
/// so this wrapper is safe.
#[cfg(target_arch = "wasm32")]
#[derive(Clone, Debug)]
struct WebDisplay;
#[cfg(target_arch = "wasm32")]
impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay {
fn display_handle(
&self,
) -> Result<egui_wgpu::wgpu::rwh::DisplayHandle<'_>, egui_wgpu::wgpu::rwh::HandleError> {
Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web())
}
}
impl WebPainterWgpu {
@@ -63,7 +82,17 @@ impl WebPainterWgpu {
) -> Result<Self, String> {
log::debug!("Creating wgpu painter");
let instance = options.wgpu_options.wgpu_setup.new_instance().await;
// Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL.
let mut wgpu_options = options.wgpu_options.clone();
if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup
&& create_new.display_handle.is_none()
{
// Force WebGL, useful for quick & dirty testing:
//create_new.instance_descriptor.backends = wgpu::Backends::GL;
create_new.display_handle = Some(Box::new(WebDisplay));
}
let instance = wgpu_options.wgpu_setup.new_instance().await;
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;
@@ -71,7 +100,7 @@ impl WebPainterWgpu {
let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0);
let render_state = RenderState::create(
&options.wgpu_options,
&wgpu_options,
&instance,
Some(&surface),
egui_wgpu::RendererOptions {
@@ -89,7 +118,7 @@ impl WebPainterWgpu {
let surface_configuration = wgpu::SurfaceConfiguration {
format: render_state.target_format,
present_mode: options.wgpu_options.present_mode,
present_mode: wgpu_options.surface.present_mode,
view_formats: vec![render_state.target_format],
..default_configuration
};
@@ -105,11 +134,12 @@ impl WebPainterWgpu {
surface_configuration,
depth_stencil_format,
depth_texture_view: None,
on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _,
on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _,
screen_capture_state: None,
capture_tx,
capture_rx,
ctx,
needs_reconfigure: false,
})
}
}
@@ -195,18 +225,28 @@ impl WebPainter for WebPainterWgpu {
);
}
if self.needs_reconfigure {
self.surface
.configure(&render_state.device, &self.surface_configuration);
self.needs_reconfigure = false;
}
let output_frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(err) => match (*self.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
return Ok(());
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
self.needs_reconfigure = true;
frame
}
other => {
match (*self.on_surface_status)(&other) {
SurfaceErrorAction::RecreateSurface => {
self.surface
.configure(&render_state.device, &self.surface_configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
SurfaceErrorAction::SkipFrame => {
return Ok(());
}
},
return Ok(());
}
};
{

View File

@@ -6,6 +6,29 @@ 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.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)
## 0.34.0 - 2026-03-26
### ⭐ Added
* Add error message when calling `.render()` without `.update_buffers()` [#8005](https://github.com/emilk/egui/pull/8005) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Put the `capture` module behind a feature flag, make the `egui` dependency optional [#7698](https://github.com/emilk/egui/pull/7698) by [@StT191](https://github.com/StT191)
* Attach stencil buffer [#7702](https://github.com/emilk/egui/pull/7702) by [@jgraef](https://github.com/jgraef)
* Update wgpu to 28.0.0 [#7853](https://github.com/emilk/egui/pull/7853) by [@SuchAFuriousDeath](https://github.com/SuchAFuriousDeath)
* Update to wgpu 29 [#7990](https://github.com/emilk/egui/pull/7990) by [@cwfitzgerald](https://github.com/cwfitzgerald)
### 🐛 Fixed
* Fix wgpu memory leak leading to panic when window is minimized (#7434) [#7928](https://github.com/emilk/egui/pull/7928) by [@landaire](https://github.com/landaire)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -15,7 +15,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu"
categories = ["gui", "game-development"]
keywords = ["wgpu", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"]
[lints]
workspace = true
@@ -25,7 +25,12 @@ all-features = true
rustdoc-args = ["--generate-link-to-definition"]
[features]
default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"]
default = [
"fragile-send-sync-non-atomic-wasm",
"macos-window-resize-jitter-fix",
"wgpu/default",
"wgpu/webgl", # A very important fallback for web support
]
## Enables the `capture` module for capturing screenshots.
capture = ["dep:egui"]

View File

@@ -24,7 +24,10 @@ mod renderer;
mod setup;
pub use renderer::*;
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
pub use setup::{
EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
WgpuSetupExisting,
};
/// Helpers for capturing screenshots of the UI.
#[cfg(feature = "capture")]
@@ -61,6 +64,43 @@ 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) // Low-latency by default.
},
};
/// 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 {
@@ -85,6 +125,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(
@@ -135,29 +180,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)
@@ -191,6 +219,7 @@ impl RenderState {
let (adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew(WgpuSetupCreateNew {
instance_descriptor: _,
display_handle: _,
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
@@ -232,6 +261,8 @@ impl RenderState {
}) => (adapter, device, queue),
};
log_adapter_info(&adapter.get_info());
let surface_formats = {
profiling::scope!("get_capabilities");
compatible_surface.map_or_else(
@@ -254,6 +285,7 @@ impl RenderState {
queue,
target_format,
renderer: Arc::new(RwLock::new(renderer)),
surface_config: config.surface,
})
}
}
@@ -272,7 +304,7 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
}
}
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
/// Specifies which action should be taken as consequence of a surface error.
pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
@@ -284,23 +316,24 @@ pub enum SurfaceErrorAction {
/// 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,
/// Callback for surface errors.
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction + Send + Sync>,
/// Callback for surface status changes.
///
/// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
/// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
/// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
/// defers surface reconfiguration to the next frame — the callback is not invoked
/// in that case either.
pub on_surface_status:
Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction + Send + Sync>,
}
#[test]
@@ -312,36 +345,49 @@ 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_error: _,
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,
wgpu_setup: Default::default(),
on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::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"
} else {
log::warn!("Dropped frame with error: {err}");
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:?}");
}
}
SurfaceErrorAction::SkipFrame
}),
}
@@ -385,6 +431,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 {
@@ -406,37 +464,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

@@ -1,5 +1,3 @@
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use ahash::HashMap;
@@ -352,7 +350,10 @@ impl Renderer {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
bind_group_layouts: &[
Some(&uniform_bind_group_layout),
Some(&texture_bind_group_layout),
],
immediate_size: 0,
});
@@ -360,8 +361,8 @@ impl Renderer {
.depth_stencil_format
.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
@@ -469,6 +470,9 @@ impl Renderer {
/// The render pass internally keeps all referenced resources alive as long as necessary.
/// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error
/// instead of a compile time error.
///
/// # Panic
/// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic!
pub fn render(
&self,
render_pass: &mut wgpu::RenderPass<'static>,
@@ -513,8 +517,12 @@ impl Renderer {
// Skip rendering zero-sized clip areas.
if let Primitive::Mesh(_) = primitive {
// If this is a mesh, we need to advance the index and vertex buffer iterators:
index_buffer_slices.next().unwrap();
vertex_buffer_slices.next().unwrap();
index_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
vertex_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
}
continue;
}
@@ -524,8 +532,12 @@ impl Renderer {
match primitive {
Primitive::Mesh(mesh) => {
let index_buffer_slice = index_buffer_slices.next().unwrap();
let vertex_buffer_slice = vertex_buffer_slices.next().unwrap();
let index_buffer_slice = index_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
let vertex_buffer_slice = vertex_buffer_slices
.next()
.expect("You must call .update_buffers() before .render()");
if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) {
render_pass.set_bind_group(1, bind_group, &[]);
@@ -951,6 +963,7 @@ impl Renderer {
let index_buffer_staging = queue.write_buffer_with(
&self.index_buffer.buffer,
0,
#[expect(clippy::unwrap_used)] // Checked above
NonZeroU64::new(required_index_buffer_size).unwrap(),
);
@@ -968,7 +981,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.indices.len() * std::mem::size_of::<u32>();
let slice = index_offset..(size + index_offset);
index_buffer_staging[slice.clone()]
index_buffer_staging
.slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.indices));
self.index_buffer.slices.push(slice);
index_offset += size;
@@ -994,6 +1008,7 @@ impl Renderer {
let vertex_buffer_staging = queue.write_buffer_with(
&self.vertex_buffer.buffer,
0,
#[expect(clippy::unwrap_used)] // Checked above
NonZeroU64::new(required_vertex_buffer_size).unwrap(),
);
@@ -1011,7 +1026,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
let slice = vertex_offset..(size + vertex_offset);
vertex_buffer_staging[slice.clone()]
vertex_buffer_staging
.slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
self.vertex_buffer.slices.push(slice);
vertex_offset += size;

View File

@@ -1,5 +1,43 @@
use std::sync::Arc;
/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
///
/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable
/// `Box<dyn WgpuHasDisplayHandle>`. This trait wraps it so it can be cloned
/// alongside the rest of the egui wgpu configuration.
///
/// Automatically implemented for all types that satisfy the bounds
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
pub trait EguiDisplayHandle:
wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
{
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
/// Clone into a new `Box<dyn EguiDisplayHandle>`.
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle>;
}
impl Clone for Box<dyn EguiDisplayHandle> {
fn clone(&self) -> Self {
// We need to deref here, otherwise this causes infinite recursion stack overflow.
(**self).clone_display_handle()
}
}
impl<T> EguiDisplayHandle for T
where
T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
{
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
Box::new(self.clone())
}
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub enum WgpuSetup {
/// Construct a wgpu setup using some predefined settings & heuristics.
@@ -22,9 +60,19 @@ pub enum WgpuSetup {
Existing(WgpuSetupExisting),
}
impl Default for WgpuSetup {
fn default() -> Self {
Self::CreateNew(WgpuSetupCreateNew::default())
impl WgpuSetup {
/// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
///
/// See [`WgpuSetupCreateNew::from_display_handle`] for details.
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle))
}
/// Creates a new [`WgpuSetup::CreateNew`] without a display handle.
///
/// See [`WgpuSetupCreateNew::without_display_handle`] for details.
pub fn without_display_handle() -> Self {
Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
}
}
@@ -65,8 +113,18 @@ impl WgpuSetup {
}
log::debug!("Creating wgpu instance with backends {backends:?}");
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
.await
let desc = &create_new.instance_descriptor;
let descriptor = wgpu::InstanceDescriptor {
backends: desc.backends,
flags: desc.flags,
backend_options: desc.backend_options.clone(),
memory_budget_thresholds: desc.memory_budget_thresholds,
display: create_new
.display_handle
.as_ref()
.map(|handle| handle.clone_for_wgpu()),
};
wgpu::util::new_instance_with_webgpu_detection(descriptor).await
}
Self::Existing(existing) => existing.instance.clone(),
}
@@ -98,18 +156,35 @@ pub type NativeAdapterSelectorMethod = Arc<
/// Configuration for creating a new wgpu setup.
///
/// Used for [`WgpuSetup::CreateNew`].
///
/// Prefer [`Self::from_display_handle`] when you have a display handle available.
/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL)
/// require it, so providing one ensures maximum compatibility.
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
///
/// Note: The display handle is stored in [`Self::display_handle`] rather than in
/// [`Self::instance_descriptor`] so the config can be cloned
/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time.
pub struct WgpuSetupCreateNew {
/// Instance descriptor for creating a wgpu instance.
/// Descriptor for the wgpu instance.
///
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which
/// controls which backends are supported (wgpu will pick one of these).
/// If you only want to support WebGL (and not WebGPU),
/// you can set this to [`wgpu::Backends::GL`].
/// By default on web, WebGPU will be used if available.
/// WebGL will only be used as a fallback,
/// and only if you have enabled the `webgl` feature of crate `wgpu`.
/// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`]
/// instead (injected at instance creation time).
///
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls
/// which backends are supported (wgpu will pick one of these). For example, set it to
/// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred
/// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`).
pub instance_descriptor: wgpu::InstanceDescriptor,
/// Display handle passed to wgpu at instance creation time.
///
/// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere.
/// With winit, use [`winit::event_loop::OwnedDisplayHandle`].
///
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
pub display_handle: Option<Box<dyn EguiDisplayHandle>>,
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
pub power_preference: wgpu::PowerPreference,
@@ -128,32 +203,37 @@ pub struct WgpuSetupCreateNew {
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
impl WgpuSetupCreateNew {
/// Creates a new configuration with the given display handle.
///
/// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
/// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
/// one. Providing it unconditionally ensures your app works everywhere.
///
/// If you don't have a display handle available, use [`Self::without_display_handle`]
/// instead — it will still work on the majority of platforms.
///
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self {
instance_descriptor: self.instance_descriptor.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
display_handle: Some(Box::new(display_handle)),
..Self::without_display_handle()
}
}
}
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WgpuSetupCreateNew")
.field("instance_descriptor", &self.instance_descriptor)
.field("power_preference", &self.power_preference)
.field(
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.finish()
}
}
impl Default for WgpuSetupCreateNew {
fn default() -> Self {
/// Creates a new configuration without a display handle.
///
/// A display handle is not required for headless operation (offscreen rendering, tests,
/// compute-only workloads). It also isn't needed on most platforms even when presenting
/// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
///
/// If you do have a display handle available, prefer [`Self::from_display_handle`] for
/// maximum compatibility.
///
/// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
///
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
pub fn without_display_handle() -> Self {
Self {
instance_descriptor: wgpu::InstanceDescriptor {
// Add GL backend, primarily because WebGPU is not stable enough yet.
@@ -163,8 +243,11 @@ impl Default for WgpuSetupCreateNew {
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
display: None,
},
display_handle: None,
power_preference: wgpu::PowerPreference::from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
@@ -192,6 +275,46 @@ impl Default for WgpuSetupCreateNew {
}
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
let desc = &self.instance_descriptor;
Self {
instance_descriptor: wgpu::InstanceDescriptor {
backends: desc.backends,
flags: desc.flags,
backend_options: desc.backend_options.clone(),
memory_budget_thresholds: desc.memory_budget_thresholds,
display: None,
},
display_handle: self.display_handle.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
}
}
}
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", instance_descriptor)
.field("display_handle", display_handle)
.field("power_preference", power_preference)
.field(
"native_adapter_selector",
&native_adapter_selector.is_some(),
)
.finish_non_exhaustive()
}
}
/// Configuration for using an existing wgpu setup.
///
/// Used for [`WgpuSetup::Existing`].

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},
@@ -17,6 +17,7 @@ struct SurfaceState {
width: u32,
height: u32,
resizing: bool,
needs_reconfigure: bool,
}
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
@@ -26,7 +27,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>,
@@ -57,16 +58,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,
@@ -93,17 +94,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
@@ -112,7 +118,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;
}
@@ -200,13 +206,9 @@ impl Painter {
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?;
let render_state =
RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
.await?;
self.render_state.get_or_insert(render_state)
};
let alpha_mode = if self.support_transparent_backbuffer {
@@ -234,6 +236,7 @@ impl Painter {
height: size.height,
alpha_mode,
resizing: false,
needs_reconfigure: false,
},
);
let Some(width) = NonZeroU32::new(size.width) else {
@@ -276,7 +279,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(
@@ -368,12 +371,12 @@ impl Painter {
hal_surface
.render_layer()
.lock()
.set_presents_with_transaction(resizing);
.setPresentsWithTransaction(resizing);
Self::configure_surface(
state,
self.render_state.as_ref().unwrap(),
&self.configuration,
&self.config.surface,
);
}
}
@@ -409,6 +412,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,
@@ -417,6 +421,7 @@ impl Painter {
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
capture_data: Vec<UserData>,
window: &winit::window::Window,
) -> f32 {
profiling::function_scope!();
@@ -445,6 +450,20 @@ impl Painter {
let capture = !capture_data.is_empty();
let mut vsync_sec = 0.0;
// 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;
};
@@ -454,7 +473,7 @@ impl Painter {
commands_submitted: false,
};
let Some(surface_state) = self.surfaces.get(&viewport_id) else {
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
return vsync_sec;
};
@@ -491,6 +510,11 @@ impl Painter {
)
};
if surface_state.needs_reconfigure {
Self::configure_surface(surface_state, render_state, &self.config.surface);
surface_state.needs_reconfigure = false;
}
let output_frame = {
profiling::scope!("get_current_texture");
// This is what vsync-waiting happens on my Mac.
@@ -501,16 +525,20 @@ impl Painter {
};
let output_frame = match output_frame {
Ok(frame) => frame,
Err(err) => match (*self.configuration.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
return vsync_sec;
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
surface_state.needs_reconfigure = true;
frame
}
other => {
match (*self.config.on_surface_status)(&other) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.config.surface);
}
SurfaceErrorAction::SkipFrame => {}
}
SurfaceErrorAction::SkipFrame => {
return vsync_sec;
}
},
return vsync_sec;
}
};
let mut capture_buffer = None;
@@ -643,6 +671,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,21 @@ 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.2 - 2026-05-04
Nothing new
## 0.34.1 - 2026-03-27
Nothing new
## 0.34.0 - 2026-03-26
* Add `is_scrolling`/`is_smooth_scrolling` util, checking for active scroll action [#7669](https://github.com/emilk/egui/pull/7669) by [@IsseW](https://github.com/IsseW)
* Fix backspacing leaving last character in IME prediction not removed on macOS native and Safari [#7810](https://github.com/emilk/egui/pull/7810) by [@umajho](https://github.com/umajho)
* Much improved IME [#7967](https://github.com/emilk/egui/pull/7967) by [@umajho](https://github.com/umajho)
* Allow fallback from smithay to arboard when getting clipboard [#7976](https://github.com/emilk/egui/pull/7976) by [@wizzeh](https://github.com/wizzeh)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -11,7 +11,7 @@ readme = "README.md"
repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit"
categories = ["gui", "game-development"]
keywords = ["winit", "egui", "gui", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true
@@ -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"]
@@ -81,6 +84,7 @@ objc2.workspace = true
objc2-foundation = { workspace = true, features = ["std", "NSThread"] }
objc2-ui-kit = { workspace = true, features = [
"std",
"objc2-core-foundation",
"UIApplication",
"UIGeometry",
"UIResponder",

View File

@@ -65,13 +65,12 @@ impl Clipboard {
feature = "smithay-clipboard"
))]
if let Some(clipboard) = &mut self.smithay {
return match clipboard.load() {
Ok(text) => Some(text),
match clipboard.load() {
Ok(text) => return Some(text),
Err(err) => {
log::error!("smithay paste error: {err}");
None
}
};
}
}
#[cfg(all(

View File

@@ -9,6 +9,9 @@
#![expect(clippy::manual_range_contains)]
#[cfg(target_os = "windows")]
use std::collections::HashSet;
#[cfg(feature = "accesskit")]
pub use accesskit_winit;
pub use egui;
@@ -98,14 +101,17 @@ 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>,
allow_ime: bool,
ime_rect_px: Option<egui::Rect>,
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
/// release events that should be filtered out. See comments in that method
/// for details.
#[cfg(target_os = "windows")]
pressed_processed_physical_keys: HashSet<winit::keyboard::PhysicalKey>,
}
impl State {
@@ -126,9 +132,11 @@ 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,
@@ -141,13 +149,13 @@ impl State {
simulate_touch_screen: false,
pointer_touch_id: None,
has_sent_ime_enabled: false,
#[cfg(feature = "accesskit")]
accesskit: None,
allow_ime: false,
ime_rect_px: None,
#[cfg(target_os = "windows")]
pressed_processed_physical_keys: HashSet::new(),
};
slf.egui_input
@@ -364,25 +372,33 @@ impl State {
is_synthetic,
..
} => {
// Winit generates fake "synthetic" KeyboardInput events when the focus
// is changed to the window, or away from it. Synthetic key presses
// represent no real key presses and should be ignored.
// See https://github.com/rust-windowing/winit/issues/3543
if *is_synthetic && event.state == ElementState::Pressed {
// Winit generates fake "synthetic" KeyboardInput events when the focus
// is changed to the window, or away from it. Synthetic key presses
// represent no real key presses and should be ignored.
// See https://github.com/rust-windowing/winit/issues/3543
EventResponse {
repaint: true,
consumed: false,
}
} else {
self.on_keyboard_input(event);
let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input();
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = self.egui_ctx.egui_wants_keyboard_input()
|| event.logical_key
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
EventResponse {
repaint: true,
consumed,
if let Some(response) =
self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input)
{
response
} else {
self.on_keyboard_input(event);
// When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes.
let consumed = egui_wants_keyboard_input
|| event.logical_key
== winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab);
EventResponse {
repaint: true,
consumed,
}
}
}
}
@@ -526,6 +542,91 @@ impl State {
}
}
#[cfg(not(target_os = "windows"))]
#[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)]
#[inline(always)]
fn try_on_ime_processed_keyboard_input(
&mut self,
_event: &winit::event::KeyEvent,
_egui_wants_keyboard_input: bool,
) -> Option<EventResponse> {
// `KeyboardInput` events processed by the IME are not emitted by
// `winit` on non-Windows platforms, so we don't need to do anything
// here.
None
}
#[cfg(target_os = "windows")]
#[inline(always)]
fn try_on_ime_processed_keyboard_input(
&mut self,
event: &winit::event::KeyEvent,
egui_wants_keyboard_input: bool,
) -> Option<EventResponse> {
if !self.allow_ime {
None
} else if event.logical_key == winit::keyboard::NamedKey::Process {
// On Windows, the current version of `winit` (0.30.12) has a bug
// where `KeyboardInput` events processed by the IME are still
// emitted. [^1]
//
// As a workaround, we detect these events by checking whether their
// `logical_key` is `winit::keyboard::NamedKey::Process`, and filter
// them out to keep behavior consistent with other platforms.
//
// `winit::keyboard::NamedKey::Process` is not documented in
// `winit`. Reading through its source code, we find that it is
// mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated
// note, Web is the only other platform that also uses it [^3].)
// According to Microsoft, “the IME sets the virtual key value
// to `VK_PROCESSKEY` after processing a key input message” [^4].
// See also [^5].
// (I can't find a documentation page dedicated to this value.)
//
// TODO(umajho): Remove this workaround once the `winit` bug is fixed
// and we've updated to a version that includes the fix. NOTE: Don't
// forget to also remove the `pressed_processed_physical_keys` field
// and its related code.
//
// [^1]: https://github.com/rust-windowing/winit/issues/4508
// [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946
// [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
// [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks
// [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages
self.pressed_processed_physical_keys
.insert(event.physical_key);
Some(EventResponse {
repaint: false,
consumed: egui_wants_keyboard_input,
})
} else if event.state == ElementState::Released
&& self
.pressed_processed_physical_keys
.remove(&event.physical_key)
{
// Unlike key-presses, we can not tell whether a key-release event
// is processed by the IME or not by looking at its `logical_key`,
// because their `logical_key` is the original value (e.g.
// `winit::keyboard::Key::Character(…)`) rather than
// `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`.
// (See the screencast for Windows in [^1].)
// So we track the physical keys of processed key-presses and
// filter out the corresponding key-releases.
//
// [^1]: https://github.com/rust-windowing/winit/issues/4508
Some(EventResponse {
repaint: false,
consumed: egui_wants_keyboard_input,
})
} else {
None
}
}
/// ## NOTE
///
/// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit.
@@ -585,17 +686,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())));
@@ -604,53 +699,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 {
@@ -999,13 +1051,32 @@ impl State {
self.set_cursor_icon(window, cursor_icon);
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 {
// Defensively clear the set to avoid unexpected behavior.
//
// We don't do the same in `ime_event_disable` because the key
// release events for IME confirmation keys arrive after
// `winit::event::Ime::Disabled`.
self.pressed_processed_physical_keys.clear();
}
profiling::scope!("set_ime_allowed");
window.set_ime_allowed(allow_ime);
}
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)

View File

@@ -36,8 +36,8 @@ mod ios {
| UISceneActivationState::ForegroundInactive
)
{
// Safe to cast, the class kind was checked above
let window_scene = Retained::cast::<UIWindowScene>(scene.clone());
// SAFETY: class kind was checked above with `isKindOfClass`
let window_scene = Retained::cast_unchecked::<UIWindowScene>(scene.clone());
if let Some(window) = window_scene.keyWindow() {
let insets = window.safeAreaInsets();
return SafeAreaInsets(MarginF32 {

View File

@@ -11,7 +11,7 @@ readme = "../../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[lints]
workspace = true

View File

@@ -4,7 +4,21 @@ use epaint::text::TextWrapMode;
/// A low-level ui building block.
///
/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
/// This can be a piece of text, an image, or even a custom widget.
/// It can be decorated with various layout hints, such as `grow`, `shrink`, `align`, and more.
///
/// `Atom` implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience.
///
/// Many widgets take an `impl` [`crate::IntoAtoms`] parameter,
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
/// ```
/// # use egui::{Vec2, AtomExt, AtomKind, Atom, Image, Id};
/// # egui::__run_test_ui(|ui| {
/// let image = egui::include_image!("../../../eframe/data/icon.png");
/// ui.button((image, "Click me!"));
/// # });
/// ```
///
/// You can directly call the `atom_*` methods on anything that implements `Into<Atom>`.
/// ```
/// # use egui::{Image, emath::Vec2};

View File

@@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> {
// If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`.
// If none is found, mark the first text item as `shrink`.
if wrap_mode != TextWrapMode::Extend {
let any_shrink = atoms.iter().any(|a| a.shrink);
let any_shrink = atoms.any_shrink();
if !any_shrink {
let first_text = atoms
.iter_mut()
@@ -318,8 +318,9 @@ impl<'a> AtomLayout<'a> {
let (_, rect) = ui.allocate_space(frame_size);
let mut response = ui.interact(rect, id, sense);
response.intrinsic_size =
Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size));
response.set_intrinsic_size(
(Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size),
);
AllocatedAtomLayout {
sized_atoms: sized_items,
@@ -520,6 +521,20 @@ impl AtomLayoutResponse {
}
}
impl Deref for AtomLayoutResponse {
type Target = Response;
fn deref(&self) -> &Self::Target {
&self.response
}
}
impl DerefMut for AtomLayoutResponse {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.response
}
}
impl Widget for AtomLayout<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response

View File

@@ -8,6 +8,15 @@ use std::ops::{Deref, DerefMut};
pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2;
/// A list of [`Atom`]s.
///
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
/// # egui::__run_test_ui(|ui| {
/// let image = egui::include_image!("../../../eframe/data/icon.png");
/// ui.button((image, "Click me!"));
/// # });
#[derive(Clone, Debug, Default)]
pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>);
@@ -69,6 +78,11 @@ impl<'a> Atoms<'a> {
string
}
/// Do any of the atoms have shrink set to `true`?
pub fn any_shrink(&self) -> bool {
self.iter().any(|a| a.shrink)
}
pub fn iter_kinds(&self) -> impl Iterator<Item = &AtomKind<'a>> {
self.0.iter().map(|atom| &atom.kind)
}
@@ -187,6 +201,16 @@ where
}
/// Trait for turning a tuple of [`Atom`]s into [`Atoms`].
///
/// Many widgets take an `impl` [`IntoAtoms`] parameter,
/// which allows you to easily create atoms from tuples of text, images, and other atoms:
/// ```
/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2};
/// # egui::__run_test_ui(|ui| {
/// let image = egui::include_image!("../../../eframe/data/icon.png");
/// ui.button((image, "Click me!"));
/// # });
/// ```
pub trait IntoAtoms<'a> {
fn collect(self, atoms: &mut Atoms<'a>);

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;
@@ -516,6 +516,7 @@ impl Area {
let move_response = ctx.create_widget(
WidgetRect {
id: interact_id,
parent_id: id,
layer_id,
rect: state.rect(),
interact_rect: state.rect().intersect(constrain_rect),

View File

@@ -451,15 +451,6 @@ impl CollapsingHeader {
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);
self
}
/// If you set this to `false`, the [`CollapsingHeader`] will be grayed out and un-clickable.
///
/// This is a convenience for [`Ui::disable`].

View File

@@ -94,12 +94,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()
@@ -283,16 +278,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.

View File

@@ -10,7 +10,7 @@
use crate::style::StyleModifier;
use crate::{
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup,
Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup,
PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _,
};
use emath::{Align, RectAlign, Vec2, vec2};
@@ -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 {
@@ -458,6 +455,7 @@ impl SubMenu {
let is_any_open = open_item.is_some();
let mut is_open = open_item == Some(id);
let was_open = is_open;
let mut set_open = None;
// We expand the button rect so there is no empty space where no menu is shown
@@ -470,9 +468,21 @@ impl SubMenu {
// But since we check if no other menu is open, nothing should be able to cover the button
let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos));
// `clicked` includes keyboard and accessibility click actions.
// We want Enter/Space to toggle an already open submenu, while pointer clicks should keep
// the submenu open (for touch and pointer interactions).
let clicked = button_response.clicked();
let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary);
let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer;
if ui.is_enabled() && is_open && clicked_by_keyboard_or_access {
set_open = Some(false);
is_open = false;
}
// The clicked handler is there for accessibility (keyboard navigation)
let should_open =
ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open));
ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open));
if should_open {
set_open = Some(true);
is_open = true;

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,

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

View File

@@ -18,9 +18,8 @@
use emath::{GuiRounding as _, Pos2};
use crate::{
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef,
Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp,
vec2,
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect, Sense,
Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
@@ -451,59 +450,6 @@ impl Panel {
}
}
// Deprecated
impl Panel {
#[deprecated = "Renamed default_size"]
pub fn default_width(self, default_size: f32) -> Self {
self.default_size(default_size)
}
#[deprecated = "Renamed min_size"]
pub fn min_width(self, min_size: f32) -> Self {
self.min_size(min_size)
}
#[deprecated = "Renamed max_size"]
pub fn max_width(self, max_size: f32) -> Self {
self.max_size(max_size)
}
#[deprecated = "Renamed size_range"]
pub fn width_range(self, size_range: impl Into<Rangef>) -> Self {
self.size_range(size_range)
}
#[deprecated = "Renamed exact_size"]
pub fn exact_width(self, size: f32) -> Self {
self.exact_size(size)
}
#[deprecated = "Renamed default_size"]
pub fn default_height(self, default_size: f32) -> Self {
self.default_size(default_size)
}
#[deprecated = "Renamed min_size"]
pub fn min_height(self, min_size: f32) -> Self {
self.min_size(min_size)
}
#[deprecated = "Renamed max_size"]
pub fn max_height(self, max_size: f32) -> Self {
self.max_size(max_size)
}
#[deprecated = "Renamed size_range"]
pub fn height_range(self, size_range: impl Into<Rangef>) -> Self {
self.size_range(size_range)
}
#[deprecated = "Renamed exact_size"]
pub fn exact_height(self, size: f32) -> Self {
self.exact_size(size)
}
}
// Public showing methods
impl Panel {
/// Show the panel inside a [`Ui`].
@@ -515,41 +461,6 @@ impl Panel {
self.show_inside_dyn(ui, Box::new(add_contents))
}
/// Show the panel at the top level.
#[deprecated = "Use show_inside() instead"]
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel if `is_expanded` is `true`,
/// otherwise don't show it, but with a nice animation between collapsed and expanded.
#[deprecated = "Use show_animated_inside() instead"]
pub fn show_animated<R>(
self,
ctx: &Context,
is_expanded: bool,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
#![expect(deprecated)]
let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded);
let animated_panel = self.get_animated_panel(ctx, is_expanded)?;
if how_expanded < 1.0 {
// Show a fake panel in this in-between animation state:
animated_panel.show(ctx, |_ui| {});
None
} else {
// Show the real panel:
Some(animated_panel.show(ctx, add_contents))
}
}
/// Show the panel if `is_expanded` is `true`,
/// otherwise don't show it, but with a nice animation between collapsed and expanded.
pub fn show_animated_inside<R>(
@@ -561,7 +472,11 @@ impl Panel {
let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded);
// Get either the fake or the real panel to animate
let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?;
let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else {
// Make sure the ids of the next widgets are the same whether we show the panel or not:
ui.skip_ahead_auto_ids(1);
return None;
};
if how_expanded < 1.0 {
// Show a fake panel in this in-between animation state:
@@ -573,34 +488,6 @@ impl Panel {
}
}
/// Show either a collapsed or a expanded panel, with a nice animation between.
#[deprecated = "Use show_animated_between_inside() instead"]
pub fn show_animated_between<R>(
ctx: &Context,
is_expanded: bool,
collapsed_panel: Self,
expanded_panel: Self,
add_contents: impl FnOnce(&mut Ui, f32) -> R,
) -> Option<InnerResponse<R>> {
#![expect(deprecated)]
let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded);
// Get either the fake or the real panel to animate
let animated_between_panel =
Self::get_animated_between_panel(ctx, is_expanded, collapsed_panel, expanded_panel);
if 0.0 == how_expanded {
Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
} else if how_expanded < 1.0 {
// Show animation:
animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded));
None
} else {
Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded)))
}
}
/// Show either a collapsed or a expanded panel, with a nice animation between.
pub fn show_animated_between_inside<R>(
ui: &mut Ui,
@@ -752,59 +639,6 @@ impl Panel {
inner_response
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
#![expect(deprecated)]
let side = self.side;
let available_rect = ctx.available_rect();
let mut panel_ui = Ui::new(
ctx.clone(),
self.id,
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(available_rect),
);
panel_ui.set_clip_rect(ctx.content_rect());
panel_ui
.response()
.widget_info(|| WidgetInfo::new(WidgetType::Panel));
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
let rect = inner_response.response.rect;
match side {
PanelSide::Vertical(side) => match side {
VerticalSide::Left => ctx.pass_state_mut(|state| {
state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max));
}),
VerticalSide::Right => ctx.pass_state_mut(|state| {
state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max));
}),
},
PanelSide::Horizontal(side) => match side {
HorizontalSide::Top => {
ctx.pass_state_mut(|state| {
state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max));
});
}
HorizontalSide::Bottom => {
ctx.pass_state_mut(|state| {
state.allocate_bottom_panel(Rect::from_min_max(
rect.min,
available_rect.max,
));
});
}
},
}
inner_response
}
fn prepare_resizable_panel(&self, panel_sizer: &mut PanelSizer<'_>, ui: &Ui) {
let resize_id = self.id.with("__resize");
let resize_response = ui.ctx().read_response(resize_id);
@@ -1040,61 +874,9 @@ impl CentralPanel {
response
}
/// Show the panel at the top level.
#[deprecated = "Use show_inside() instead"]
pub fn show<R>(
self,
ctx: &Context,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<R> {
self.show_dyn(ctx, Box::new(add_contents))
}
/// Show the panel at the top level.
fn show_dyn<'c, R>(
self,
ctx: &Context,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> {
#![expect(deprecated)]
let id = Id::new((ctx.viewport_id(), "central_panel"));
let mut panel_ui = Ui::new(
ctx.clone(),
id,
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(ctx.available_rect()),
);
panel_ui.set_clip_rect(ctx.content_rect());
if false {
// TODO(emilk): @lucasmerlin shouldn't we enable this?
panel_ui
.response()
.widget_info(|| WidgetInfo::new(WidgetType::Panel));
}
let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents);
// Only inform ctx about what we actually used, so we can shrink the native window to fit.
ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect));
inner_response
}
}
fn clamp_to_range(x: f32, range: Rangef) -> f32 {
let range = range.as_positive();
x.clamp(range.min, range.max)
}
// ----------------------------------------------------------------------------
#[deprecated = "Use Panel::left or Panel::right instead"]
pub type SidePanel = super::Panel;
#[deprecated = "Use Panel::top or Panel::bottom instead"]
pub type TopBottomPanel = super::Panel;

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,
}
@@ -666,10 +664,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

@@ -69,13 +69,6 @@ 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 {

View File

@@ -5,7 +5,7 @@
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use emath::GuiRounding as _;
use epaint::Margin;
use epaint::{Color32, Direction, Margin, Shape};
use crate::{
Context, CursorIcon, Id, NumExt as _, Pos2, Rangef, Rect, Response, Sense, Ui, UiBuilder,
@@ -423,13 +423,6 @@ 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 {
@@ -530,32 +523,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 {
@@ -1019,13 +986,17 @@ impl ScrollArea {
.inner;
let (content_size, state) = prepared.end(ui);
ScrollAreaOutput {
let output = ScrollAreaOutput {
inner,
id,
state,
content_size,
inner_rect,
}
};
paint_fade_areas(ui, &output);
output
}
}
@@ -1504,3 +1475,88 @@ impl Prepared {
(content_size, state)
}
}
/// Paint fade-out gradients at the top and/or bottom of a scroll area to
/// indicate that more content is available beyond the visible region.
fn paint_fade_areas<R>(ui: &Ui, scroll_output: &ScrollAreaOutput<R>) {
let crate::style::ScrollFadeStyle {
strength,
size: fade_size,
} = ui.spacing().scroll.fade;
if strength <= 0.0 {
return;
}
let bg = ui.stack().bg_color();
let offset = scroll_output.state.offset;
let overflow = scroll_output.content_size - scroll_output.inner_rect.size();
let paint_rect = scroll_output
.inner_rect
.intersect(ui.min_rect())
.expand(ui.visuals().clip_rect_margin);
// Top fade: animate opacity based on how far we've scrolled down.
if 0.0 < offset.y {
let t = (offset.y / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
paint_rect.left_top(),
pos2(paint_rect.right(), paint_rect.top() + fade_size),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::TopDown,
[bg_faded, Color32::TRANSPARENT],
));
}
// Bottom fade: animate opacity based on distance from the bottom.
let distance_from_bottom = overflow.y - offset.y;
if 0.0 < distance_from_bottom {
let t = (distance_from_bottom / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
pos2(paint_rect.left(), paint_rect.bottom() - fade_size),
paint_rect.right_bottom(),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::BottomUp,
[bg_faded, Color32::TRANSPARENT],
));
}
// Left fade: animate opacity based on how far we've scrolled right.
if 0.0 < offset.x {
let t = (offset.x / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
paint_rect.left_top(),
pos2(paint_rect.left() + fade_size, paint_rect.bottom()),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::LeftToRight,
[bg_faded, Color32::TRANSPARENT],
));
}
// Right fade: animate opacity based on distance from the right edge.
let distance_from_right = overflow.x - offset.x;
if 0.0 < distance_from_right {
let t = (distance_from_right / fade_size).clamp(0.0, 1.0) * strength;
let bg_faded = bg.gamma_multiply(t);
let rect = Rect::from_min_max(
pos2(paint_rect.right() - fade_size, paint_rect.top()),
paint_rect.right_bottom(),
);
ui.painter().add(Shape::gradient_rect(
rect,
Direction::RightToLeft,
[bg_faded, Color32::TRANSPARENT],
));
}
}

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

@@ -262,7 +262,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 +275,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);
@@ -427,7 +427,7 @@ impl<'open> Window<'open> {
/// Enable/disable scrolling on the window by dragging with the pointer. `true` by default.
///
/// See [`ScrollArea::drag_to_scroll`] for more.
/// See [`ScrollArea::scroll_source`] for more.
#[inline]
pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self {
self.scroll = self.scroll.scroll_source(ScrollSource {
@@ -673,7 +673,7 @@ impl Window<'_> {
title_bar.ui(
&mut area_content_ui,
&content_response,
content_response.as_ref(),
open.as_deref_mut(),
&mut collapsing,
collapsible,
@@ -962,6 +962,7 @@ fn do_resize_interaction(
WidgetRect {
layer_id,
id,
parent_id: layer_id.id,
rect,
interact_rect: rect,
sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable
@@ -1255,7 +1256,7 @@ impl TitleBar {
fn ui(
self,
ui: &mut Ui,
content_response: &Option<Response>,
content_response: Option<&Response>,
open: Option<&mut bool>,
collapsing: &mut CollapsingState,
collapsible: bool,
@@ -1299,7 +1300,7 @@ impl TitleBar {
ui.visuals().text_color(),
);
if let Some(content_response) = &content_response {
if let Some(content_response) = content_response {
// Paint separator between title and content:
let content_rect = content_response.rect;
if false {

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();
@@ -697,8 +697,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_inside(ui, |ui| {
/// ui.label("Hello world!");
/// if ui.button("Click me").clicked() {
/// // take some action here
@@ -780,9 +780,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 +788,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 +879,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 +892,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 +905,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 +1005,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 +1013,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 +1029,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 +1046,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 {
@@ -1360,6 +1317,7 @@ impl Context {
let WidgetRect {
id,
parent_id: _,
layer_id,
rect,
interact_rect,
@@ -1378,8 +1336,8 @@ impl Context {
interact_rect,
sense,
flags: Flags::empty(),
interact_pointer_pos: None,
intrinsic_size: None,
interact_pointer_pos_or_nan: Pos2::NAN,
intrinsic_size_or_nan: Vec2::NAN,
};
res.flags.set(Flags::ENABLED, enabled);
@@ -1470,14 +1428,11 @@ impl Context {
|| res.long_touched()
|| clicked
|| res.drag_stopped();
if is_interacted_with {
res.interact_pointer_pos = input.pointer.interact_pos();
if let (Some(to_global), Some(pos)) = (
memory.to_global.get(&res.layer_id),
&mut res.interact_pointer_pos,
) {
*pos = to_global.inverse() * *pos;
if is_interacted_with && let Some(mut pos) = input.pointer.interact_pos() {
if let Some(to_global) = memory.to_global.get(&res.layer_id) {
pos = to_global.inverse() * pos;
}
res.interact_pointer_pos_or_nan = pos;
}
if input.pointer.any_down() && !is_interacted_with {
@@ -1547,6 +1502,11 @@ impl Context {
crate::debug_text::print(self, text);
}
/// Current time in seconds, relative to some unknown epoch.
pub fn time(&self) -> f64 {
self.input(|i| i.time)
}
/// What operating system are we running on?
///
/// When compiling natively, this is
@@ -1662,7 +1622,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 {
@@ -1671,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_for`].
pub fn cumulative_frame_nr_for(&self, id: ViewportId) -> u64 {
@@ -1691,7 +1651,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`].
@@ -1701,7 +1661,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
@@ -1935,7 +1895,7 @@ impl Context {
}
}
/// Callbacks
/// Plugins
impl Context {
/// Call the given callback at the start of each pass of each viewport.
///
@@ -2076,7 +2036,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:
@@ -2093,12 +2053,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.
///
@@ -2113,21 +2067,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.
@@ -2139,18 +2078,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:
@@ -2397,6 +2324,12 @@ impl Context {
crate::gui_zoom::zoom_with_keyboard(self);
}
for shortcut in self.options(|o| o.quit_shortcuts.clone()) {
if self.input_mut(|i| i.consume_shortcut(&shortcut)) {
self.send_viewport_cmd(ViewportCommand::Close);
}
}
#[cfg(debug_assertions)]
self.debug_painting();
@@ -2408,17 +2341,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;
@@ -2491,13 +2418,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);
}
@@ -2562,7 +2493,7 @@ impl Context {
);
self.viewport(|vp| {
for reason in &vp.output.request_discard_reasons {
warning += &format!("\n {reason}");
write!(warning, "\n {reason}").ok();
}
});
@@ -2597,6 +2528,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();
@@ -2636,6 +2573,19 @@ impl ContextImpl {
}
}
#[cfg(debug_assertions)]
let shapes = if self.memory.options.style().debug.warn_if_rect_changes_id {
let mut shapes = shapes;
warn_if_rect_changes_id(
&mut shapes,
&viewport.prev_pass.widgets,
&viewport.this_pass.widgets,
);
shapes
} else {
shapes
};
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
if repaint_needed {
@@ -2815,24 +2765,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();
}
@@ -2840,46 +2780,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.
@@ -2891,17 +2818,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.
@@ -2909,23 +2825,18 @@ 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()) {
crate::text_edit::TextEditState::load(self, id).is_some()
} else {
false
}
}
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
@@ -2938,18 +2849,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).
@@ -2960,18 +2859,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
@@ -3586,17 +3473,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",
@@ -3949,8 +3825,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,
) {
@@ -4237,6 +4113,112 @@ fn context_impl_send_sync() {
assert_send_sync::<Context>();
}
/// Check if any [`Rect`] appears with different [`Id`]s between two passes.
///
/// This helps detect cases where the same screen area is claimed by different widget ids
/// across passes, which is often a sign of id instability.
#[cfg(debug_assertions)]
fn warn_if_rect_changes_id(
out_shapes: &mut Vec<ClippedShape>,
prev_widgets: &crate::WidgetRects,
new_widgets: &crate::WidgetRects,
) {
profiling::function_scope!();
use std::collections::BTreeMap;
/// A wrapper around [`Rect`] that implements [`Ord`] using the bit representation of its floats.
#[derive(Clone, Copy, PartialEq, Eq)]
struct OrderedRect(Rect);
impl PartialOrd for OrderedRect {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedRect {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let lhs = self.0;
let rhs = other.0;
lhs.min
.x
.to_bits()
.cmp(&rhs.min.x.to_bits())
.then(lhs.min.y.to_bits().cmp(&rhs.min.y.to_bits()))
.then(lhs.max.x.to_bits().cmp(&rhs.max.x.to_bits()))
.then(lhs.max.y.to_bits().cmp(&rhs.max.y.to_bits()))
}
}
fn create_lookup<'a>(
widgets: impl Iterator<Item = &'a WidgetRect>,
) -> BTreeMap<OrderedRect, Vec<&'a WidgetRect>> {
let mut lookup: BTreeMap<OrderedRect, Vec<&'a WidgetRect>> = BTreeMap::default();
for w in widgets {
lookup.entry(OrderedRect(w.rect)).or_default().push(w);
}
lookup
}
for (layer_id, new_layer_widgets) in new_widgets.layers() {
let prev = create_lookup(prev_widgets.get_layer(*layer_id));
let new = create_lookup(new_layer_widgets.iter());
for (hashable_rect, new_at_rect) in new {
let Some(prev_at_rect) = prev.get(&hashable_rect) else {
continue; // this rect did not exist in the previous pass
};
if prev_at_rect
.iter()
.any(|w| new_at_rect.iter().any(|nw| nw.id == w.id))
{
continue; // at least one id stayed the same, so this is not an id change
}
// Only warn if at least one of the previous ids is gone from this layer entirely.
// If they all still exist (just at a different rect), then the rect match
// is just a coincidence caused by widgets shifting (e.g. a window being dragged).
if prev_at_rect.iter().all(|w| new_widgets.contains(w.id)) {
continue;
}
// Only warn if at least one widget has the same parent_id in both frames.
// If all parent_ids changed too, this is a cascading id shift, not a widget bug.
if !prev_at_rect
.iter()
.any(|pw| new_at_rect.iter().any(|nw| nw.parent_id == pw.parent_id))
{
continue;
}
let rect = new_at_rect[0].rect;
log::warn!(
"Widget rect {rect:?} changed id between passes: prev ids: {:?}, new ids: {:?}",
prev_at_rect
.iter()
.map(|w| w.id.short_debug_format())
.collect::<Vec<_>>(),
new_at_rect
.iter()
.map(|w| w.id.short_debug_format())
.collect::<Vec<_>>(),
);
out_shapes.push(ClippedShape {
clip_rect: Rect::EVERYTHING,
shape: epaint::Shape::rect_stroke(
rect,
0,
(2.0, Color32::RED),
StrokeKind::Outside,
),
});
}
}
}
#[cfg(test)]
mod test {
use super::Context;

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:?}"))
}
@@ -441,6 +443,10 @@ pub enum Event {
Text(String),
/// A key was pressed or released.
///
/// ## Note for integration authors
///
/// Key events that has been processed by IMEs should not be sent to `egui`.
Key {
/// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak
/// keyboard layout, it will be taken into account.
@@ -599,15 +605,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.
@@ -123,6 +126,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.

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`].
///
@@ -450,6 +450,7 @@ mod tests {
fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect {
WidgetRect {
id,
parent_id: Id::NULL,
layer_id: LayerId::background(),
rect,
interact_rect: rect,

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

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

@@ -1,7 +1,7 @@
use emath::GuiRounding as _;
use crate::{
Align,
Align, Direction,
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
};
const INFINITY: f32 = f32::INFINITY;
@@ -87,36 +87,6 @@ impl Region {
// ----------------------------------------------------------------------------
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Direction {
LeftToRight,
RightToLeft,
TopDown,
BottomUp,
}
impl Direction {
#[inline(always)]
pub fn is_horizontal(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => true,
Self::TopDown | Self::BottomUp => false,
}
}
#[inline(always)]
pub fn is_vertical(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => false,
Self::TopDown | Self::BottomUp => true,
}
}
}
// ----------------------------------------------------------------------------
/// The layout of a [`Ui`][`crate::Ui`], e.g. "vertical & centered".
///
/// ```
@@ -623,12 +593,24 @@ impl Layout {
if (self.is_vertical() && self.horizontal_align() == Align::Center)
|| self.horizontal_justify()
{
frame_size.x = frame_size.x.max(available_rect.width()); // fill full width
// For wrapping layouts, fill the current column width, not the entire layout width.
let width = if self.main_wrap {
region.cursor.width()
} else {
available_rect.width()
};
frame_size.x = frame_size.x.max(width); // fill full width
}
if (self.is_horizontal() && self.vertical_align() == Align::Center)
|| self.vertical_justify()
{
frame_size.y = frame_size.y.max(available_rect.height()); // fill full height
// For wrapping layouts, fill the current row height, not the entire layout height.
let height = if self.main_wrap {
region.cursor.height()
} else {
available_rect.height()
};
frame_size.y = frame_size.y.max(height); // fill full height
}
let align2 = match self.main_dir {
@@ -791,14 +773,14 @@ impl Layout {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(region.max_rect.left(), new_top),
pos2(INFINITY, new_top + region.cursor.height()),
pos2(INFINITY, new_top),
);
}
Direction::RightToLeft => {
let new_top = region.cursor.bottom() + spacing.y;
region.cursor = Rect::from_min_max(
pos2(-INFINITY, new_top),
pos2(region.max_rect.right(), new_top + region.cursor.height()),
pos2(region.max_rect.right(), new_top),
);
}
Direction::TopDown | Direction::BottomUp => {}

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_inside(ui, |ui| {
//! ui.label("Hello world!");
//! if ui.button("Click me").clicked() {
//! // take some action here
@@ -407,8 +407,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 +432,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;
@@ -449,7 +444,7 @@ pub use emath::{
remap_clamp, vec2,
};
pub use epaint::{
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
ClippedPrimitive, ColorImage, CornerRadius, Direction, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
@@ -458,8 +453,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,
};
}
@@ -478,7 +473,7 @@ pub use self::{
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap},
id::{Id, IdMap, IdSet},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState, SurrenderFocusOn},
layers::{LayerId, Order},
layout::*,
@@ -499,9 +494,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

@@ -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`.
@@ -234,6 +240,16 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))]
pub zoom_with_keyboard: bool,
/// Keyboard shortcuts to close the application.
///
/// Pressing any of these will send [`crate::ViewportCommand::Close`]
/// to the root viewport.
///
/// Defaults to `Cmd-Q` (which is Ctrl-Q on Linux/Windows, Cmd-Q on Mac).
/// Set to empty to disable.
#[cfg_attr(feature = "serde", serde(skip))]
pub quit_shortcuts: Vec<crate::KeyboardShortcut>,
/// Controls the tessellator.
pub tessellation_options: epaint::TessellationOptions,
@@ -256,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,
@@ -304,6 +320,10 @@ impl Default for Options {
system_theme: None,
zoom_factor: 1.0,
zoom_with_keyboard: true,
quit_shortcuts: vec![crate::KeyboardShortcut::new(
crate::Modifiers::COMMAND,
crate::Key::Q,
)],
tessellation_options: Default::default(),
repaint_on_widget_change: false,
@@ -363,6 +383,7 @@ impl Options {
system_theme: _,
zoom_factor,
zoom_with_keyboard,
quit_shortcuts: _, // not shown in ui
tessellation_options,
repaint_on_widget_change,
max_passes,
@@ -746,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);
@@ -797,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()
@@ -860,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.
@@ -978,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.
@@ -1004,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
{
@@ -1045,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 {

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

@@ -55,7 +55,7 @@ pub struct Response {
/// Where the pointer (mouse/touch) were when this widget was clicked or dragged.
/// `None` if the widget is not being interacted with.
#[doc(hidden)]
pub interact_pointer_pos: Option<Pos2>,
pub interact_pointer_pos_or_nan: Pos2,
/// The intrinsic / desired size of the widget.
///
@@ -67,12 +67,22 @@ pub struct Response {
/// At the time of writing, this is only used by external crates
/// for improved layouting.
/// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex).
pub intrinsic_size: Option<Vec2>,
#[doc(hidden)]
pub intrinsic_size_or_nan: Vec2,
#[doc(hidden)]
pub flags: Flags,
}
#[test]
fn test_response_size() {
assert_eq!(
std::mem::size_of::<Response>(),
88,
"Keep Response small, because we create them often, and we want to keep it lean and fast"
);
}
/// A bit set for various boolean properties of `Response`.
#[doc(hidden)]
#[derive(Copy, Clone, Debug)]
@@ -141,6 +151,22 @@ bitflags::bitflags! {
}
impl Response {
/// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
///
/// Looks up the [`WidgetRect`] from the current (or previous) pass.
pub fn parent_id(&self) -> Id {
let id = self.ctx.viewport(|viewport| {
viewport
.this_pass
.widgets
.get(self.id)
.or_else(|| viewport.prev_pass.widgets.get(self.id))
.map(|w| w.parent_id)
});
debug_assert!(id.is_some(), "WidgetRect for Response not found!");
id.unwrap_or(Id::NULL)
}
/// Returns true if this widget was clicked this frame by the primary button.
///
/// A click is registered when the mouse or touch is released within
@@ -246,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
@@ -489,7 +515,26 @@ impl Response {
/// `None` if the widget is not being interacted with.
#[inline]
pub fn interact_pointer_pos(&self) -> Option<Pos2> {
self.interact_pointer_pos
let pos = self.interact_pointer_pos_or_nan;
if pos.any_nan() { None } else { Some(pos) }
}
/// The intrinsic / desired size of the widget.
///
/// This is the size that a non-wrapped, non-truncated, non-justified version of the widget
/// would have.
///
/// If this is `None`, use [`Self::rect`] instead.
#[inline]
pub fn intrinsic_size(&self) -> Option<Vec2> {
let size = self.intrinsic_size_or_nan;
if size.any_nan() { None } else { Some(size) }
}
/// Set the intrinsic / desired size of the widget.
#[inline]
pub fn set_intrinsic_size(&mut self, size: Vec2) {
self.intrinsic_size_or_nan = size;
}
/// If it is a good idea to show a tooltip, where is pointer?
@@ -732,6 +777,7 @@ impl Response {
WidgetRect {
layer_id: self.layer_id,
id: self.id,
parent_id: self.parent_id(),
rect: self.rect,
interact_rect: self.interact_rect,
sense: self.sense | sense,
@@ -1007,8 +1053,10 @@ impl Response {
interact_rect: self.interact_rect.union(other.interact_rect),
sense: self.sense.union(other.sense),
flags: self.flags | other.flags,
interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos),
intrinsic_size: None,
interact_pointer_pos_or_nan: self
.interact_pointer_pos()
.unwrap_or(other.interact_pointer_pos_or_nan),
intrinsic_size_or_nan: Vec2::NAN,
}
}
}

View File

@@ -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`.
@@ -586,6 +575,8 @@ pub struct ScrollStyle {
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub interact_handle_opacity: f32,
pub fade: ScrollFadeStyle,
}
impl Default for ScrollStyle {
@@ -616,6 +607,8 @@ impl ScrollStyle {
dormant_handle_opacity: 0.0,
active_handle_opacity: 0.6,
interact_handle_opacity: 1.0,
fade: Default::default(),
}
}
@@ -699,6 +692,8 @@ impl ScrollStyle {
dormant_handle_opacity,
active_handle_opacity,
interact_handle_opacity,
fade,
} = self;
ui.horizontal(|ui| {
@@ -772,6 +767,52 @@ impl ScrollStyle {
ui.label("Inner margin");
});
}
ui.separator();
fade.ui(ui);
}
}
/// Controls if and how to fade out the sides of a [`crate::ScrollArea`]
/// to indicate there is more there if you scroll.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollFadeStyle {
/// Opacity of the fade effect at the outer edge, in 0.0-1.0.
///
/// Set to 0.0 to disable the fade effect.
pub strength: f32,
/// Size of the fade-area (height for vertical scrolling,
/// width for horizontal scrolling).
pub size: f32,
}
impl Default for ScrollFadeStyle {
fn default() -> Self {
Self {
strength: 0.5,
size: 20.0,
}
}
}
impl ScrollFadeStyle {
pub fn ui(&mut self, ui: &mut Ui) {
let Self { strength, size } = self;
ui.horizontal(|ui| {
ui.add(DragValue::new(strength).speed(0.01).range(0.0..=1.0));
ui.label("Fade strength");
});
if 0.0 < *strength {
ui.horizontal(|ui| {
ui.add(DragValue::new(size).range(0.0..=64.0));
ui.label("Fade size");
});
}
}
}
@@ -1114,13 +1155,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 {
@@ -1251,11 +1285,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
@@ -1303,6 +1332,10 @@ pub struct DebugOptions {
/// Show interesting widgets under the mouse cursor.
pub show_widget_hits: bool,
/// Show a warning if the same `Rect` had different `Id` and the same parent `Id` on the
/// previous frame.
pub warn_if_rect_changes_id: bool,
/// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`].
///
/// See [`emath::GuiRounding`] for more.
@@ -1329,6 +1362,7 @@ impl Default for DebugOptions {
show_resize: false,
show_interactive_widgets: false,
show_widget_hits: false,
warn_if_rect_changes_id: cfg!(debug_assertions),
show_unaligned: cfg!(debug_assertions),
show_focused_widget: false,
}
@@ -1353,7 +1387,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,
@@ -1361,7 +1394,6 @@ 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(),
@@ -1667,7 +1699,6 @@ use crate::{
impl Style {
pub fn ui(&mut self, ui: &mut crate::Ui) {
#[expect(deprecated)]
let Self {
override_font_id,
override_text_style,
@@ -1675,7 +1706,6 @@ impl Style {
text_styles,
drag_value_text_style,
number_formatter: _, // can't change callbacks in the UI
wrap: _,
wrap_mode,
spacing,
interaction,
@@ -2287,11 +2317,13 @@ impl Visuals {
max_texture_side: _,
alpha_from_coverage,
font_hinting,
subpixel_binning,
} = text_options;
text_alpha_from_coverage_ui(ui, alpha_from_coverage);
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| {
@@ -2491,6 +2523,7 @@ impl DebugOptions {
show_resize,
show_interactive_widgets,
show_widget_hits,
warn_if_rect_changes_id,
show_unaligned,
show_focused_widget,
} = self;
@@ -2522,6 +2555,11 @@ impl DebugOptions {
ui.checkbox(show_widget_hits, "Show widgets under mouse pointer");
ui.checkbox(
warn_if_rect_changes_id,
"Warn if a Rect changes Id between frames",
);
ui.checkbox(
show_unaligned,
"Show rectangles not aligned to integer point coordinates",
@@ -2850,8 +2888,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");
@@ -2867,18 +2908,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");
@@ -2924,6 +2967,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

@@ -3,12 +3,10 @@
use std::{any::Any, hash::Hash, ops::Deref, sync::Arc};
use emath::GuiRounding as _;
use epaint::mutex::RwLock;
use crate::containers::menu;
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
use crate::{containers::*, ecolor::*, layout::*, placer::Placer, widgets::*, *};
use emath::GuiRounding as _;
// ----------------------------------------------------------------------------
/// This is what you use to place widgets.
@@ -76,10 +74,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>,
@@ -162,7 +156,6 @@ impl Ui {
placer,
enabled: true,
sizing_pass,
menu_state: None,
stack: Arc::new(ui_stack),
sense,
min_rect_already_remembered: false,
@@ -178,6 +171,7 @@ impl Ui {
ui.ctx().create_widget(
WidgetRect {
id: ui.unique_id,
parent_id: ui.id,
layer_id: ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
@@ -202,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.
@@ -327,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,
@@ -347,6 +299,7 @@ impl Ui {
child_ui.ctx().create_widget(
WidgetRect {
id: child_ui.unique_id,
parent_id: self.id,
layer_id: child_ui.layer_id(),
rect: start_rect,
interact_rect: start_rect,
@@ -368,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]
@@ -418,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
@@ -493,6 +434,12 @@ impl Ui {
&mut self.style_mut().visuals
}
/// Is this [`Ui`] in a tooltip?
#[inline]
pub fn is_tooltip(&self) -> bool {
self.layer_id().order == Order::Tooltip
}
/// Get a reference to this [`Ui`]'s [`UiStack`].
#[inline]
pub fn stack(&self) -> &Arc<UiStack> {
@@ -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 {
@@ -1051,6 +926,7 @@ impl Ui {
self.ctx().create_widget(
WidgetRect {
id,
parent_id: self.id,
layer_id: self.layer_id(),
rect,
interact_rect: self.clip_rect().intersect(rect),
@@ -1062,18 +938,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`].
///
@@ -1085,7 +949,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| {
@@ -1120,6 +984,7 @@ impl Ui {
let mut response = self.ctx().create_widget(
WidgetRect {
id: self.unique_id,
parent_id: self.id,
layer_id: self.layer_id(),
rect: self.min_rect(),
interact_rect: self.clip_rect().intersect(self.min_rect()),
@@ -1135,16 +1000,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,
@@ -1289,7 +1144,7 @@ impl Ui {
pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response {
let (id, rect) = self.allocate_space(desired_size);
let mut response = self.interact(rect, id, sense);
response.intrinsic_size = Some(desired_size);
response.set_intrinsic_size(desired_size);
response
}
@@ -1497,34 +1352,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.
@@ -1815,7 +1642,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| {
@@ -1840,38 +1667,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.
@@ -2379,21 +2174,6 @@ impl Ui {
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:
@@ -2434,26 +2214,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,
@@ -3007,22 +2767,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

@@ -2,6 +2,8 @@ use std::sync::Arc;
use std::{any::Any, iter::FusedIterator};
use crate::widget_style::Classes;
use epaint::Color32;
use crate::{Direction, Frame, Id, Rect};
/// What kind is this [`crate::Ui`]?
@@ -255,6 +257,25 @@ impl UiStack {
pub fn has_visible_frame(&self) -> bool {
!self.info.frame.stroke.is_empty()
}
/// The background color of this [`crate::Ui`].
///
/// This blend together all [`Frame::fill`] colors
/// up to the root.
#[inline]
pub fn bg_color(&self) -> Color32 {
let mut total = Color32::TRANSPARENT;
for node in self.iter() {
let fill = node.frame().fill;
if fill != Color32::TRANSPARENT {
total = fill.blend(total);
if total.is_opaque() {
break;
}
}
}
total
}
}
// these methods act on the entire stack

View File

@@ -4,7 +4,6 @@
// This will also allow users to pick their own serialization format per type.
use std::{any::Any, sync::Arc};
// -----------------------------------------------------------------------------------------------
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
@@ -182,6 +181,18 @@ impl Element {
}
}
pub fn is_temp(&self) -> bool {
match self {
#[cfg(feature = "persistence")]
Self::Value { serialize_fn, .. } => serialize_fn.is_none(),
#[cfg(not(feature = "persistence"))]
Self::Value { .. } => true,
Self::Serialized(_) => false,
}
}
#[inline]
pub(crate) fn get_temp<T: 'static>(&self) -> Option<&T> {
match self {
@@ -316,6 +327,41 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
use crate::Id;
/// The key used in [`IdTypeMap`], which is a combination of an [`Id`] and a [`TypeId`].
///
/// This key can be used to remove or access values in the [`IdTypeMap`] without
/// knowledge of the `TypeId` `T` that is required for other accessors.
///
/// [`RawKey`]s make no guarantees about layout or their ability to be persisted.
/// They only produce deterministic results if they are used with the map
/// they were initially obtained from. Using them on other instances of [`IdTypeMap`]
/// may produce unexpected behavior.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[repr(transparent)]
pub struct RawKey(u64);
impl nohash_hasher::IsEnabled for RawKey {}
impl RawKey {
/// Create a new key for the given type.
///
/// Note that two keys with the same id but different types
/// will be different keys.
///
/// ```
/// use egui::{Id, util::id_type_map::RawKey};
/// assert_ne!(
/// RawKey::new::<i32>(Id::NULL),
/// RawKey::new::<String>(Id::NULL),
/// );
/// ```
#[inline(always)]
pub fn new<T: 'static>(id: Id) -> Self {
let type_id = TypeId::of::<T>();
Self(type_id.value() ^ id.value())
}
}
// TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap.
/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value.
///
@@ -358,7 +404,7 @@ use crate::Id;
#[derive(Clone, Debug)]
// We use `id XOR typeid` as a key, so we don't need to hash again!
pub struct IdTypeMap {
map: nohash_hasher::IntMap<u64, Element>,
map: nohash_hasher::IntMap<RawKey, Element>,
max_bytes_per_type: usize,
}
@@ -375,16 +421,23 @@ impl Default for IdTypeMap {
impl IdTypeMap {
/// Insert a value that will not be persisted.
#[inline]
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.map.insert(hash, Element::new_temp(value));
pub fn insert_temp<T: 'static + Any + Clone + Send + Sync>(
&mut self,
id: Id,
value: T,
) -> RawKey {
let key = RawKey::new::<T>(id);
self.map.insert(key, Element::new_temp(value));
key
}
/// Insert a value that will be persisted next time you start the app.
#[inline]
pub fn insert_persisted<T: SerializableAny>(&mut self, id: Id, value: T) {
let hash = hash(TypeId::of::<T>(), id);
self.map.insert(hash, Element::new_persisted(value));
let key = RawKey::new::<T>(id);
self.map.insert(key, Element::new_persisted(value));
// We don't yet return the key here, because currently all our `raw`
// methods are only for temporary values.
}
/// Read a value without trying to deserialize a persisted value.
@@ -392,8 +445,28 @@ impl IdTypeMap {
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_temp<T: 'static + Clone>(&self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
self.map.get(&hash).and_then(|x| x.get_temp()).cloned()
let key = RawKey::new::<T>(id);
self.map.get(&key).and_then(|x| x.get_temp()).cloned()
}
/// Gets a reference to a value for a given raw key.
///
/// Serialized values are ignored.
pub fn get_temp_raw(&self, raw: RawKey) -> Option<&(dyn Any + Send + Sync)> {
match self.map.get(&raw)? {
Element::Value { value, .. } => Some(value.as_ref()),
Element::Serialized(_) => None,
}
}
/// Gets a mutable reference to a value for a given raw key.
///
/// Serialized values are ignored.
pub fn get_temp_raw_mut(&mut self, raw: RawKey) -> Option<&mut (dyn Any + Send + Sync)> {
match self.map.get_mut(&raw)? {
Element::Value { value, .. } => Some(value.as_mut()),
Element::Serialized(_) => None,
}
}
/// Read a value, optionally deserializing it if available.
@@ -404,9 +477,9 @@ impl IdTypeMap {
/// The call clones the value (if found), so make sure it is cheap to clone!
#[inline]
pub fn get_persisted<T: SerializableAny>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
self.map
.get_mut(&hash)
.get_mut(&key)
.and_then(|x| x.get_mut_persisted())
.cloned()
}
@@ -443,9 +516,9 @@ impl IdTypeMap {
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
use std::collections::hash_map::Entry;
match self.map.entry(hash) {
match self.map.entry(key) {
Entry::Vacant(vacant) => {
// this unwrap will never panic, because we insert correct type right now
#[expect(clippy::unwrap_used)]
@@ -465,9 +538,9 @@ impl IdTypeMap {
id: Id,
insert_with: impl FnOnce() -> T,
) -> &mut T {
let hash = hash(TypeId::of::<T>(), id);
let key = RawKey::new::<T>(id);
use std::collections::hash_map::Entry;
match self.map.entry(hash) {
match self.map.entry(key) {
Entry::Vacant(vacant) => {
// this unwrap will never panic, because we insert correct type right now
#[expect(clippy::unwrap_used)]
@@ -486,7 +559,7 @@ impl IdTypeMap {
#[cfg(feature = "persistence")]
#[allow(clippy::allow_attributes, unused)]
fn get_generation<T: SerializableAny>(&self, id: Id) -> Option<usize> {
let element = self.map.get(&hash(TypeId::of::<T>(), id))?;
let element = self.map.get(&RawKey::new::<T>(id))?;
match element {
Element::Value { .. } => Some(0),
Element::Serialized(SerializedElement { generation, .. }) => Some(*generation),
@@ -496,18 +569,33 @@ impl IdTypeMap {
/// Remove the state of this type and id.
#[inline]
pub fn remove<T: 'static>(&mut self, id: Id) {
let hash = hash(TypeId::of::<T>(), id);
self.map.remove(&hash);
let key = RawKey::new::<T>(id);
self.map.remove(&key);
}
/// Remove and fetch the state of this type and id.
#[inline]
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
let hash = hash(TypeId::of::<T>(), id);
let mut element = self.map.remove(&hash)?;
let key = RawKey::new::<T>(id);
let mut element = self.map.remove(&key)?;
Some(std::mem::take(element.get_mut_temp()?))
}
/// Remove a temporary value given a raw key.
///
/// Serialized values are ignored.
pub fn remove_temp_raw(&mut self, raw: RawKey) -> Option<Box<dyn Any + Send + Sync>> {
use std::collections::hash_map::Entry;
if let Entry::Occupied(e) = self.map.entry(raw)
&& e.get().is_temp()
&& let Element::Value { value, .. } = e.remove()
{
Some(value)
} else {
None
}
}
/// Note all state of the given type.
pub fn remove_by_type<T: 'static>(&mut self) {
let key = TypeId::of::<T>();
@@ -532,6 +620,18 @@ impl IdTypeMap {
self.map.len()
}
/// Returns all [`RawKey`]s to values in this map.
///
/// The returned keys can only be used with this map.
///
/// Serializable values will be ignored.
pub fn temp_keys(&self) -> impl Iterator<Item = RawKey> {
self.map
.iter()
.filter(|(_, v)| v.is_temp())
.map(|(k, _)| *k)
}
/// Count how many values are stored but not yet deserialized.
#[inline]
pub fn count_serialized(&self) -> usize {
@@ -576,11 +676,6 @@ impl IdTypeMap {
}
}
#[inline(always)]
fn hash(type_id: TypeId, id: Id) -> u64 {
type_id.value() ^ id.value()
}
// ----------------------------------------------------------------------------
/// How [`IdTypeMap`] is persisted.
@@ -613,13 +708,13 @@ impl PersistedMap {
{
profiling::scope!("gather");
for (hash, element) in &map.map {
for (key, element) in &map.map {
if let Some(element) = element.to_serialize() {
let stats = types_map.entry(element.type_id).or_default();
stats.num_bytes += element.ron.len();
let generation_stats = stats.generations.entry(element.generation).or_default();
generation_stats.num_bytes += element.ron.len();
generation_stats.elements.push((*hash, element));
generation_stats.elements.push((key.0, element));
} else {
// temporary value that shouldn't be serialized
}
@@ -659,7 +754,7 @@ impl PersistedMap {
.into_iter()
.map(
|(
hash,
raw,
SerializedElement {
type_id,
ron,
@@ -667,7 +762,7 @@ impl PersistedMap {
},
)| {
(
hash,
RawKey(raw),
Element::Serialized(SerializedElement {
type_id,
ron,

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

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

@@ -17,6 +17,12 @@ pub struct WidgetRect {
/// You can ensure globally unique ids using [`crate::Ui::push_id`].
pub id: Id,
/// The [`Id`] of the parent [`crate::Ui`] that hosts this widget.
///
/// Used by debug checks to distinguish true id-instability from
/// cascading id shifts caused by a parent Ui's auto-id changing.
pub parent_id: Id,
/// What layer the widget is on.
pub layer_id: LayerId,
@@ -46,6 +52,7 @@ impl WidgetRect {
pub fn transform(self, transform: emath::TSTransform) -> Self {
let Self {
id,
parent_id,
layer_id,
rect,
interact_rect,
@@ -54,6 +61,7 @@ impl WidgetRect {
} = self;
Self {
id,
parent_id,
layer_id,
rect: transform * rect,
interact_rect: transform * interact_rect,

View File

@@ -156,7 +156,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 +170,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;

View File

@@ -202,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.
@@ -242,6 +236,18 @@ impl<'a> Button<'a> {
self
}
/// Show some text on the left side of the button.
#[inline]
pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self {
self.layout.push_left(Atom::grow());
for atom in left_text.into_atoms() {
self.layout.push_left(atom);
}
self
}
/// Show some text on the right side of the button.
#[inline]
pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self {
@@ -364,6 +370,12 @@ impl<'a> Button<'a> {
AtomLayoutResponse::empty(prepared.response)
};
if let Some(cursor) = ui.visuals().interact_cursor
&& response.response.hovered()
{
ui.ctx().set_cursor_icon(cursor);
}
response.response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)

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

@@ -220,7 +220,7 @@ impl Label {
.rect_without_leading_space()
.translate(pos.to_vec2());
let mut response = ui.allocate_rect(rect, sense);
response.intrinsic_size = Some(galley.intrinsic_size());
response.set_intrinsic_size(galley.intrinsic_size());
for placed_row in galley.rows.iter().skip(1) {
let rect = placed_row.rect().translate(pos.to_vec2());
response |= ui.allocate_rect(rect, sense);
@@ -256,7 +256,7 @@ impl Label {
let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job));
let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense);
response.intrinsic_size = Some(galley.intrinsic_size());
response.set_intrinsic_size(galley.intrinsic_size());
let galley_pos = match galley.job.halign {
Align::LEFT => rect.left_top(),
Align::Center => rect.center_top(),

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

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

@@ -1,19 +1,20 @@
use std::sync::Arc;
use emath::{Rect, TSTransform};
use epaint::{
StrokeKind,
text::{Galley, LayoutJob, cursor::CCursor},
};
use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor};
use crate::{
Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent,
Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer,
TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint,
Align, Align2, 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,
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,
};
@@ -67,15 +68,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc<Galley
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct TextEdit<'t> {
text: &'t mut dyn TextBuffer,
hint_text: WidgetText,
hint_text_font: Option<FontSelection>,
prefix: Atoms<'static>,
suffix: Atoms<'static>,
hint_text: Atoms<'static>,
id: Option<Id>,
id_salt: Option<Id>,
font_selection: FontSelection,
text_color: Option<Color32>,
layouter: Option<LayouterFn<'t>>,
password: bool,
frame: bool,
frame: Option<Frame>,
margin: Margin,
multiline: bool,
interactive: bool,
@@ -120,15 +122,16 @@ impl<'t> TextEdit<'t> {
pub fn multiline(text: &'t mut dyn TextBuffer) -> Self {
Self {
text,
prefix: Default::default(),
suffix: Default::default(),
hint_text: Default::default(),
hint_text_font: None,
id: None,
id_salt: None,
font_selection: Default::default(),
text_color: None,
layouter: None,
password: false,
frame: true,
frame: None,
margin: Margin::symmetric(4, 2),
multiline: true,
interactive: true,
@@ -202,8 +205,22 @@ impl<'t> TextEdit<'t> {
/// # });
/// ```
#[inline]
pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
self.hint_text = hint_text.into();
pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self {
self.hint_text = hint_text.into_atoms();
self
}
/// Add a prefix to the text edit. This will always be shown before the editable text.
#[inline]
pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self {
self.prefix = prefix.into_atoms();
self
}
/// Add a suffix to the text edit. This will always be shown after the editable text.
#[inline]
pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self {
self.suffix = suffix.into_atoms();
self
}
@@ -215,13 +232,6 @@ impl<'t> TextEdit<'t> {
self
}
/// Set a specific style for the hint text.
#[inline]
pub fn hint_text_font(mut self, hint_text_font: impl Into<FontSelection>) -> Self {
self.hint_text_font = Some(hint_text_font.into());
self
}
/// If true, hide the letters from view and prevent copying from the field.
#[inline]
pub fn password(mut self, password: bool) -> Self {
@@ -290,10 +300,10 @@ impl<'t> TextEdit<'t> {
self
}
/// Default is `true`. If set to `false` there will be no frame showing that this is editable text!
/// Customize the [`Frame`] around the text edit.
#[inline]
pub fn frame(mut self, frame: bool) -> Self {
self.frame = frame;
pub fn frame(mut self, frame: Frame) -> Self {
self.frame = Some(frame);
self
}
@@ -402,7 +412,7 @@ impl<'t> TextEdit<'t> {
impl Widget for TextEdit<'_> {
fn ui(self, ui: &mut Ui) -> Response {
self.show(ui).response
self.show(ui).response.response
}
}
@@ -423,63 +433,18 @@ impl TextEdit<'_> {
/// # });
/// ```
pub fn show(self, ui: &mut Ui) -> TextEditOutput {
let is_mutable = self.text.is_mutable();
let frame = self.frame;
let where_to_put_background = ui.painter().add(Shape::Noop);
let background_color = self
.background_color
.unwrap_or_else(|| ui.visuals().text_edit_bg_color());
let output = self.show_content(ui);
if frame {
let visuals = ui.style().interact(&output.response);
let frame_rect = output.response.rect.expand(visuals.expansion);
let shape = if is_mutable {
if output.response.has_focus() {
epaint::RectShape::new(
frame_rect,
visuals.corner_radius,
background_color,
ui.visuals().selection.stroke,
StrokeKind::Inside,
)
} else {
epaint::RectShape::new(
frame_rect,
visuals.corner_radius,
background_color,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
StrokeKind::Inside,
)
}
} else {
let visuals = &ui.style().visuals.widgets.inactive;
epaint::RectShape::stroke(
frame_rect,
visuals.corner_radius,
visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop".
StrokeKind::Inside,
)
};
ui.painter().set(where_to_put_background, shape);
}
output
}
fn show_content(self, ui: &mut Ui) -> TextEditOutput {
let TextEdit {
text,
hint_text,
hint_text_font,
prefix,
suffix,
mut hint_text,
id,
id_salt,
font_selection,
text_color,
layouter,
password,
frame: _,
frame,
margin,
multiline,
interactive,
@@ -492,7 +457,7 @@ impl TextEdit<'_> {
clip_text,
char_limit,
return_key,
background_color: _,
background_color,
} = self;
let text_color = text_color
@@ -501,53 +466,45 @@ impl TextEdit<'_> {
.unwrap_or_else(|| ui.visuals().widgets.inactive.text_color());
let prev_text = text.as_str().to_owned();
let hint_text_str = hint_text.text().to_owned();
let hint_text_str = hint_text.text().unwrap_or_default().to_string();
let font_id = font_selection.resolve(ui.style());
let row_height = ui.fonts_mut(|f| f.row_height(&font_id));
const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this.
let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);
let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width);
let wrap_width = if ui.layout().horizontal_justify() {
available_width
} else {
desired_width.min(available_width)
};
let available_width = ui.available_width().at_least(MIN_WIDTH);
let desired_width = desired_width
.unwrap_or_else(|| ui.spacing().text_edit_width)
.at_least(min_size.x);
let allocate_width = desired_width.at_most(available_width);
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))
};
let layouter = layouter.unwrap_or(&mut default_layouter);
let mut galley = layouter(ui, text, wrap_width);
let desired_inner_width = if clip_text {
wrap_width // visual clipping with scroll in singleline input.
} else {
galley.size().x.max(wrap_width)
};
let desired_height = (desired_height_rows.at_least(1) as f32) * row_height;
let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height));
let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
let rect = outer_rect - margin; // inner rect (excluding frame/margin).
let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height;
let id = id.unwrap_or_else(|| {
if let Some(id_salt) = id_salt {
ui.make_persistent_id(id_salt)
} else {
auto_id // Since we are only storing the cursor a persistent Id is not super important
// Since we are only storing the cursor a persistent Id is not super important
let id = ui.next_auto_id();
ui.skip_ahead_auto_ids(1);
id
}
});
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
// On touch screens (e.g. mobile in `eframe` web), should
// dragging select text, or scroll the enclosing [`ScrollArea`] (if any)?
@@ -565,12 +522,221 @@ impl TextEdit<'_> {
} else {
Sense::hover()
};
let mut response = ui.interact(outer_rect, id, sense);
response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y));
// Don't sent `OutputEvent::Clicked` when a user presses the space bar
let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default();
let mut cursor_range = None;
let mut prev_cursor_range = None;
let mut text_changed = false;
let text_mutable = text.is_mutable();
let mut handle_events = |ui: &Ui, galley: &mut Arc<Galley>, layouter, wrap_width, text| {
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CCursorRange::one(galley.end())
} else {
CCursorRange::default()
};
prev_cursor_range = state.cursor.range(galley);
let (changed, new_cursor_range) = events(
ui,
&mut state,
text,
galley,
layouter,
id,
wrap_width,
multiline,
password,
default_cursor_range,
char_limit,
event_filter,
return_key,
);
if changed {
text_changed = true;
}
cursor_range = Some(new_cursor_range);
}
};
// We need to calculate the galley within the atom closure, so we can calculate it based on
// the available width (in case of wrapping multiline text edits). But we show it later,
// so we can clip it to the available size. Thus, extract it from the atom closure here.
let mut get_galley = None;
let inner_rect_id = Id::new("text_edit_rect");
let mut response = {
let any_shrink = hint_text.any_shrink();
// Ideally we could just do `let mut atoms = prefix` here, but that won't compile
// but due to servo/rust-smallvec#146 (also see the comment below).
let mut atoms: Atoms<'_> = Atoms::new(());
// 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 prefix {
atoms.push_right(atom);
}
if text.as_str().is_empty() && !hint_text.is_empty() {
// Add hint_text (if any):
let mut shrunk = any_shrink;
let mut first = true;
// Since we can't set a fallback color per atom, we have to override it here.
// Sucks, since it means users won't be able to override it.
hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color()));
for mut atom in hint_text {
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;
}
if first {
// The first atom in the hint text gets inner_rect_id, so we can know
// where to paint the cursor
atom = atom.atom_id(inner_rect_id);
first = false;
}
// The hint text should be shown left top instead of centered (important for
// multi line text edits)
atoms.push_right(atom.atom_align(Align2::LEFT_TOP));
}
// Calculate the empty galley, so it can be read later. The available width is
// technically wrong, but doesn't matter since the galley is empty
let available_width = allocate_width - margin.sum().x;
let galley = layouter(ui, text, available_width);
// We can't update the galley immediately here, since it would show both hint text
// and the newly typed letter. So we pass a clone instead, and accept having a frame
// delay on the very first keystroke.
let mut galley_clone = Arc::clone(&galley);
handle_events(ui, &mut galley_clone, layouter, available_width, text);
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
atoms.push_right(
AtomKind::closure(|ui, args| {
let mut galley = layouter(ui, text, args.available_size.x);
// Handling events here allows us to update the galley immediately on
// keystrokes, avoiding frame delays, and ensuring the scroll_to within
// ScrollAreas works correctly.
handle_events(ui, &mut galley, layouter, args.available_size.x, text);
let intrinsic_size = galley.intrinsic_size();
let mut size = galley.size();
size.y = size.y.at_least(min_inner_height);
if clip_text {
size.x = size.x.at_most(args.available_size.x);
}
// We paint the galley later, so we can do clipping and offsetting
get_galley = Some(galley);
IntoSizedResult {
intrinsic_size,
sized: SizedAtomKind::Empty { size: Some(size) },
}
})
.atom_grow(true)
.atom_align(self.align)
.atom_id(inner_rect_id)
.atom_shrink(should_shrink),
);
}
// 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 {
atoms.push_right(atom);
}
let custom_frame = frame.is_some();
let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin));
let min_height = min_inner_height + frame.total_margin().sum().y;
// This wrap mode only affects the hint_text
let wrap_mode = if multiline {
TextWrapMode::Wrap
} else {
TextWrapMode::Truncate
};
let mut allocated = AtomLayout::new(atoms)
.id(id)
.min_size(Vec2::new(allocate_width, min_height))
.max_width(allocate_width)
.sense(sense)
.frame(frame)
.align2(align)
.wrap_mode(wrap_mode)
.allocate(ui);
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());
let (corner_radius, background_color, stroke) = if text_mutable {
if allocated.response.has_focus() {
(
visuals.corner_radius,
background_color,
ui.visuals().selection.stroke,
)
} else {
(visuals.corner_radius, background_color, visuals.bg_stroke)
}
} else {
let visuals = &ui.style().visuals.widgets.inactive;
(
visuals.corner_radius,
Color32::TRANSPARENT,
visuals.bg_stroke,
)
};
allocated
.frame
.fill(background_color)
.corner_radius(corner_radius)
.inner_margin(
allocated.frame.inner_margin
+ Margin::same((visuals.expansion - stroke.width).round() as i8),
)
.outer_margin(Margin::same(-(visuals.expansion as i8)))
.stroke(stroke)
};
allocated.paint(ui)
};
let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO);
// Our atom closure was now called, so the galley should always be available here
let mut galley = get_galley.expect("Galley should be available here");
// Don't send `OutputEvent::Clicked` when a user presses the space bar
response.flags -= response::Flags::FAKE_PRIMARY_CLICKED;
let text_clip_rect = rect;
let text_clip_rect = inner_rect;
let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor
if interactive && let Some(pointer_pos) = response.interact_pointer_pos() {
@@ -580,20 +746,22 @@ 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 - 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(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);
}
let is_being_dragged = ui.ctx().is_being_dragged(response.id);
let is_being_dragged = ui.is_being_dragged(response.id);
let did_interact = state.cursor.pointer_interaction(
ui,
&response,
@@ -613,44 +781,15 @@ impl TextEdit<'_> {
ui.set_cursor_icon(CursorIcon::Text);
}
let mut cursor_range = None;
let prev_cursor_range = state.cursor.range(&galley);
if interactive && ui.memory(|mem| mem.has_focus(id)) {
ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));
let default_cursor_range = if cursor_at_end {
CCursorRange::one(galley.end())
} else {
CCursorRange::default()
};
let (changed, new_cursor_range) = events(
ui,
&mut state,
text,
&mut galley,
layouter,
id,
wrap_width,
multiline,
password,
default_cursor_range,
char_limit,
event_filter,
return_key,
);
if changed {
response.mark_changed();
}
cursor_range = Some(new_cursor_range);
if text_changed {
response.mark_changed();
}
let mut galley_pos = align
.align_size_within_rect(galley.size(), rect)
.intersect(rect) // limit pos to the response rect area
.align_size_within_rect(galley.size(), inner_rect)
.intersect(inner_rect) // limit pos to the response rect area
.min;
let align_offset = rect.left_top() - galley_pos;
let align_offset = inner_rect.left_top() - galley_pos;
// Visual clipping for singleline text editor with text larger than width
if clip_text && align_offset.x == 0.0 {
@@ -660,18 +799,18 @@ impl TextEdit<'_> {
};
let mut offset_x = state.text_offset.x;
let visible_range = offset_x..=offset_x + desired_inner_size.x;
let visible_range = offset_x..=offset_x + inner_rect.width();
if !visible_range.contains(&cursor_pos) {
if cursor_pos < *visible_range.start() {
offset_x = cursor_pos;
} else {
offset_x = cursor_pos - desired_inner_size.x;
offset_x = cursor_pos - inner_rect.width();
}
}
offset_x = offset_x
.at_most(galley.size().x - desired_inner_size.x)
.at_most(galley.size().x - inner_rect.width())
.at_least(0.0);
state.text_offset = vec2(offset_x, align_offset.y);
@@ -688,32 +827,7 @@ impl TextEdit<'_> {
false
};
if ui.is_rect_visible(rect) {
if text.as_str().is_empty() && !hint_text.is_empty() {
let hint_text_color = ui.visuals().weak_text_color();
let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into());
let galley = if multiline {
hint_text.into_galley(
ui,
Some(TextWrapMode::Wrap),
desired_inner_size.x,
hint_text_font_id,
)
} else {
hint_text.into_galley(
ui,
Some(TextWrapMode::Extend),
f32::INFINITY,
hint_text_font_id,
)
};
let galley_pos = align
.align_size_within_rect(galley.size(), rect)
.intersect(rect)
.min;
painter.galley(galley_pos, galley, hint_text_color);
}
if ui.is_rect_visible(inner_rect) {
let has_focus = ui.memory(|mem| mem.has_focus(id));
if has_focus && let Some(cursor_range) = state.cursor.range(&galley) {
@@ -721,53 +835,19 @@ impl TextEdit<'_> {
paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None);
}
// Allocate additional space if edits were made this frame that changed the size. This is important so that,
// if there's a ScrollArea, it can properly scroll to the cursor.
// Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640)
if !clip_text
&& let extra_size = galley.size() - rect.size()
&& (extra_size.x > 0.0 || extra_size.y > 0.0)
{
match ui.layout().main_dir() {
crate::Direction::LeftToRight | crate::Direction::TopDown => {
ui.allocate_rect(
Rect::from_min_size(outer_rect.max, extra_size),
Sense::hover(),
);
}
crate::Direction::RightToLeft => {
ui.allocate_rect(
Rect::from_min_size(
emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y),
extra_size,
),
Sense::hover(),
);
}
crate::Direction::BottomUp => {
ui.allocate_rect(
Rect::from_min_size(
emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y),
extra_size,
),
Sense::hover(),
);
}
}
} else {
// Avoid an ID shift during this pass if the textedit grow
ui.skip_ahead_auto_ids(1);
}
painter.galley(galley_pos, Arc::clone(&galley), text_color);
painter.galley(
galley_pos - vec2(galley.rect.left(), 0.0),
Arc::clone(&galley),
text_color,
);
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:
ui.scroll_to_rect(primary_cursor_rect + margin, None);
ui.scroll_to_rect(primary_cursor_rect, None);
}
if text.is_mutable() && interactive {
@@ -789,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.ctx().output_mut(|o| {
o.ime = Some(crate::output::IMEOutput {
rect: to_global * 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() {
@@ -846,24 +917,22 @@ impl TextEdit<'_> {
});
}
{
let role = if password {
accesskit::Role::PasswordInput
} else if multiline {
accesskit::Role::MultilineTextInput
} else {
accesskit::Role::TextInput
};
let role = if password {
accesskit::Role::PasswordInput
} else if multiline {
accesskit::Role::MultilineTextInput
} else {
accesskit::Role::TextInput
};
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
id,
cursor_range,
role,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
}
crate::text_selection::accesskit_text::update_accesskit_for_text_widget(
ui.ctx(),
id,
cursor_range,
role,
TSTransform::from_translation(galley_pos.to_vec2()),
&galley,
);
TextEditOutput {
response,
@@ -911,7 +980,7 @@ fn events(
event_filter: EventFilter,
return_key: Option<KeyboardShortcut>,
) -> (bool, CCursorRange) {
let os = ui.ctx().os();
let os = ui.os();
let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range);
@@ -930,12 +999,11 @@ fn events(
let mut any_change = false;
let mut events = ui.input(|i| i.filtered_events(&event_filter));
let events = ui.input(|i| i.filtered_events(&event_filter));
if state.ime_enabled {
remove_ime_incompatible_events(&mut events);
// Process IME events first:
events.sort_by_key(|e| !matches!(e, Event::Ime(_)));
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 {
@@ -958,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);
@@ -968,8 +1038,6 @@ fn events(
}
Some(CCursorRange::one(ccursor))
} else {
None
}
}
Event::Text(text_to_insert) => {
@@ -1065,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.
@@ -1099,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))
}
}
}
@@ -1169,27 +1249,6 @@ fn events(
// ----------------------------------------------------------------------------
fn remove_ime_incompatible_events(events: &mut Vec<Event>) {
// Remove key events which cause problems while 'IME' is being used.
// See https://github.com/emilk/egui/pull/4509
events.retain(|event| {
!matches!(
event,
Event::Key { repeat: true, .. }
| Event::Key {
key: Key::Backspace
| Key::ArrowUp
| Key::ArrowDown
| Key::ArrowLeft
| Key::ArrowRight,
..
}
)
});
}
// ----------------------------------------------------------------------------
/// Returns `Some(new_cursor)` if we did mutate `text`.
fn check_for_mutating_key_press(
os: OperatingSystem,

View File

@@ -5,7 +5,7 @@ use crate::text::CCursorRange;
/// The output from a [`TextEdit`](crate::TextEdit).
pub struct TextEditOutput {
/// The interaction response.
pub response: crate::Response,
pub response: crate::AtomLayoutResponse,
/// How the text was displayed.
pub galley: Arc<crate::Galley>,

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,12 +23,12 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["wgpu", "persistence"]
default = ["wgpu", "persistence", "wayland", "x11"]
# image_viewer adds about 0.9 MB of WASM
web_app = ["http", "persistence"]
accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"]
easymark = [] # easymark is off by default, because it a pretty shitty markup language
http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"]
image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"]
persistence = ["eframe/persistence", "egui_extras/serde", "egui/persistence", "serde"]
@@ -37,20 +37,20 @@ serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"]
syntect = ["egui_demo_lib/syntect"]
glow = ["eframe/glow"]
wgpu = ["eframe/wgpu", "bytemuck", "dep:wgpu"]
wgpu = ["eframe/wgpu", "bytemuck"]
wayland = ["eframe/wayland"]
x11 = ["eframe/x11"]
[dependencies]
chrono = { workspace = true, features = ["js-sys", "wasmbind"] }
eframe = { workspace = true, default-features = false, features = ["web_screen_reader"] }
egui = { workspace = true, features = ["callstack", "default"] }
egui_demo_lib = { workspace = true, features = ["default", "chrono"] }
egui_demo_lib = { workspace = true, features = ["default", "jiff"] }
egui_extras = { workspace = true, features = ["default", "image"] }
image = { workspace = true, default-features = false, features = [
# Ensure we can display the test images
"png",
] }
jiff = { workspace = true, features = ["std", "tz-system", "js"] }
log.workspace = true
profiling.workspace = true
@@ -60,9 +60,6 @@ accesskit_consumer = { workspace = true, optional = true }
bytemuck = { workspace = true, optional = true }
puffin = { workspace = true, optional = true }
puffin_http = { workspace = true, optional = true }
# Enable both WebGL & WebGPU when targeting the web (these features have no effect when not targeting wasm32)
# Also enable the default features so we have a supported backend for every platform.
wgpu = { workspace = true, features = ["default", "webgpu", "webgl"], optional = true }
# feature "http":

View File

@@ -40,7 +40,7 @@ impl Custom3d {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("custom3d"),
bind_group_layouts: &[&bind_group_layout],
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});

View File

@@ -211,7 +211,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let wgpu_adapter_details_ui = |ui: &mut egui::Ui, adapter: &eframe::wgpu::Adapter| {
let info = &adapter.get_info();
let wgpu::AdapterInfo {
let eframe::wgpu::AdapterInfo {
name,
vendor,
device,
@@ -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

@@ -9,9 +9,10 @@ pub use wrap_app::{Anchor, WrapApp};
/// Time of day as seconds since midnight. Used for clock in demo app.
pub(crate) fn seconds_since_midnight() -> f64 {
use chrono::Timelike as _;
let time = chrono::Local::now().time();
time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64)
jiff::Zoned::now()
.time()
.duration_since(jiff::civil::Time::midnight())
.as_secs_f64()
}
/// Trait that wraps different parts of the demo app.

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

View File

@@ -8,12 +8,14 @@ use core::any::Any;
use crate::DemoApp;
#[cfg(feature = "easymark")]
#[derive(Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct EasyMarkApp {
editor: egui_demo_lib::easy_mark::EasyMarkEditor,
}
#[cfg(feature = "easymark")]
impl DemoApp for EasyMarkApp {
fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.editor.panels(ui);
@@ -152,12 +154,18 @@ enum Command {
#[cfg_attr(feature = "serde", serde(default))]
pub struct State {
demo: DemoWindows,
#[cfg(feature = "easymark")]
easy_mark_editor: EasyMarkApp,
#[cfg(feature = "http")]
http: crate::apps::HttpApp,
#[cfg(feature = "image_viewer")]
image_viewer: crate::apps::ImageViewer,
pub clock: FractalClockApp,
rendering_test: ColorTestApp,
selected_anchor: Anchor,
@@ -212,6 +220,7 @@ impl WrapApp {
Anchor::Demo,
&mut self.state.demo as &mut dyn DemoApp,
),
#[cfg(feature = "easymark")]
(
"🖹 EasyMark editor",
Anchor::EasyMarkEditor,
@@ -400,6 +409,8 @@ impl WrapApp {
}
fn bar_contents(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame, cmd: &mut Command) {
ui.add_space(8.0);
egui::widgets::global_theme_preference_switch(ui);
ui.separator();
@@ -455,10 +466,10 @@ impl WrapApp {
for file in &i.raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
} else {
} else if file.mime.is_empty() {
text += "\n???";
} else {
write!(text, "\n{}", file.mime).ok();
}
}
text
@@ -494,10 +505,10 @@ impl WrapApp {
for file in &self.dropped_files {
let mut info = if let Some(path) = &file.path {
path.display().to_string()
} else if !file.name.is_empty() {
file.name.clone()
} else {
} else if file.name.is_empty() {
"???".to_owned()
} else {
file.name.clone()
};
let mut additional_info = vec![];
@@ -508,7 +519,8 @@ impl WrapApp {
additional_info.push(format!("{} bytes", bytes.len()));
}
if !additional_info.is_empty() {
info += &format!(" ({})", additional_info.join(", "));
use std::fmt::Write as _;
write!(info, " ({})", additional_info.join(", ")).ok();
}
ui.label(info);

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4cff85a005ad897624f6c7a2b2ad599325ec99e0b3c9c35963f16611f283997
size 335371
oid sha256:288e11a1fa684575155826a760d5aecc5855e1f4b68bc8954441bf3ac015ee84
size 335175

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cc55083688d043234c37d22c74635a44b00f4d28c3802c4327c2eaf563c73eed
size 92800
oid sha256:d674918c635bfc865043f2123c0f5d4a671dd21ba7b878c056e817b19f2e8f00
size 92770

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78d91fa4657cd1cb375487f606b80d418ed6fdbd8a0c0225b9383eead5001563
size 169682
oid sha256:fa67354cfe4d9cb8774c8de614be5975853a4a817ceaf96c3ec7a968de638d8d
size 169740

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256
size 96759
oid sha256:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390
size 98934

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