1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 04:40:03 -04:00

Merge branch 'main' into theme_plugin

# Conflicts:
#	crates/egui/src/widget_style.rs
This commit is contained in:
Lucas Meurer
2026-08-21 11:33:30 +02:00
252 changed files with 4993 additions and 1936 deletions

View File

@@ -6,6 +6,13 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.36.1 - 2026-08-07
Nothing new
## 0.36.0 - 2026-08-05
Nothing new
## 0.35.0 - 2026-06-25
Nothing new

View File

@@ -1,4 +1,4 @@
use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
use crate::{Rgba, fast_round, mul_frac_round};
/// This format is used for space-efficient color representation (32 bits).
///
@@ -30,15 +30,15 @@ use crate::{Rgba, fast_round, linear_f32_from_linear_u8};
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Color32(pub(crate) [u8; 4]);
impl std::fmt::Debug for Color32 {
impl core::fmt::Debug for Color32 {
/// Prints the contents with premultiplied alpha!
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let [r, g, b, a] = self.0;
write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}")
}
}
impl std::ops::Index<usize> for Color32 {
impl core::ops::Index<usize> for Color32 {
type Output = u8;
#[inline]
@@ -47,7 +47,7 @@ impl std::ops::Index<usize> for Color32 {
}
}
impl std::ops::IndexMut<usize> for Color32 {
impl core::ops::IndexMut<usize> for Color32 {
#[inline]
fn index_mut(&mut self, index: usize) -> &mut u8 {
&mut self.0[index]
@@ -131,35 +131,10 @@ impl Color32 {
/// but for transparent colors what you get back might be slightly different (rounding errors).
#[inline]
pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self {
use std::sync::OnceLock;
match a {
// common-case optimization:
0 => Self::TRANSPARENT,
// common-case optimization:
255 => Self::from_rgb(r, g, b),
a => {
static LOOKUP_TABLE: OnceLock<Box<[u8]>> = OnceLock::new();
let lut = LOOKUP_TABLE.get_or_init(|| {
(0..=u16::MAX)
.map(|i| {
let [value, alpha] = i.to_ne_bytes();
fast_round(value as f32 * linear_f32_from_linear_u8(alpha))
})
.collect()
});
let [r, g, b] =
[r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
Self::from_rgba_unmultiplied_const(r, g, b, a)
}
/// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context.
///
/// It is slightly slower when operating on non-const data.
/// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts.
#[inline]
pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self {
match a {
@@ -170,9 +145,9 @@ impl Color32 {
255 => Self::from_rgb(r, g, b),
a => {
let r = fast_round(r as f32 * linear_f32_from_linear_u8(a));
let g = fast_round(g as f32 * linear_f32_from_linear_u8(a));
let b = fast_round(b as f32 * linear_f32_from_linear_u8(a));
let r = mul_frac_round(r, a);
let g = mul_frac_round(g, a);
let b = mul_frac_round(b, a);
Self::from_rgba_premultiplied(r, g, b, a)
}
}
@@ -378,7 +353,7 @@ impl Color32 {
}
}
impl std::ops::Mul for Color32 {
impl core::ops::Mul for Color32 {
type Output = Self;
/// Fast gamma-space multiplication.
@@ -393,7 +368,7 @@ impl std::ops::Mul for Color32 {
}
}
impl std::ops::Add for Color32 {
impl core::ops::Add for Color32 {
type Output = Self;
#[inline]
@@ -489,7 +464,7 @@ mod test {
} else {
// There will be small rounding errors whenever the alpha is not 0 or 255,
// because we multiply and then unmultiply the alpha.
for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) {
for (&a, &b) in core::iter::zip(&in_rgba, &out_rgba) {
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
}
}
@@ -535,4 +510,14 @@ mod test {
Color32::from_rgba_unmultiplied(255, 0, 0, 128)
);
}
#[test]
fn mul_frac_round_vs_old() {
for x in (0..=255u8).step_by(4) {
for a in (1..=255u8).step_by(4) {
let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a));
assert_eq!(old, mul_frac_round(x, a));
}
}
}
}

View File

@@ -3,7 +3,7 @@
//! Supports the 3, 4, 6, and 8-digit formats, according to the specification in
//! <https://drafts.csswg.org/css-color-4/#hex-color>
use std::{fmt::Display, str::FromStr};
use core::{fmt::Display, str::FromStr};
use crate::Color32;
@@ -31,7 +31,7 @@ pub enum HexColor {
pub enum ParseHexColorError {
MissingHash,
InvalidLength,
InvalidInt(std::num::ParseIntError),
InvalidInt(core::num::ParseIntError),
}
impl FromStr for HexColor {
@@ -45,7 +45,7 @@ impl FromStr for HexColor {
}
impl Display for HexColor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Hex3(color) => {
let [r, g, b, _] = color.to_srgba_unmultiplied().map(|u| u >> 4);

View File

@@ -41,7 +41,6 @@ impl Hsva {
/// From linear RGBA with premultiplied alpha
#[inline]
pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![expect(clippy::many_single_char_names)]
if a <= 0.0 {
if r == 0.0 && b == 0.0 && a == 0.0 {
Self::default()
@@ -57,7 +56,6 @@ impl Hsva {
/// From linear RGBA without premultiplied alpha
#[inline]
pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self {
#![expect(clippy::many_single_char_names)]
let (h, s, v) = hsv_from_rgb([r, g, b]);
Self { h, s, v, a }
}
@@ -189,7 +187,6 @@ impl From<Color32> for Hsva {
/// All ranges in 0-1, rgb is linear.
#[inline]
pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
#![expect(clippy::many_single_char_names)]
let min = r.min(g.min(b));
let max = r.max(g.max(b)); // value
@@ -213,7 +210,6 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) {
/// All ranges in 0-1, rgb is linear.
#[inline]
pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] {
#![expect(clippy::many_single_char_names)]
let h = (h.fract() + 1.0).fract(); // wrap
let s = s.clamp(0.0, 1.0);

View File

@@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 {
(r + 0.5) as _ // rust does a saturating cast since 1.45
}
/// Compute val * (frac/255) with no floating point or divisions.
#[inline]
const fn mul_frac_round(val: u8, frac: u8) -> u8 {
// Treat this as a simple fixed point calculation
let p = (val as u16) * (frac as u16) + 128;
((p + (p >> 8)) >> 8) as u8
// Logic split out a bit more.
//let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate.
//return ((p + 1 + (p >> 8)) >> 8) as u8;
}
#[test]
pub fn test_srgba_conversion() {
for b in 0..=255 {

View File

@@ -9,7 +9,7 @@ use crate::Color32;
#[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))]
pub struct Rgba(pub(crate) [f32; 4]);
impl std::ops::Index<usize> for Rgba {
impl core::ops::Index<usize> for Rgba {
type Output = f32;
#[inline]
@@ -18,7 +18,7 @@ impl std::ops::Index<usize> for Rgba {
}
}
impl std::ops::IndexMut<usize> for Rgba {
impl core::ops::IndexMut<usize> for Rgba {
#[inline]
fn index_mut(&mut self, index: usize) -> &mut f32 {
&mut self.0[index]
@@ -27,20 +27,20 @@ impl std::ops::IndexMut<usize> for Rgba {
/// Deterministically hash an `f32`, treating all NANs as equal, and ignoring the sign of zero.
#[inline]
pub(crate) fn f32_hash<H: std::hash::Hasher>(state: &mut H, f: f32) {
pub(crate) fn f32_hash<H: core::hash::Hasher>(state: &mut H, f: f32) {
if f == 0.0 {
state.write_u8(0);
} else if f.is_nan() {
state.write_u8(1);
} else {
use std::hash::Hash as _;
use core::hash::Hash as _;
f.to_bits().hash(state);
}
}
impl std::hash::Hash for Rgba {
impl core::hash::Hash for Rgba {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
crate::f32_hash(state, self.0[0]);
crate::f32_hash(state, self.0[1]);
crate::f32_hash(state, self.0[2]);
@@ -219,7 +219,7 @@ impl Rgba {
}
}
impl std::ops::Add for Rgba {
impl core::ops::Add for Rgba {
type Output = Self;
#[inline]
@@ -233,7 +233,7 @@ impl std::ops::Add for Rgba {
}
}
impl std::ops::Mul for Rgba {
impl core::ops::Mul for Rgba {
type Output = Self;
#[inline]
@@ -247,7 +247,7 @@ impl std::ops::Mul for Rgba {
}
}
impl std::ops::Mul<f32> for Rgba {
impl core::ops::Mul<f32> for Rgba {
type Output = Self;
#[inline]
@@ -261,7 +261,7 @@ impl std::ops::Mul<f32> for Rgba {
}
}
impl std::ops::Mul<Rgba> for f32 {
impl core::ops::Mul<Rgba> for f32 {
type Output = Rgba;
#[inline]
@@ -336,7 +336,7 @@ mod test {
} else {
// There will be small rounding errors whenever the alpha is not 0 or 255,
// because we multiply and then unmultiply the alpha.
for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) {
for (&a, &b) in core::iter::zip(&in_rgba, &out_rgba) {
assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}");
}
}

View File

@@ -7,6 +7,24 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.36.1 - 2026-08-07
Nothing new
## 0.36.0 - 2026-08-05
### 🔧 Changed
* Improve robustness of text input handling for `eframe/web` [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho)
* Eframe: make webbrowser dependency optional [#8372](https://github.com/emilk/egui/pull/8372) by [@wyvernbw](https://github.com/wyvernbw)
* Store `web_sys::File` inside of `DroppedFile` [#8354](https://github.com/emilk/egui/pull/8354) by [@grtlr](https://github.com/grtlr)
### 🐛 Fixed
* Web: don't scroll host page when text agent or canvas grabs focus [#8296](https://github.com/emilk/egui/pull/8296) by [@emilk](https://github.com/emilk)
* Fix missing modifier events on eframe web, handle physical keys [#8345](https://github.com/emilk/egui/pull/8345) by [@lucasmerlin](https://github.com/lucasmerlin)
* Web: Avoid panic from lost texture updates when loaded on a background tab [#8313](https://github.com/emilk/egui/pull/8313) by [@kevinmehall](https://github.com/kevinmehall)
* Web: anchor the text agent to the canvas [#8297](https://github.com/emilk/egui/pull/8297) by [@emilk](https://github.com/emilk)
* Never run an egui pass when nothing will be shown [#8387](https://github.com/emilk/egui/pull/8387) by [@emilk](https://github.com/emilk)
## 0.35.0 - 2026-06-25
### ⭐ Added
* Add Context::set_cursor_image for OS-level custom cursors [#8155](https://github.com/emilk/egui/pull/8155) by [@all3f0r1](https://github.com/all3f0r1)

View File

@@ -28,6 +28,7 @@ workspace = true
default = [
"accesskit",
"default_fonts",
"links",
"wayland", # Required for Linux support (including CI!)
"web_screen_reader",
"wgpu",
@@ -66,7 +67,7 @@ experimental = ["egui/experimental"]
glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"]
## Enable saving app state to disk.
persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"]
persistence = ["egui-winit/serde", "egui/persistence", "ron", "serde"]
## Enables wayland support and fixes clipboard issue.
##
@@ -128,6 +129,9 @@ __screenshot = []
## and capture screenshots. Off unless the env var is set; no-op on wasm.
inspection = ["dep:egui_inspection", "accesskit"]
## Enables the `links` feature on `egui-winit`, allowing for links to open in browser.
links = ["egui-winit/links"]
[dependencies]
egui = { workspace = true, default-features = false, features = ["bytemuck"] }
@@ -151,7 +155,7 @@ serde = { workspace = true, optional = true }
# -------------------------------------------
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] }
egui-winit = { workspace = true, default-features = false, features = ["clipboard"] }
image = { workspace = true, features = ["png"] } # Needed for app icon
winit = { workspace = true, default-features = false, features = ["rwh_06"] }
@@ -167,7 +171,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe
"egl",
"wgl",
] }
home = { workspace = true, optional = true }
# mac:
[target.'cfg(any(target_os = "macos"))'.dependencies]
@@ -212,7 +215,6 @@ image = { workspace = true, features = ["png"] } # For copying images
js-sys.workspace = true
percent-encoding.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
web-sys = { workspace = true, features = [
"AddEventListenerOptions",
"BinaryType",

View File

@@ -7,7 +7,7 @@
#![warn(missing_docs)] // Let's keep `epi` well-documented.
#[cfg(target_arch = "wasm32")]
use std::any::Any;
use core::any::Any;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
@@ -41,7 +41,7 @@ pub type EventLoopBuilderHook = Box<dyn FnOnce(&mut EventLoopBuilder<UserEvent>)
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub type WindowBuilderHook = Box<dyn FnOnce(egui::ViewportBuilder) -> egui::ViewportBuilder>;
type DynError = Box<dyn std::error::Error + Send + Sync>;
type DynError = Box<dyn core::error::Error + Send + Sync>;
/// This is how your app is created.
///
@@ -73,7 +73,7 @@ pub struct CreationContext<'s> {
/// The `get_proc_address` wrapper of underlying GL context
#[cfg(feature = "glow")]
pub get_proc_address:
Option<std::sync::Arc<dyn Fn(&std::ffi::CStr) -> *const std::ffi::c_void + Send + Sync>>,
Option<std::sync::Arc<dyn Fn(&core::ffi::CStr) -> *const core::ffi::c_void + Send + Sync>>,
/// The underlying WGPU render state.
///
@@ -155,6 +155,12 @@ pub trait App {
///
/// You may NOT show any ui or do any painting during the call to [`Self::logic`].
///
/// While the window is hidden, `eframe` runs no egui pass at all (so that no ui state is
/// disturbed), and calls this via [`egui::Context::run_logic`] instead.
/// You can then still tell that the window is hidden with
/// [`egui::InputState::viewport`], but the rest of [`egui::Context::input`]
/// (events, time, …) is that of the last shown frame.
///
/// The [`egui::Context`] can be cloned and saved if you like.
///
/// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread).
@@ -225,8 +231,8 @@ pub trait App {
// Settings:
/// Time between automatic calls to [`Self::save`]
fn auto_save_interval(&self) -> std::time::Duration {
std::time::Duration::from_secs(30)
fn auto_save_interval(&self) -> core::time::Duration {
core::time::Duration::from_secs(30)
}
/// Background color values for the app, e.g. what is sent to `gl.clearColor`.
@@ -615,8 +621,8 @@ impl Default for Renderer {
}
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::fmt::Display for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for Renderer {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
#[cfg(feature = "glow")]
Self::Glow => "glow".fmt(f),
@@ -628,7 +634,7 @@ impl std::fmt::Display for Renderer {
}
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
impl std::str::FromStr for Renderer {
impl core::str::FromStr for Renderer {
type Err = String;
fn from_str(name: &str) -> Result<Self, String> {

View File

@@ -503,7 +503,7 @@ pub fn run_ui_native(
#[derive(Debug)]
pub enum Error {
/// Something went wrong in user code when creating the app.
AppCreation(Box<dyn std::error::Error + Send + Sync>),
AppCreation(Box<dyn core::error::Error + Send + Sync>),
/// An error from [`winit`].
#[cfg(not(target_arch = "wasm32"))]
@@ -519,7 +519,7 @@ pub enum Error {
/// An error from [`glutin`] when using [`glow`].
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn std::error::Error>),
NoGlutinConfigs(glutin::config::ConfigTemplate, Box<dyn core::error::Error>),
/// An error from [`glutin`] when using [`glow`].
#[cfg(feature = "glow")]
@@ -530,7 +530,7 @@ pub enum Error {
Wgpu(egui_wgpu::WgpuError),
}
impl std::error::Error for Error {}
impl core::error::Error for Error {}
#[cfg(not(target_arch = "wasm32"))]
impl From<winit::error::OsError> for Error {
@@ -572,8 +572,8 @@ impl From<egui_wgpu::WgpuError> for Error {
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::AppCreation(err) => write!(f, "app creation error: {err}"),
@@ -614,4 +614,4 @@ impl std::fmt::Display for Error {
}
/// Short for `Result<T, eframe::Error>`.
pub type Result<T = (), E = Error> = std::result::Result<T, E>;
pub type Result<T = (), E = Error> = core::result::Result<T, E>;

View File

@@ -123,7 +123,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
)
.is_err()
{
return std::ptr::null_mut();
return core::ptr::null_mut();
}
// SAFETY: Creating an HICON which should be readonly on our data.
@@ -161,16 +161,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
if icon_big.is_null() {
log::warn!("Failed to create HICON (for big icon) from embedded png data.");
return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad!
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_BIG as usize,
icon_big as isize,
);
}
}
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_BIG as usize,
icon_big as isize,
);
}
}
{
@@ -180,16 +180,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus {
if icon_small.is_null() {
log::warn!("Failed to create HICON (for small icon) from embedded png data.");
return AppIconStatus::NotSetIgnored;
} else {
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_SMALL as usize,
icon_small as isize,
);
}
}
// SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior.
unsafe {
SendMessageW(
window_handle,
WM_SETICON,
ICON_SMALL as usize,
icon_small as isize,
);
}
}

View File

@@ -83,7 +83,7 @@ pub fn viewport_builder(
}
}
match std::mem::take(&mut native_options.window_builder) {
match core::mem::take(&mut native_options.window_builder) {
Some(hook) => hook(viewport_builder),
None => viewport_builder,
}
@@ -156,6 +156,11 @@ pub struct EpiIntegration {
pub beginning: Instant,
is_first_frame: bool,
pub egui_ctx: egui::Context,
/// Input that we have received, but not yet given to egui,
/// because we haven't run any pass since (see [`Self::update_logic_only`]).
pending_raw_input: egui::RawInput,
pending_full_output: egui::FullOutput,
/// When set, it is time to close the native window.
@@ -215,6 +220,7 @@ impl EpiIntegration {
Self {
frame,
last_auto_save: Instant::now(),
pending_raw_input: Default::default(),
pending_full_output: Default::default(),
close: false,
can_drag_window: false,
@@ -262,57 +268,109 @@ impl EpiIntegration {
/// Run user code - this can create immediate viewports, so hold no locks over this!
///
/// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`].
/// If `viewport_ui_cb` is None, we are in the root viewport and will call
/// [`crate::App::logic`] and [`crate::App::ui`].
///
/// Only call this when the ui will actually be shown;
/// use [`Self::update_logic_only`] otherwise.
pub fn update(
&mut self,
app: &mut dyn epi::App,
viewport_ui_cb: Option<&DeferredViewportUiCallback>,
mut raw_input: egui::RawInput,
is_visible: bool,
raw_input: egui::RawInput,
) -> egui::FullOutput {
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
let raw_input = self.prepare_raw_input(app, raw_input);
let close_requested = raw_input.viewport().close_requested();
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
let is_root_viewport = viewport_ui_cb.is_none();
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
if let Some(viewport_ui_cb) = viewport_ui_cb {
// Child viewport
if is_visible {
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
}
profiling::scope!("viewport_callback");
viewport_ui_cb(ui);
} else {
{
profiling::scope!("App::logic");
app.logic(ui.ctx(), &mut self.frame);
}
if is_visible {
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
{
profiling::scope!("App::ui");
app.ui(ui, &mut self.frame);
}
}
});
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport && close_requested {
let canceled = full_output.viewport_output[&ViewportId::ROOT]
.commands
.contains(&egui::ViewportCommand::CancelClose);
if canceled {
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
} else {
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
self.close = true;
}
self.handle_close_request(canceled);
}
self.pending_full_output.append(full_output);
std::mem::take(&mut self.pending_full_output)
core::mem::take(&mut self.pending_full_output)
}
/// Let the app tick its logic without showing any ui,
/// because the window is minimized or occluded.
///
/// No egui pass is run, so all ui state is left untouched:
/// the app will find everything where it left it once the window is visible again.
///
/// Only call this for the root viewport: only it has [`crate::App::logic`].
pub fn update_logic_only(
&mut self,
app: &mut dyn epi::App,
raw_input: egui::RawInput,
) -> egui::LogicOutput {
let raw_input = self.prepare_raw_input(app, raw_input);
let close_requested = raw_input.viewport().close_requested();
let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| {
profiling::scope!("App::logic");
app.logic(ctx, &mut self.frame);
});
// No pass consumed the input, so save it for the next one:
self.pending_raw_input = raw_input;
if close_requested {
let canceled = logic_output
.viewport_commands
.get(&ViewportId::ROOT)
.is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose));
self.handle_close_request(canceled);
}
logic_output
}
/// Prepend any input we couldn't give to egui earlier, set the time, and run the app hook.
fn prepare_raw_input(
&mut self,
app: &mut dyn epi::App,
new_input: egui::RawInput,
) -> egui::RawInput {
let mut raw_input = core::mem::take(&mut self.pending_raw_input);
raw_input.append(new_input); // The new input wins where they overlap
raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
app.raw_input_hook(&self.egui_ctx, &mut raw_input);
raw_input
}
fn handle_close_request(&mut self, canceled: bool) {
if canceled {
log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
} else {
log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
self.close = true;
}
}
pub fn report_frame_time(&mut self, seconds: f32) {
@@ -321,7 +379,7 @@ impl EpiIntegration {
pub fn post_rendering(&mut self, window: &winit::window::Window) {
profiling::function_scope!();
if std::mem::take(&mut self.is_first_frame) {
if core::mem::take(&mut self.is_first_frame) {
// We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
window.set_visible(true);
}

View File

@@ -1,4 +1,4 @@
use std::cell::Cell;
use core::cell::Cell;
use winit::event_loop::ActiveEventLoop;
thread_local! {
@@ -14,7 +14,7 @@ impl EventLoopGuard {
cell.get().is_none(),
"Attempted to set a new event loop while one is already set"
);
cell.set(Some(std::ptr::from_ref::<ActiveEventLoop>(event_loop)));
cell.set(Some(core::ptr::from_ref::<ActiveEventLoop>(event_loop)));
});
Self
}

View File

@@ -21,7 +21,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
OS::Nix => var_os("XDG_DATA_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| home::home_dir().map(|p| p.join(".local").join("share")))
.or_else(|| std::env::home_dir().map(|p| p.join(".local").join("share")))
.map(|p| {
p.join(
app_id
@@ -29,7 +29,7 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
.replace(|c: char| c.is_ascii_whitespace(), ""),
)
}),
OS::Mac => home::home_dir().map(|p| {
OS::Mac => std::env::home_dir().map(|p| {
p.join("Library")
.join("Application Support")
.join(app_id.replace(|c: char| c.is_ascii_whitespace(), "-"))
@@ -44,10 +44,10 @@ pub fn storage_dir(app_id: &str) -> Option<PathBuf> {
#[cfg(all(windows, not(target_vendor = "uwp")))]
#[expect(unsafe_code)]
fn roaming_appdata() -> Option<PathBuf> {
use core::ptr;
use core::slice;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt as _;
use std::ptr;
use std::slice;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
@@ -66,8 +66,8 @@ fn roaming_appdata() -> Option<PathBuf> {
SHGetKnownFolderPath(
&FOLDERID_RoamingAppData,
KF_FLAG_DONT_VERIFY as u32,
std::ptr::null_mut(),
&mut path_raw,
core::ptr::null_mut(),
&raw mut path_raw,
)
};

View File

@@ -8,7 +8,8 @@
#![expect(clippy::undocumented_unsafe_blocks)]
#![expect(clippy::unwrap_used)]
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use core::{cell::RefCell, num::NonZeroU32};
use std::{rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use glutin::{
@@ -31,16 +32,17 @@ use egui::{
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized},
};
use log::warn;
use super::{
epi_integration, event_loop_context,
winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context},
};
use crate::epaint::textures::TexturesDelta;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized},
};
// ----------------------------------------------------------------------------
// Types:
@@ -73,6 +75,16 @@ struct GlowWinitRunning<'app> {
// NOTE: one painter shared by all viewports.
painter: Rc<RefCell<egui_glow::Painter>>,
/// Any not yet applied deltas for this app.
pending_deltas: TexturesDelta,
}
impl Drop for GlowWinitRunning<'_> {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_deltas.clear();
}
}
/// This struct will contain both persistent and temporary glutin state.
@@ -114,6 +126,9 @@ struct Viewport {
info: ViewportInfo,
actions_requested: Vec<egui_winit::ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// The user-callback that shows the ui.
/// None for immediate viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -125,6 +140,34 @@ struct Viewport {
egui_winit: Option<egui_winit::State>,
}
impl Viewport {
/// Apply the commands, or defer them until we have a window.
fn process_commands(
&mut self,
egui_ctx: &egui::Context,
mut commands: Vec<egui::ViewportCommand>,
) {
self.deferred_commands.append(&mut commands);
if let Some(window) = &self.window {
egui_winit::process_viewport_commands(
egui_ctx,
&mut self.info,
core::mem::take(&mut self.deferred_commands),
window,
&mut self.actions_requested,
);
}
}
}
impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_delta.clear();
}
}
// ----------------------------------------------------------------------------
impl<'app> GlowWinitApp<'app> {
@@ -296,7 +339,7 @@ impl<'app> GlowWinitApp<'app> {
log::warn!("set_cursor_hittest(false) failed: {err}");
}
let app_creator = std::mem::take(&mut self.app_creator)
let app_creator = core::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken");
crate::maybe_attach_inspection_plugin(&integration.egui_ctx, Some(self.app_name.clone()));
@@ -353,6 +396,7 @@ impl<'app> GlowWinitApp<'app> {
app,
glutin,
painter,
pending_deltas: Default::default(),
}))
}
}
@@ -557,7 +601,7 @@ impl GlowWinitRunning<'_> {
}
}
let (raw_input, viewport_ui_cb, is_visible, run_ui) = {
let (raw_input, viewport_ui_cb, is_visible, show_ui) = {
let mut glutin = self.glutin.borrow_mut();
let egui_ctx = glutin.egui_ctx.clone();
let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else {
@@ -576,7 +620,7 @@ impl GlowWinitRunning<'_> {
let mut raw_input = egui_winit.take_egui_input(window);
let viewport_ui_cb = viewport.viewport_ui_cb.clone();
let run_ui =
let show_ui =
is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id);
self.integration.pre_update();
@@ -588,9 +632,58 @@ impl GlowWinitRunning<'_> {
.map(|(id, viewport)| (*id, viewport.info.clone()))
.collect();
(raw_input, viewport_ui_cb, is_visible, run_ui)
(raw_input, viewport_ui_cb, is_visible, show_ui)
};
if !show_ui {
// Nothing will be shown, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when this viewport becomes visible again.
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport {
// The app logic keeps ticking, so it can e.g. ask to be shown again:
let egui::LogicOutput {
platform_output,
viewport_commands,
} = self
.integration
.update_logic_only(self.app.as_mut(), raw_input);
let mut glutin = self.glutin.borrow_mut();
if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) {
viewport.info.events.clear(); // they should have been processed
if let Some(window) = viewport.window.clone()
&& let Some(egui_winit) = viewport.egui_winit.as_mut()
{
egui_winit.handle_platform_output_with_event_loop(
&window,
event_loop,
platform_output,
);
}
}
for (id, commands) in viewport_commands {
if let Some(viewport) = glutin.viewports.get_mut(&id) {
viewport.process_commands(&self.integration.egui_ctx, commands);
}
}
}
sleep_if_invisible_or_minimized(
self.glutin
.borrow()
.viewports
.get(&viewport_id)
.and_then(|viewport| viewport.window.as_deref()),
);
return Ok(if self.integration.should_close() {
EventResult::CloseRequested
} else {
EventResult::Wait
});
}
// HACK: In order to get the right clear_color, the system theme needs to be set, which
// usually only happens in the `update` call. So we call Options::begin_pass early
// to set the right theme. Without this there would be a black flash on the first frame.
@@ -639,12 +732,9 @@ impl GlowWinitRunning<'_> {
// The update function, which could call immediate viewports,
// so make sure we don't hold any locks here required by the immediate viewports rendeer.
let full_output = self.integration.update(
self.app.as_mut(),
viewport_ui_cb.as_deref(),
raw_input,
run_ui,
);
let full_output =
self.integration
.update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
// ------------------------------------------------------------
@@ -653,6 +743,7 @@ impl GlowWinitRunning<'_> {
app,
glutin,
painter,
pending_deltas,
..
} = self;
@@ -666,6 +757,7 @@ impl GlowWinitRunning<'_> {
pixels_per_point,
viewport_output,
} = full_output;
pending_deltas.append(textures_delta);
glutin.remove_viewports_not_in(&viewport_output);
@@ -687,30 +779,28 @@ impl GlowWinitRunning<'_> {
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
// Upload textures even when not visible: the atlas dirty region is already
// consumed, so dropping the delta would desync the font texture.
let has_texture_updates = !textures_delta.set.is_empty() || !textures_delta.free.is_empty();
if is_visible || has_texture_updates {
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
for (id, image_delta) in &textures_delta.set {
painter.set_texture(*id, image_delta);
}
if is_visible {
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
{
// We may need to switch contexts again, because of immediate viewports:
frame_timer.pause();
change_gl_context(current_gl_context, not_current_gl_context, gl_surface);
frame_timer.resume();
}
let screen_size_in_pixels: [u32; 2] = window.inner_size().into();
if !clear_before_update {
painter.clear(screen_size_in_pixels, clear_color);
}
painter.paint_primitives(screen_size_in_pixels, pixels_per_point, &clipped_primitives);
painter.paint_and_update_textures(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
pending_deltas,
);
{
for action in viewport.actions_requested.drain(..) {
@@ -772,25 +862,13 @@ impl GlowWinitRunning<'_> {
}
}
// Free textures *after* painting, since they may still be used in the frame we just drew.
for id in &textures_delta.free {
painter.free_texture(*id);
}
glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output);
integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time
integration.maybe_autosave(app.as_mut(), Some(&window));
if is_invisible_or_minimized(&window) {
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
sleep_if_invisible_or_minimized(Some(&window));
if integration.should_close() {
Ok(EventResult::CloseRequested)
@@ -1120,6 +1198,7 @@ impl GlutinWindowContext {
deferred_commands: vec![],
info: viewport_info,
actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb: None,
gl_surface: None,
window: window.map(Arc::new),
@@ -1330,7 +1409,7 @@ impl GlutinWindowContext {
}
}
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
fn get_proc_address(&self, addr: &core::ffi::CStr) -> *const core::ffi::c_void {
self.gl_config.display().get_proc_address(addr)
}
@@ -1362,7 +1441,7 @@ impl GlutinWindowContext {
class,
builder,
viewport_ui_cb,
mut commands,
commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
@@ -1377,25 +1456,18 @@ impl GlutinWindowContext {
viewport_ui_cb,
);
if let Some(window) = &viewport.window {
let old_inner_size = window.inner_size();
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
viewport.deferred_commands.append(&mut commands);
viewport.process_commands(egui_ctx, commands);
egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
std::mem::take(&mut viewport.deferred_commands),
window,
&mut viewport.actions_requested,
);
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux") {
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
self.resize(viewport_id, new_inner_size);
}
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux")
&& let Some(window) = &viewport.window
&& let Some(old_inner_size) = old_inner_size
{
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size {
self.resize(viewport_id, new_inner_size);
}
}
}
@@ -1436,6 +1508,7 @@ fn initialize_or_update_viewport(
deferred_commands: vec![],
info: Default::default(),
actions_requested: Default::default(),
pending_delta: Default::default(),
viewport_ui_cb,
window: None,
egui_winit: None,
@@ -1584,8 +1657,10 @@ fn render_immediate_viewport(
} = &mut *glutin;
let Some(viewport) = viewports.get_mut(&viewport_id) else {
warn!("Viewport disappeared unexpectedly!");
return;
};
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed
@@ -1621,7 +1696,7 @@ fn render_immediate_viewport(
screen_size_in_pixels,
pixels_per_point,
&clipped_primitives,
&textures_delta,
&mut viewport.pending_delta,
);
{
@@ -1645,7 +1720,7 @@ fn save_screenshot_and_exit(
screen_size_in_pixels: [u32; 2],
) {
assert!(
path.ends_with(".png"),
egui::load::has_extension(path, "png"),
"Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}"
);
let screenshot = painter.read_screen_rgba(screen_size_in_pixels);

View File

@@ -1,4 +1,5 @@
use std::time::{Duration, Instant};
use core::time::Duration;
use std::time::Instant;
use winit::{
application::ApplicationHandler,
@@ -41,7 +42,7 @@ fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result<EventLoo
))
})?);
if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) {
if let Some(hook) = core::mem::take(&mut native_options.event_loop_builder) {
hook(&mut builder);
}
@@ -58,7 +59,7 @@ fn with_event_loop<R>(
mut native_options: epi::NativeOptions,
f: impl FnOnce(&mut EventLoop<UserEvent>, epi::NativeOptions) -> R,
) -> Result<R> {
thread_local!(static EVENT_LOOP: std::cell::RefCell<Option<EventLoop<UserEvent>>> = const { std::cell::RefCell::new(None) });
thread_local!(static EVENT_LOOP: core::cell::RefCell<Option<EventLoop<UserEvent>>> = const { core::cell::RefCell::new(None) });
EVENT_LOOP.with(|event_loop| {
// Since we want to reference NativeOptions when creating the EventLoop we can't
@@ -206,7 +207,12 @@ impl<T: WinitApp> WinitAppWrapper<T> {
invisible_window_ids.push(*window_id);
} else {
log::trace!("request_redraw for {window_id:?}");
event_loop.set_control_flow(ControlFlow::Poll);
// Don't switch to `ControlFlow::Poll` here. `request_redraw`
// is enough to wake the event loop, and on Wayland the
// `RedrawRequested` event is only delivered once the
// compositor sends a frame callback. Polling in the meantime
// busy-loops a whole CPU core.
// See https://github.com/emilk/egui/issues/8326.
window.request_redraw();
}
} else {
@@ -236,10 +242,16 @@ impl<T: WinitApp> WinitAppWrapper<T> {
}
}
// Always set an explicit, sleeping control flow. Previously we only set
// `WaitUntil` when a repaint was already scheduled, which meant that a
// `ControlFlow::Poll` set earlier was never undone once the last timed
// repaint had been consumed, leaving the loop spinning.
// See https://github.com/emilk/egui/issues/8326.
let next_repaint_time = self.windows_next_repaint_times.values().min().copied();
if let Some(next_repaint_time) = next_repaint_time {
event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time));
}
event_loop.set_control_flow(match next_repaint_time {
Some(next_repaint_time) => ControlFlow::WaitUntil(next_repaint_time),
None => ControlFlow::Wait,
});
}
}
@@ -550,7 +562,7 @@ impl<'a> EframeWinitApplication<'a> {
pub fn pump_eframe_app(
&mut self,
event_loop: &mut EventLoop<UserEvent>,
timeout: Option<std::time::Duration>,
timeout: Option<core::time::Duration>,
) -> EframePumpStatus {
use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus};

View File

@@ -5,7 +5,8 @@
//! There is a bunch of improvements we could do,
//! like removing a bunch of `unwraps`.
use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant};
use core::{cell::RefCell, num::NonZeroU32};
use std::{rc::Rc, sync::Arc, time::Instant};
use egui_winit::ActionRequested;
use parking_lot::Mutex;
@@ -17,19 +18,20 @@ use winit::{
use ahash::HashMap;
use egui::{
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap,
DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, TexturesDelta,
ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo,
ViewportOutput,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use log::warn;
use winit_integration::UserEvent;
use crate::{
App, AppCreator, CreationContext, NativeOptions, Result, Storage,
native::{
epi_integration::EpiIntegration,
winit_integration::{EventResult, is_invisible_or_minimized},
winit_integration::{EventResult, sleep_if_invisible_or_minimized},
},
};
@@ -65,6 +67,15 @@ struct WgpuWinitRunning<'app> {
/// Wrapped in an `Rc<RefCell<…>>` so it can be re-entrantly shared via a weak-pointer.
shared: Rc<RefCell<SharedState>>,
pending_deltas: TexturesDelta,
}
impl Drop for WgpuWinitRunning<'_> {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_deltas.clear();
}
}
/// Everything needed by the immediate viewport renderer.\
@@ -91,6 +102,9 @@ pub struct Viewport {
info: ViewportInfo,
actions_requested: Vec<ActionRequested>,
/// Any not yet applied deltas for this viewport.
pending_delta: TexturesDelta,
/// `None` for sync viewports.
viewport_ui_cb: Option<Arc<DeferredViewportUiCallback>>,
@@ -102,6 +116,13 @@ pub struct Viewport {
egui_winit: Option<egui_winit::State>,
}
impl Drop for Viewport {
fn drop(&mut self) {
// Avoid debug panic when dropping unapplied deltas on teardown
self.pending_delta.clear();
}
}
// ----------------------------------------------------------------------------
impl<'app> WgpuWinitApp<'app> {
@@ -289,7 +310,7 @@ impl<'app> WgpuWinitApp<'app> {
egui_winit.init_accesskit(event_loop, &window, event_loop_proxy);
}
let app_creator = std::mem::take(&mut self.app_creator)
let app_creator = core::mem::take(&mut self.app_creator)
.expect("Single-use AppCreator has unexpectedly already been taken");
crate::maybe_attach_inspection_plugin(&egui_ctx, Some(self.app_name.clone()));
@@ -328,6 +349,7 @@ impl<'app> WgpuWinitApp<'app> {
viewport_ui_cb: None,
window: Some(window),
egui_winit: Some(egui_winit),
pending_delta: Default::default(),
},
);
@@ -358,6 +380,7 @@ impl<'app> WgpuWinitApp<'app> {
integration,
app,
shared,
pending_deltas: Default::default(),
}))
}
}
@@ -596,12 +619,13 @@ impl WgpuWinitRunning<'_> {
app,
integration,
shared,
pending_deltas,
} = self;
let mut frame_timer = crate::stopwatch::Stopwatch::new();
frame_timer.start();
let (viewport_ui_cb, raw_input, is_visible, run_ui) = {
let (viewport_ui_cb, raw_input, is_visible, show_ui) = {
profiling::scope!("Prepare");
let mut shared_lock = shared.borrow_mut();
@@ -657,7 +681,7 @@ impl WgpuWinitRunning<'_> {
};
let mut raw_input = egui_winit.take_egui_input(window);
let run_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
let show_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id);
integration.pre_update();
@@ -669,15 +693,67 @@ impl WgpuWinitRunning<'_> {
painter.handle_screenshots(&mut raw_input.events);
(viewport_ui_cb, raw_input, is_visible, run_ui)
(viewport_ui_cb, raw_input, is_visible, show_ui)
};
if !show_ui {
// Nothing will be shown, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when this viewport becomes visible again.
let is_root_viewport = viewport_ui_cb.is_none();
if is_root_viewport {
// The app logic keeps ticking, so it can e.g. ask to be shown again:
let egui::LogicOutput {
platform_output,
viewport_commands,
} = integration.update_logic_only(app.as_mut(), raw_input);
let mut shared_mut = shared.borrow_mut();
let SharedState { viewports, .. } = &mut *shared_mut;
if let Some(viewport) = viewports.get_mut(&viewport_id) {
viewport.info.events.clear(); // they should have been processed
if let Viewport {
window: Some(window),
egui_winit: Some(egui_winit),
..
} = viewport
{
egui_winit.handle_platform_output_with_event_loop(
window,
event_loop,
platform_output,
);
}
}
for (id, commands) in viewport_commands {
if let Some(viewport) = viewports.get_mut(&id) {
viewport.process_commands(&integration.egui_ctx, commands);
}
}
}
sleep_if_invisible_or_minimized(
shared
.borrow()
.viewports
.get(&viewport_id)
.and_then(|viewport| viewport.window.as_deref()),
);
return Ok(if integration.should_close() {
EventResult::CloseRequested
} else {
EventResult::Wait
});
}
// ------------------------------------------------------------
// Runs the update, which could call immediate viewports,
// so make sure we hold no locks here!
let full_output =
integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, run_ui);
let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input);
// ------------------------------------------------------------
@@ -699,6 +775,8 @@ impl WgpuWinitRunning<'_> {
viewport_output,
} = full_output;
pending_deltas.append(textures_delta);
remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output);
let Some(viewport) = viewports.get_mut(&viewport_id) else {
@@ -735,7 +813,7 @@ impl WgpuWinitRunning<'_> {
pixels_per_point,
app.clear_color(&egui_ctx.global_style().visuals),
&clipped_primitives,
&textures_delta,
pending_deltas,
screenshot_commands,
window,
);
@@ -796,16 +874,7 @@ impl WgpuWinitRunning<'_> {
integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref()));
if let Some(window) = window
&& is_invisible_or_minimized(window)
{
// On Mac, a minimized Window uses up all CPU:
// https://github.com/emilk/egui/issues/325
// On Windows, an invisible window also uses up all CPU:
// https://github.com/emilk/egui/issues/7776
profiling::scope!("minimized_sleep");
std::thread::sleep(std::time::Duration::from_millis(10));
}
sleep_if_invisible_or_minimized(window.map(|window| window.as_ref()));
if integration.should_close() {
Ok(EventResult::CloseRequested)
@@ -960,6 +1029,25 @@ impl WgpuWinitRunning<'_> {
}
impl Viewport {
/// Apply the commands, or defer them until we have a window.
fn process_commands(
&mut self,
egui_ctx: &egui::Context,
mut commands: Vec<egui::ViewportCommand>,
) {
self.deferred_commands.append(&mut commands);
if let Some(window) = self.window.as_ref() {
egui_winit::process_viewport_commands(
egui_ctx,
&mut self.info,
core::mem::take(&mut self.deferred_commands),
window,
&mut self.actions_requested,
);
}
}
/// Create winit window, if needed.
fn initialize_window(
&mut self,
@@ -1125,8 +1213,11 @@ fn render_immediate_viewport(
} = &mut *shared_mut;
let Some(viewport) = viewports.get_mut(&ids.this) else {
warn!("Viewport disappeared unexpectedly!");
return;
};
viewport.pending_delta.append(textures_delta);
viewport.info.events.clear(); // they should have been processed
let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else {
return;
@@ -1149,7 +1240,7 @@ fn render_immediate_viewport(
pixels_per_point,
[0.0, 0.0, 0.0, 0.0],
&clipped_primitives,
&textures_delta,
&mut viewport.pending_delta,
vec![],
window,
);
@@ -1194,7 +1285,7 @@ fn handle_viewport_output(
class,
builder,
viewport_ui_cb,
mut commands,
commands,
repaint_delay: _, // ignored - we listened to the repaint callback instead
},
) in viewport_output.clone()
@@ -1204,30 +1295,23 @@ fn handle_viewport_output(
let viewport =
initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter);
if let Some(window) = viewport.window.as_ref() {
let old_inner_size = window.inner_size();
let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size());
viewport.deferred_commands.append(&mut commands);
viewport.process_commands(egui_ctx, commands);
egui_winit::process_viewport_commands(
egui_ctx,
&mut viewport.info,
std::mem::take(&mut viewport.deferred_commands),
window,
&mut viewport.actions_requested,
);
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux") {
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size
&& let (Some(width), Some(height)) = (
NonZeroU32::new(new_inner_size.width),
NonZeroU32::new(new_inner_size.height),
)
{
painter.on_window_resized(viewport_id, width, height);
}
// For Wayland : https://github.com/emilk/egui/issues/4196
if cfg!(target_os = "linux")
&& let Some(window) = viewport.window.as_ref()
&& let Some(old_inner_size) = old_inner_size
{
let new_inner_size = window.inner_size();
if new_inner_size != old_inner_size
&& let (Some(width), Some(height)) = (
NonZeroU32::new(new_inner_size.width),
NonZeroU32::new(new_inner_size.height),
)
{
painter.on_window_resized(viewport_id, width, height);
}
}
}
@@ -1268,6 +1352,7 @@ fn initialize_or_update_viewport<'a>(
viewport_ui_cb,
window: None,
egui_winit: None,
pending_delta: Default::default(),
})
}

View File

@@ -17,6 +17,18 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool {
window.is_visible() == Some(false) || window.is_minimized() == Some(true)
}
/// On Mac, a minimized window uses up all CPU:
/// <https://github.com/emilk/egui/issues/325>
///
/// On Windows, an invisible window also uses up all CPU:
/// <https://github.com/emilk/egui/issues/7776>
pub fn sleep_if_invisible_or_minimized(window: Option<&Window>) {
if window.is_some_and(is_invisible_or_minimized) {
profiling::scope!("minimized_sleep");
std::thread::sleep(core::time::Duration::from_millis(10));
}
}
/// Create an egui context, restoring it from storage if possible.
pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context {
profiling::function_scope!();

View File

@@ -280,52 +280,71 @@ impl AppRunner {
.and_then(|v| v.visible())
.unwrap_or(true);
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame);
if is_visible {
if is_visible {
let full_output = self.egui_ctx.run_ui(raw_input, |ui| {
self.app.logic(ui.ctx(), &mut self.frame);
self.app.ui(ui, &mut self.frame);
}
});
let egui::FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = full_output;
});
let egui::FullOutput {
platform_output,
textures_delta,
shapes,
pixels_per_point,
viewport_output,
} = full_output;
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
for (_viewport_id, viewport_output) in viewport_output {
for command in viewport_output.commands {
match command {
ViewportCommand::Screenshot(user_data) => {
self.screenshot_commands_with_frame_delay
.push((user_data, 1));
}
_ => {
// TODO(emilk): handle some of the commands
log::warn!(
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
);
}
}
if viewport_output.len() > 1 {
log::warn!("Multiple viewports not yet supported on the web");
}
}
self.handle_viewport_commands(
viewport_output
.into_values()
.flat_map(|viewport_output| viewport_output.commands),
);
self.handle_platform_output(platform_output);
if is_visible || !textures_delta.is_empty() {
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point));
} else {
// The tab is hidden, so we run no egui pass at all.
// That way all ui state is left untouched, and is still there
// when the tab is shown again.
let egui::LogicOutput {
platform_output,
viewport_commands,
} = self.egui_ctx.run_logic(&raw_input, |ctx| {
self.app.logic(ctx, &mut self.frame);
});
// No pass consumed the input, so save it for the next one:
self.input.raw.append(raw_input);
self.handle_viewport_commands(viewport_commands.into_values().flatten());
self.handle_platform_output(platform_output);
}
}
fn handle_viewport_commands(&mut self, commands: impl Iterator<Item = ViewportCommand>) {
for command in commands {
match command {
ViewportCommand::Screenshot(user_data) => {
self.screenshot_commands_with_frame_delay
.push((user_data, 1));
}
_ => {
// TODO(emilk): handle some of the commands
log::warn!(
"Unhandled egui viewport command: {command:?} - not implemented in web backend"
);
}
}
}
}
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self) {
let textures_delta = std::mem::take(&mut self.textures_delta);
let clipped_primitives = std::mem::take(&mut self.clipped_primitives);
let clipped_primitives = core::mem::take(&mut self.clipped_primitives);
if let Some(clipped_primitives) = clipped_primitives {
let mut screenshot_commands = vec![];
@@ -347,7 +366,7 @@ impl AppRunner {
self.app.clear_color(&self.egui_ctx.global_style().visuals),
&clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
&mut self.textures_delta,
screenshot_commands,
) {
log::error!("Failed to paint: {}", super::string_from_js_value(&err));
@@ -395,7 +414,10 @@ impl AppRunner {
if self.has_focus() {
// The eframe app has focus.
if ime.is_some() {
if let Some(ime) = ime {
if ime.should_interrupt_composition {
self.text_agent.interrupt_ime_composition();
}
// We are editing text: give the focus to the text agent.
self.text_agent.focus();
} else {
@@ -407,7 +429,7 @@ impl AppRunner {
if let Err(err) = self
.text_agent
.move_to(ime, self.canvas(), self.egui_ctx.zoom_factor())
.update(ime, self.canvas(), self.egui_ctx.zoom_factor())
{
log::error!(
"failed to update text agent position: {}",

View File

@@ -0,0 +1,45 @@
use core::{future::Future, pin::Pin};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub(crate) struct WebFile {
file: web_sys::File,
// We store a `PathBuf` here so that we can hand out `Path`s
// without allocating each time.
path: PathBuf,
}
impl From<web_sys::File> for WebFile {
fn from(file: web_sys::File) -> Self {
let path = file.name().into();
Self { file, path }
}
}
impl egui::DroppedFile for WebFile {
fn path(&self) -> &Path {
&self.path
}
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>> {
let file = self.file.clone();
Box::pin(async move {
if file.size() > f64::from(u32::MAX) {
return Err(format!(
"File is too large: browser file reads are limited to {} bytes",
u32::MAX
));
}
let array_buffer = file
.array_buffer()
.await
.map_err(|err| crate::web::string_from_js_value(&err))?;
Ok(js_sys::Uint8Array::new(&array_buffer).to_vec())
})
}
fn web_file(&self) -> Option<&web_sys::File> {
Some(&self.file)
}
}

View File

@@ -190,11 +190,6 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner)
return;
}
if event.is_composing() || event.key_code() == 229 {
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
return;
}
let modifiers = modifiers_from_kb_event(&event);
runner.input.set_modifiers(modifiers);
@@ -978,62 +973,25 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result
event.prevent_default();
})?;
runner_ref.add_event_listener(target, "drop", {
let runner_ref = runner_ref.clone();
runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
move |event: web_sys::DragEvent, runner| {
if let Some(data_transfer) = event.data_transfer() {
// TODO(https://github.com/emilk/egui/issues/3702): support dropping folders
runner.input.raw.hovered_files.clear();
runner.needs_repaint.repaint_asap();
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size());
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
let name = file.name();
let mime = file.type_();
let last_modified = std::time::UNIX_EPOCH
+ std::time::Duration::from_millis(file.last_modified() as u64);
log::debug!("Loading {:?} ({} bytes)…", name, file.size());
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
let runner_ref = runner_ref.clone();
let future = async move {
match future.await {
Ok(array_buffer) => {
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
log::debug!("Loaded {:?} ({} bytes).", name, bytes.len());
if let Some(mut runner_lock) = runner_ref.try_lock() {
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
mime,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.repaint_asap();
}
}
Err(err) => {
log::error!(
"Failed to read file: {}",
string_from_js_value(&err)
);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
runner.input.raw.dropped_files.push(std::sync::Arc::new(
super::dropped_file::WebFile::from(file),
));
}
}
event.stop_propagation();
event.prevent_default();
}
event.stop_propagation();
event.prevent_default();
}
})?;

View File

@@ -32,7 +32,7 @@ pub fn primary_touch_pos(
event: &web_sys::TouchEvent,
) -> Option<(egui::Pos2, web_sys::Touch)> {
// On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those:
let all_touches: Vec<_> = std::iter::chain(
let all_touches: Vec<_> = core::iter::chain(
(0..event.touches().length()).filter_map(|i| event.touches().get(i)),
(0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)),
)

View File

@@ -5,6 +5,7 @@
mod app_runner;
mod backend;
mod dropped_file;
mod events;
mod input;
mod panic_handler;
@@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) {
return;
}
let promise = window.navigator().clipboard().write_text(s);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
if let Err(err) = promise.await {
log::error!("Copy/cut action failed: {}", string_from_js_value(&err));
}
};
wasm_bindgen_futures::spawn_local(future);
js_sys::futures::spawn_local(future);
}
}
@@ -248,16 +248,15 @@ fn set_clipboard_image(image: &egui::ColorImage) {
};
let items = js_sys::Array::of1(&item);
let promise = window.navigator().clipboard().write(&items);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
if let Err(err) = promise.await {
log::error!(
"Copy/cut image action failed: {}",
string_from_js_value(&err)
);
}
};
wasm_bindgen_futures::spawn_local(future);
js_sys::futures::spawn_local(future);
}
}

View File

@@ -1,16 +1,16 @@
//! The text agent is a hidden `<input>` element used to capture
//! IME and mobile keyboard input events.
use std::cell::Cell;
use core::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use web_sys::Document;
use super::{AppRunner, WebRunner};
pub struct TextAgent {
input: web_sys::HtmlInputElement,
prev_ime_output: Cell<Option<egui::output::IMEOutput>>,
input_state: Rc<RefCell<InputState>>,
}
impl TextAgent {
@@ -19,7 +19,8 @@ impl TextAgent {
runner_ref: &WebRunner,
canvas: &web_sys::HtmlCanvasElement,
) -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
// create an `<input>` element
let input = document
@@ -27,11 +28,11 @@ impl TextAgent {
.dyn_into::<web_sys::HtmlInputElement>()?;
input.set_type("text");
input.set_attribute("autocapitalize", "off")?;
let input_state = Rc::new(RefCell::new(InputState::new(input.clone())));
// Hide the element, and park it over the canvas
// Hide the element, and park it over the top-left corner of the canvas
// so that focusing it can never scroll some other part
// of the page into view.
let canvas_rect = super::canvas_content_rect(canvas);
let style = input.style();
style.set_property("background-color", "transparent")?;
style.set_property("border", "none")?;
@@ -40,21 +41,22 @@ impl TextAgent {
style.set_property("height", "1px")?;
style.set_property("caret-color", "transparent")?;
style.set_property("position", "absolute")?;
style.set_property("top", &format!("{}px", canvas_rect.min.y))?;
style.set_property("left", &format!("{}px", canvas_rect.min.x))?;
style.set_property("top", &format!("{}px", canvas.offset_top()))?;
style.set_property("left", &format!("{}px", canvas.offset_left()))?;
// Prevent auto-zoom on mobile browsers (requires at least 16px).
style.set_property("font-size", "16px")?;
let root = canvas.get_root_node();
if root.has_type::<Document>() {
// root object is a document, append to its body
root.dyn_into::<Document>()?
.body()
.unwrap()
.append_child(&input)?;
} else {
// append input into root directly
root.append_child(&input)?;
// Insert the input as a sibling of the canvas, so that its
// `position: absolute` resolves against the same containing block
// as the canvas' `offset_top`/`offset_left`.
// This anchors the input to the canvas regardless of how the page
// is scrolled or how the canvas is embedded, and also works when
// the canvas is inside a shadow DOM.
if let Some(parent) = canvas.parent_node() {
parent.insert_before(&input, canvas.next_sibling().as_ref())?;
} else if let Some(body) = document.body() {
log::warn!("Canvas has no parent element - appending text agent to document body");
body.append_child(&input)?;
}
// Focus the app on startup, without scrolling the page.
@@ -66,152 +68,67 @@ impl TextAgent {
// attach event listeners
let on_input = {
let input = input.clone();
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
let text = input.value();
// Workaround for an Android Gboard issue: after typing a word,
// the user has to delete invisible characters (whose count
// matches the length of the current suggestion) before actual
// characters are deleted, unless the focus has been reset.
//
// this issue appears to have been fixed in Gboard sometime
// between versions 14.7.09 and 17.0.12.
if !event.is_composing() {
input.blur().ok();
super::focus_without_scroll(&input).ok();
}
if event.is_composing() {
// if `is_composing` is true, then user is using IME, for
// example: emoji, pinyin, kanji, hangul, etc. In that case,
// the browser emits both `input` and `compositionupdate`
// events.
// We handle the composition update here instead of in the
// `compositionupdate` event because the selection range
// has not yet been updated when `compositionupdate` fires.
let Some(text) = event.data() else { return };
let selection_start = input
.selection_start()
.unwrap_or(None)
.map(|pos| pos as usize);
let selection_end = input
.selection_end()
.unwrap_or(None)
.map(|pos| pos as usize);
let active_range_chars = if let Some(selection_start) = selection_start
&& let Some(selection_end) = selection_end
{
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
let text_before_selection =
String::from_utf16_lossy(&text_utf16[..selection_start]);
let text_in_selection =
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
let count_before_selection = text_before_selection.chars().count();
let count_in_selection = text_in_selection.chars().count();
Some(count_before_selection..count_before_selection + count_in_selection)
} else {
None
};
let event = egui::Event::Ime(egui::ImeEvent::Preedit {
text,
active_range_chars,
});
runner.input.raw.events.push(event);
} else {
if text.is_empty() {
return;
}
input.set_value("");
let event = egui::Event::Text(text);
runner.input.raw.events.push(event);
}
runner.needs_repaint.repaint_asap();
}
};
let on_composition_start = {
runner_ref.add_event_listener(
&input,
"compositionstart",
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
// Repaint moves the text agent into place,
// see `move_to` in `AppRunner::handle_platform_output`.
// see `AppRunner::handle_platform_output`, which calls
// `TextAgent::update`.
runner.needs_repaint.repaint_asap();
},
)?;
runner_ref.add_event_listener(&input, "input", {
let input_state = Rc::clone(&input_state);
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
input_state.borrow_mut().handle_input_event(&event, runner);
}
};
let on_composition_end = {
let input = input.clone();
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
let Some(text) = event.data() else { return };
input.set_value("");
let event = egui::Event::Ime(egui::ImeEvent::Commit(text));
runner.input.raw.events.push(event);
runner.needs_repaint.repaint_asap();
})?;
runner_ref.add_event_listener(&input, "compositionend", {
let input_state = Rc::clone(&input_state);
move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| {
input_state
.borrow_mut()
.handle_composition_end_event(runner);
}
};
})?;
runner_ref.add_event_listener(&input, "input", on_input)?;
runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?;
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
runner_ref.add_event_listener(&input, "keydown", {
let input_state = Rc::clone(&input_state);
move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| {
let is_consumed = InputState::handle_keydown_event(&input_state, &event);
if !is_consumed {
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
super::events::on_keydown(event, runner);
}
}
})?;
runner_ref.add_event_listener(&input, "keyup", {
let input_state = Rc::clone(&input_state);
move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| {
let is_consumed = InputState::handle_keyup_event(&input_state, &event);
if !is_consumed {
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
super::events::on_keyup(event, runner);
}
}
})?;
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?;
runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?;
Ok(Self {
input,
prev_ime_output: Default::default(),
})
Ok(Self { input, input_state })
}
pub fn move_to(
pub fn update(
&self,
ime: Option<egui::output::IMEOutput>,
canvas: &web_sys::HtmlCanvasElement,
zoom_factor: f32,
) -> Result<(), JsValue> {
// Don't move the text agent unless the position actually changed:
if self.prev_ime_output.get() == ime {
return Ok(());
}
self.prev_ime_output.set(ime);
let Some(ime) = ime else { return Ok(()) };
if ime.should_interrupt_composition {
// no-op for now: currently, the text agent is sizeless, so any
// click shifts focus to the canvas, which naturally interrupts the
// composition.
}
let mut canvas_rect = super::canvas_content_rect(canvas);
// Fix for safari with virtual keyboard flapping position
if is_mobile_safari() {
canvas_rect.min.y = canvas.offset_top() as f32;
}
let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2());
let style = self.input.style();
let native_ppp = super::native_pixels_per_point();
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
let logical_canvas_width = canvas.width() as f32 / native_ppp;
let visible_x = cursor_rect.center().x * zoom_factor;
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
let logical_canvas_height = canvas.height() as f32 / native_ppp;
let visible_y = cursor_rect.center().y * zoom_factor;
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
// This is where the IME input will point to:
style.set_property("left", &format!("{clamped_x}px"))?;
style.set_property("top", &format!("{clamped_y}px"))?;
Ok(())
self.input_state
.borrow_mut()
.update(ime, canvas, zoom_factor)
}
pub fn set_focus(&self, on: bool) {
@@ -248,6 +165,11 @@ impl TextAgent {
if let Err(err) = self.input.blur() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
}
self.input_state.borrow_mut().clear();
}
pub(crate) fn interrupt_ime_composition(&self) {
self.input_state.borrow_mut().clear();
}
}
@@ -257,15 +179,273 @@ impl Drop for TextAgent {
}
}
/// Returns `true` if the app is likely running on a mobile device on navigator Safari.
fn is_mobile_safari() -> bool {
(|| {
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
let is_ios = user_agent.contains("iPhone")
|| user_agent.contains("iPad")
|| user_agent.contains("iPod");
let is_safari = user_agent.contains("Safari");
Some(is_ios && is_safari)
})()
.unwrap_or(false)
struct InputState {
input: web_sys::HtmlInputElement,
last_text: String,
ime_output: Option<egui::output::IMEOutput>,
keydown_special_case: KeydownSpecialCase,
}
#[derive(Clone, Copy)]
enum KeydownSpecialCase {
None,
/// On Android Gboard 14.7.09, when suggestions remain visible while typing
/// letters without IME composition (e.g., Latin or Cyrillic), pressing
/// Backspace produces key code 229 instead of the expected Backspace key
/// code.
/// Without the workaround, users have to press Backspace twice before text
/// starts being deleted.
///
/// This workaround is also required for Android Gboard corrections and
/// completions (e.g., `tex|` -> `Texas`) to work correctly. In these
/// cases, a `deleteContentBackward` input event fires first (e.g., to
/// delete `tex`), followed by an `insertText` input event (e.g., to insert
/// `Texas`).
///
/// Since it is difficult to distinguish between a Backspace press and a
/// correction or completion (e.g., when the state is `t|`, it is unclear
/// whether the user wants to delete `t` or replace it with `Texas`), we
/// send a `DeleteSurrounding` IME event in all cases instead of
/// synthetically generating Backspace press and release events.
AndroidKeycode229,
/// iOS (18.6)'s built-in Korean keyboard uses `deleteContentBackward` to
/// compose Hangul characters. In these cases, the key code is 0.
IosKeycode0,
}
impl InputState {
fn new(input: web_sys::HtmlInputElement) -> Self {
Self {
input,
last_text: String::new(),
ime_output: None,
keydown_special_case: KeydownSpecialCase::None,
}
}
fn update(
&mut self,
ime: Option<egui::output::IMEOutput>,
canvas: &web_sys::HtmlCanvasElement,
zoom_factor: f32,
) -> Result<(), JsValue> {
// Don't move the text agent unless the position actually changed:
if self.ime_output == ime {
return Ok(());
}
self.ime_output = ime;
let Some(ime) = ime else { return Ok(()) };
// NOTE: we don't set the input's `type` to `password` based on
// `ime.purpose`, because that would confuse some password managers.
// For example, Chrome's password manager will always think the last
// letter typed in the password field is the password.
let style = self.input.style();
let native_ppp = super::native_pixels_per_point();
// The input is a sibling of the canvas (see `attach`), so we position
// it relative to the same containing block using the canvas offset.
// Unlike `get_bounding_client_rect`, the offset is unaffected by page
// scrolling, and doesn't flap when the virtual keyboard is shown on
// mobile Safari.
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
let logical_canvas_width = canvas.width() as f32 / native_ppp;
let visible_x = ime.cursor_rect.center().x * zoom_factor;
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
let logical_canvas_height = canvas.height() as f32 / native_ppp;
let visible_y = ime.cursor_rect.center().y * zoom_factor;
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
// This is where the IME input will point to:
style.set_property(
"left",
&format!("{}px", canvas.offset_left() as f32 + clamped_x),
)?;
style.set_property(
"top",
&format!("{}px", canvas.offset_top() as f32 + clamped_y),
)?;
Ok(())
}
fn clear(&mut self) {
self.input.set_value("");
self.last_text.clear();
}
fn handle_input_event(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
if self
.ime_output
.as_ref()
.is_some_and(|ime| ime.purpose == egui::IMEPurpose::Password)
{
self.handle_input_event_password(event, runner);
return;
}
let input_type = event.input_type();
if !event.is_composing()
&& input_type != "insertText"
// iOS uses this for corrections and completions (e.g., `tex|` ->
// `Texas`).
&& input_type != "insertReplacementText"
&& (matches!(self.keydown_special_case, KeydownSpecialCase::None)
|| input_type != "deleteContentBackward")
{
self.clear();
return;
}
let text = self.input.value();
let prefix_len = longest_common_prefix_length(&text, &self.last_text);
let last_text_len = self.last_text.chars().count();
if prefix_len < last_text_len {
let out_event = egui::Event::Ime(egui::ImeEvent::DeleteSurrounding {
before_chars: last_text_len - prefix_len,
after_chars: 0,
});
runner.input.raw.events.push(out_event);
}
let preedit_text: String = text.chars().skip(prefix_len).collect();
let out_event = if event.is_composing() {
// We handle the composition update here instead of in a
// `compositionupdate` event because the selection range
// has not yet been updated when `compositionupdate` fires.
let active_range_chars = self.active_range_chars(&text, prefix_len);
egui::Event::Ime(egui::ImeEvent::Preedit {
text: preedit_text,
active_range_chars,
})
} else {
egui::Event::Text(preedit_text)
};
runner.input.raw.events.push(out_event);
if event.is_composing() {
self.last_text = text.chars().take(prefix_len).collect();
} else {
self.last_text = text;
}
runner.needs_repaint.repaint_asap();
}
fn handle_input_event_password(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) {
let input_type = event.input_type();
if input_type != "insertText" {
return;
}
let text = self.input.value();
runner.input.raw.events.push(egui::Event::Text(text));
self.clear();
}
/// Compute the active range (cursor or conversion segment) within the
/// preedit text, based on the selection in the input element.
///
/// `text` is the full `input.value()`, and `prefix_len_chars` is the
/// number of chars at the start of `text` that are committed (not part
/// of the preedit). `selectionStart`/`selectionEnd` are UTF-16 offsets
/// within the full `input.value()`, so they are adjusted to be relative
/// to the preedit text.
fn active_range_chars(
&self,
text: &str,
prefix_len_chars: usize,
) -> Option<core::ops::Range<usize>> {
let selection_start = self.input.selection_start().unwrap_or(None)? as usize;
let selection_end = self.input.selection_end().unwrap_or(None)? as usize;
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
if selection_start > text_utf16.len() || selection_end > text_utf16.len() {
// This can occur on Android Chrome. see discussion in:
// <https://github.com/emilk/egui/pull/8045>.
return None;
}
let text_before_selection = String::from_utf16_lossy(&text_utf16[..selection_start]);
let text_in_selection =
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
let count_before_selection = text_before_selection.chars().count();
let count_in_selection = text_in_selection.chars().count();
// Adjust for the committed prefix to get the range within the preedit text.
let start = count_before_selection.saturating_sub(prefix_len_chars);
let end = start + count_in_selection;
Some(start..end)
}
fn handle_composition_end_event(&mut self, runner: &mut AppRunner) {
let text = self.input.value();
let commit_text = {
let prefix_len = self.last_text.chars().count();
text.chars().skip(prefix_len).collect::<String>()
};
let out_event = egui::Event::Ime(egui::ImeEvent::Commit(commit_text));
runner.input.raw.events.push(out_event);
self.last_text = text;
runner.needs_repaint.repaint_asap();
}
/// ## Returns
/// Whether the event is consumed. If `true`, the caller should not do
/// further processing for this event.
fn handle_keydown_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
// Platform-sniffing methods are unreliable, so they are not used as
// guards here.
let special_case = match event.key_code() {
229 => KeydownSpecialCase::AndroidKeycode229,
0 => KeydownSpecialCase::IosKeycode0,
_ => KeydownSpecialCase::None,
};
input_state.borrow_mut().keydown_special_case = special_case;
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
if event.is_composing() || !matches!(special_case, KeydownSpecialCase::None) {
true
} else {
if event.key().chars().count() > 1
|| event.ctrl_key()
|| event.alt_key()
|| event.meta_key()
{
input_state.borrow_mut().clear();
}
false
}
}
/// ## Returns
/// Whether the event is consumed. If `true`, the caller should not do
/// further processing for this event.
fn handle_keyup_event(input_state: &RefCell<Self>, event: &web_sys::KeyboardEvent) -> bool {
input_state.borrow_mut().keydown_special_case = KeydownSpecialCase::None;
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
event.is_composing() || event.key_code() == 229
}
}
fn longest_common_prefix_length(a: &str, b: &str) -> usize {
core::iter::zip(a.chars(), b.chars())
.take_while(|(a, b)| a == b)
.count()
}

View File

@@ -24,7 +24,7 @@ pub(crate) trait WebPainter {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue>;

View File

@@ -61,13 +61,16 @@ impl WebPainter for WebPainterGlow {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture: Vec<UserData>,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
for (id, image_delta) in &textures_delta.set {
self.painter.set_texture(*id, image_delta);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
self.painter.set_texture(id, &image_delta);
}
}
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
@@ -79,7 +82,8 @@ impl WebPainter for WebPainterGlow {
self.screenshots.push((image, capture));
}
for &id in &textures_delta.free {
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
self.painter.free_texture(id);
}

View File

@@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu {
clear_color: [f32; 4],
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
textures_delta: &mut egui::TexturesDelta,
capture_data: Vec<UserData>,
) -> Result<(), JsValue> {
let capture = !capture_data.is_empty();
@@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu {
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
renderer.update_texture(
&render_state.device,
&render_state.queue,
id,
&image_delta,
);
}
}
renderer.update_buffers(
@@ -365,7 +368,7 @@ impl WebPainter for WebPainterWgpu {
// Submit the commands: both the main buffer and user-defined ones.
render_state
.queue
.submit(std::iter::chain(user_cmd_bufs, [encoder.finish()]));
.submit(core::iter::chain(user_cmd_bufs, [encoder.finish()]));
if let Some((frame, capture_buffer)) = frame_and_capture_buffer {
if let Some(capture_buffer) = capture_buffer
@@ -388,8 +391,9 @@ impl WebPainter for WebPainterWgpu {
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
renderer.free_texture(&id);
}
}

View File

@@ -1,4 +1,5 @@
use std::{cell::RefCell, rc::Rc};
use core::cell::RefCell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
@@ -107,7 +108,7 @@ impl WebRunner {
fn unsubscribe_from_all_events(&self) {
let events_to_unsubscribe: Vec<_> =
std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
core::mem::take(&mut *self.events_to_unsubscribe.borrow_mut());
if !events_to_unsubscribe.is_empty() {
log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len());
@@ -139,7 +140,7 @@ impl WebRunner {
/// Returns `None` if there has been a panic, or if we have been destroyed.
/// In that case, just return to JS.
pub(crate) fn try_lock(&self) -> Option<std::cell::RefMut<'_, AppRunner>> {
pub(crate) fn try_lock(&self) -> Option<core::cell::RefMut<'_, AppRunner>> {
if self.panic_handler.has_panicked() {
// Unsubscribe from all events so that we don't get any more callbacks
// that will try to access the poisoned runner.
@@ -147,7 +148,7 @@ impl WebRunner {
None
} else {
let lock = self.app_runner.try_borrow_mut().ok()?;
std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
core::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() })
.ok()
}
}
@@ -158,9 +159,9 @@ impl WebRunner {
/// and return `None` if this runner has panicked.
pub fn app_mut<ConcreteApp: 'static + App>(
&self,
) -> Option<std::cell::RefMut<'_, ConcreteApp>> {
) -> Option<core::cell::RefMut<'_, ConcreteApp>> {
self.try_lock()
.map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
.map(|lock| core::cell::RefMut::map(lock, |runner| runner.app_mut::<ConcreteApp>()))
}
/// Convenience function to reduce boilerplate and ensure that all event handlers

View File

@@ -6,6 +6,16 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.36.1 - 2026-08-07
Nothing new
## 0.36.0 - 2026-08-05
* Upgrade wgpu to v30 [#8289](https://github.com/emilk/egui/pull/8289) by [@akx](https://github.com/akx)
* Fix: ensure mapped range is dropped before unmapping buffer in capture [#8337](https://github.com/emilk/egui/pull/8337) by [@MagicCrazyMan](https://github.com/MagicCrazyMan)
* Make wgpu Instance public [#8321](https://github.com/emilk/egui/pull/8321) by [@oleflb](https://github.com/oleflb)
## 0.35.0 - 2026-06-25
* Call `pre_present_notify` before presenting [#8089](https://github.com/emilk/egui/pull/8089) by [@dimtpap](https://github.com/dimtpap)
* Wgpu: Allow configuring VSync and frame latency at runtime [#8114](https://github.com/emilk/egui/pull/8114) by [@emilk](https://github.com/emilk)

View File

@@ -255,7 +255,7 @@ struct BufferPadding {
impl BufferPadding {
fn new(width: u32) -> Self {
let bytes_per_pixel = std::mem::size_of::<u32>() as u32;
let bytes_per_pixel = core::mem::size_of::<u32>() as u32;
let unpadded_bytes_per_row = width * bytes_per_pixel;
let padded_bytes_per_row =
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);

View File

@@ -98,20 +98,31 @@ fn vs_main(
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
@group(1) @binding(1) var r_tex_sampler: sampler;
/// 1 if the texture sampler uses nearest filtering, 0 if linear.
/// Only read when `predictable_texture_filtering` is on.
@group(1) @binding(2) var<uniform> r_tex_nearest_filtering: u32;
fn sample_texture(in: VertexOutput) -> vec4<f32> {
if r_locals.predictable_texture_filtering == 0 {
// Hardware filtering: fast, but varies across GPUs and drivers.
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
} else {
// Manual bilinear filtering with four taps at pixel centers using textureLoad
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
let texture_size_f = vec2<f32>(texture_size);
let max_coord = texture_size - vec2<i32>(1, 1);
if r_tex_nearest_filtering == 1 {
// Nearest filtering: load the texel under the sample position.
let texel = clamp(vec2<i32>(in.tex_coord * texture_size_f), vec2<i32>(0, 0), max_coord);
return textureLoad(r_tex_color, texel, 0);
}
// Manual bilinear filtering with four taps at pixel centers using textureLoad
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
let pixel_fract = fract(pixel_coord);
let pixel_floor = vec2<i32>(floor(pixel_coord));
// Manual texture clamping
let max_coord = texture_size - vec2<i32>(1, 1);
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);

View File

@@ -115,6 +115,9 @@ pub struct RenderState {
#[cfg(not(target_arch = "wasm32"))]
pub available_adapters: Vec<wgpu::Adapter>,
/// Wgpu instance used for creating surfaces and adapters.
pub instance: wgpu::Instance,
/// Wgpu device used for rendering, created from the adapter.
pub device: wgpu::Device,
@@ -218,7 +221,7 @@ impl RenderState {
instance.enumerate_adapters(backends).await
};
let (adapter, device, queue) = match config.wgpu_setup.clone() {
let (instance, adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew(WgpuSetupCreateNew {
instance_descriptor: _,
display_handle: _,
@@ -253,14 +256,14 @@ impl RenderState {
.await?
};
(adapter, device, queue)
(instance.clone(), adapter, device, queue)
}
WgpuSetup::Existing(WgpuSetupExisting {
instance: _,
instance,
adapter,
device,
queue,
}) => (adapter, device, queue),
}) => (instance, adapter, device, queue),
};
log_adapter_info(&adapter.get_info());
@@ -280,6 +283,7 @@ impl RenderState {
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
Ok(Self {
instance,
adapter,
#[cfg(not(target_arch = "wasm32"))]
available_adapters,
@@ -354,8 +358,8 @@ fn wgpu_config_impl_send_sync() {
assert_send_sync::<WgpuConfiguration>();
}
impl std::fmt::Debug for WgpuConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WgpuConfiguration {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self {
surface,
wgpu_setup,
@@ -482,7 +486,7 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
use std::fmt::Write as _;
use core::fmt::Write as _;
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");

View File

@@ -1,4 +1,5 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use core::{num::NonZeroU64, ops::Range};
use std::borrow::Cow;
use ahash::HashMap;
use bytemuck::Zeroable as _;
@@ -244,6 +245,12 @@ pub struct Renderer {
uniform_bind_group: wgpu::BindGroup,
texture_bind_group_layout: wgpu::BindGroupLayout,
/// Uniform buffers each holding a single `u32`:
/// 1 if the texture sampler uses nearest filtering, 0 otherwise.
/// Indexed by that flag value.
/// Read by the shader when `predictable_texture_filtering` is on.
nearest_filtering_flag_buffers: [wgpu::Buffer; 2],
/// Map of egui texture IDs to textures and their associated bindgroups (texture view +
/// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided
/// sampler.
@@ -299,7 +306,9 @@ impl Renderer {
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(std::mem::size_of::<UniformBuffer>() as _),
min_binding_size: NonZeroU64::new(
core::mem::size_of::<UniformBuffer>() as _
),
ty: wgpu::BufferBindingType::Uniform,
},
count: None,
@@ -344,10 +353,28 @@ impl Renderer {
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Buffer {
has_dynamic_offset: false,
min_binding_size: NonZeroU64::new(core::mem::size_of::<u32>() as _),
ty: wgpu::BufferBindingType::Uniform,
},
count: None,
},
],
})
};
let nearest_filtering_flag_buffers = [0_u32, 1_u32].map(|flag| {
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(&format!("egui_nearest_filtering_flag_{flag}")),
contents: bytemuck::bytes_of(&flag),
usage: wgpu::BufferUsages::UNIFORM,
})
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[
@@ -434,9 +461,9 @@ impl Renderer {
};
const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
(std::mem::size_of::<Vertex>() * 1024) as _;
(core::mem::size_of::<Vertex>() * 1024) as _;
const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress =
(std::mem::size_of::<u32>() * 1024 * 3) as _;
(core::mem::size_of::<u32>() * 1024 * 3) as _;
Self {
pipeline,
@@ -455,6 +482,7 @@ impl Renderer {
previous_uniform_buffer_content: UniformBuffer::zeroed(),
uniform_bind_group,
texture_bind_group_layout,
nearest_filtering_flag_buffers,
textures: HashMap::default(),
next_user_texture_id: 0,
samplers: HashMap::default(),
@@ -706,6 +734,8 @@ impl Renderer {
};
let bind_group = bind_group.unwrap_or_else(|| {
let nearest =
image_delta.options.magnification == epaint::textures::TextureFilter::Nearest;
let sampler = self
.samplers
.entry(image_delta.options)
@@ -724,6 +754,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
.as_entire_binding(),
},
],
})
});
@@ -826,6 +861,7 @@ impl Renderer {
) -> epaint::TextureId {
profiling::function_scope!();
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -843,6 +879,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
.as_entire_binding(),
},
],
});
@@ -882,6 +923,7 @@ impl Renderer {
.get_mut(&id)
.expect("Tried to update a texture that has not been allocated yet.");
let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest;
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: None,
..sampler_descriptor
@@ -899,6 +941,11 @@ impl Renderer {
binding: 1,
resource: wgpu::BindingResource::Sampler(&sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: self.nearest_filtering_flag_buffers[usize::from(nearest)]
.as_entire_binding(),
},
],
});
@@ -962,7 +1009,7 @@ impl Renderer {
self.index_buffer.slices.clear();
let required_index_buffer_size = (std::mem::size_of::<u32>() * index_count) as u64;
let required_index_buffer_size = (core::mem::size_of::<u32>() * index_count) as u64;
if self.index_buffer.capacity < required_index_buffer_size {
// Resize index buffer if needed.
self.index_buffer.capacity =
@@ -989,7 +1036,7 @@ impl Renderer {
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
match primitive {
Primitive::Mesh(mesh) => {
let size = mesh.indices.len() * std::mem::size_of::<u32>();
let size = mesh.indices.len() * core::mem::size_of::<u32>();
let slice = index_offset..(size + index_offset);
index_buffer_staging
.slice(slice.clone())
@@ -1006,7 +1053,8 @@ impl Renderer {
self.vertex_buffer.slices.clear();
let required_vertex_buffer_size = (std::mem::size_of::<Vertex>() * vertex_count) as u64;
let required_vertex_buffer_size =
(core::mem::size_of::<Vertex>() * vertex_count) as u64;
if self.vertex_buffer.capacity < required_vertex_buffer_size {
// Resize vertex buffer if needed.
self.vertex_buffer.capacity =
@@ -1034,7 +1082,7 @@ impl Renderer {
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs {
match primitive {
Primitive::Mesh(mesh) => {
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
let size = mesh.vertices.len() * core::mem::size_of::<Vertex>();
let slice = vertex_offset..(size + vertex_offset);
vertex_buffer_staging
.slice(slice.clone())

View File

@@ -9,7 +9,7 @@ use std::sync::Arc;
/// Automatically implemented for all types that satisfy the bounds
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
pub trait EguiDisplayHandle:
wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
wgpu::rwh::HasDisplayHandle + core::fmt::Debug + Send + Sync + 'static
{
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
@@ -27,7 +27,7 @@ impl Clone for Box<dyn EguiDisplayHandle> {
impl<T> EguiDisplayHandle for T
where
T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
T: wgpu::rwh::HasDisplayHandle + Clone + core::fmt::Debug + Send + Sync + 'static,
{
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
Box::new(self.clone())
@@ -77,8 +77,8 @@ impl WgpuSetup {
}
}
impl std::fmt::Debug for WgpuSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WgpuSetup {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::CreateNew(create_new) => f
.debug_tuple("WgpuSetup::CreateNew")
@@ -295,8 +295,8 @@ impl Clone for WgpuSetupCreateNew {
}
}
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self {
instance_descriptor,
display_handle,

View File

@@ -8,8 +8,9 @@ use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
};
use core::num::NonZeroU32;
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
use std::{num::NonZeroU32, sync::Arc};
use std::sync::Arc;
struct SurfaceState {
surface: wgpu::Surface<'static>,
@@ -478,7 +479,7 @@ impl Painter {
pixels_per_point: f32,
clear_color: [f32; 4],
clipped_primitives: &[epaint::ClippedPrimitive],
textures_delta: &epaint::textures::TexturesDelta,
textures_delta: &mut epaint::textures::TexturesDelta,
capture_data: Vec<UserData>,
window: &Arc<winit::window::Window>,
) -> f32 {
@@ -545,21 +546,6 @@ impl Painter {
commands_submitted: false,
};
{
// Upload textures before the surface-dependent early-returns below:
// uploads only need the device + queue, and the atlas dirty region is
// already consumed, so dropping the delta would desync the font texture.
let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set {
renderer.update_texture(
&render_state.device,
&render_state.queue,
*id,
image_delta,
);
}
}
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
return vsync_sec;
};
@@ -579,6 +565,18 @@ impl Painter {
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write();
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for (id, image_deltas) in textures_delta.set.drain() {
for image_delta in image_deltas {
renderer.update_texture(
&render_state.device,
&render_state.queue,
id,
&image_delta,
);
}
}
renderer.update_buffers(
&render_state.device,
&render_state.queue,
@@ -730,7 +728,7 @@ impl Painter {
let start = web_time::Instant::now();
render_state
.queue
.submit(std::iter::chain(user_cmd_bufs, [encoded]));
.submit(core::iter::chain(user_cmd_bufs, [encoded]));
vsync_sec += start.elapsed().as_secs_f32();
};
@@ -742,8 +740,9 @@ impl Painter {
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
{
let mut renderer = render_state.renderer.write();
for id in &textures_delta.free {
renderer.free_texture(id);
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
for id in textures_delta.free.drain() {
renderer.free_texture(&id);
}
}

View File

@@ -5,6 +5,14 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.36.1 - 2026-08-07
Nothing new
## 0.36.0 - 2026-08-05
Nothing new
## 0.35.0 - 2026-06-25
* Delegate handling of IME interruptions to integrations to fix virtual keyboard flickering on web [#8078](https://github.com/emilk/egui/pull/8078) by [@umajho](https://github.com/umajho)
* Always enable windows undecorated shadows [#8169](https://github.com/emilk/egui/pull/8169) by [@Wumpf](https://github.com/Wumpf)

View File

@@ -0,0 +1,22 @@
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub(crate) struct NativeFile {
path: PathBuf,
}
impl From<PathBuf> for NativeFile {
fn from(path: PathBuf) -> Self {
Self { path }
}
}
impl egui::DroppedFile for NativeFile {
fn path(&self) -> &Path {
&self.path
}
fn bytes(&self) -> Result<Vec<u8>, String> {
std::fs::read(&self.path).map_err(|err| err.to_string())
}
}

View File

@@ -21,6 +21,7 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId
pub use winit;
pub mod clipboard;
mod dropped_file;
mod safe_area;
mod window_settings;
@@ -28,6 +29,8 @@ pub use window_settings::WindowSettings;
use raw_window_handle::HasDisplayHandle;
use dropped_file::NativeFile;
use winit::{
dpi::{PhysicalPosition, PhysicalSize},
event::ElementState,
@@ -121,6 +124,7 @@ pub struct State {
allow_ime: bool,
ime_rect_px: Option<egui::Rect>,
old_ime_purpose: egui::IMEPurpose,
/// Used by [`State::try_on_ime_processed_keyboard_input`] to track key
/// release events that should be filtered out. See comments in that method
@@ -171,6 +175,7 @@ impl State {
allow_ime: false,
ime_rect_px: None,
old_ime_purpose: egui::IMEPurpose::Normal,
#[cfg(target_os = "windows")]
pressed_processed_physical_keys: HashSet::new(),
};
@@ -468,10 +473,9 @@ impl State {
}
WindowEvent::DroppedFile(path) => {
self.egui_input.hovered_files.clear();
self.egui_input.dropped_files.push(egui::DroppedFile {
path: Some(path.clone()),
..Default::default()
});
self.egui_input
.dropped_files
.push(std::sync::Arc::new(NativeFile::from(path.clone())));
EventResponse {
repaint: true,
consumed: false,
@@ -1158,6 +1162,11 @@ impl State {
window.set_ime_allowed(true);
}
if ime.purpose != self.old_ime_purpose {
self.old_ime_purpose = ime.purpose;
window.set_ime_purpose(to_winit_ime_purpose(ime.purpose));
}
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
let ime_rect_px = pixels_per_point * ime.rect;
if self.ime_rect_px != Some(ime_rect_px)
@@ -1880,11 +1889,7 @@ fn process_viewport_command(
);
}
ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v),
ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p {
egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password,
egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
}),
ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(to_winit_ime_purpose(p)),
ViewportCommand::Focus => {
if !window.has_focus() {
window.focus_window();
@@ -1945,6 +1950,14 @@ fn process_viewport_command(
}
}
fn to_winit_ime_purpose(purpose: egui::IMEPurpose) -> winit::window::ImePurpose {
match purpose {
egui::IMEPurpose::Password => winit::window::ImePurpose::Password,
egui::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal,
egui::IMEPurpose::Normal => winit::window::ImePurpose::Normal,
}
}
/// Build and intitlaize a window.
///
/// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`.

View File

@@ -19,6 +19,7 @@ workspace = true
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--generate-link-to-definition"]
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]
[lib]
@@ -96,3 +97,9 @@ document-features = { workspace = true, optional = true }
ron = { workspace = true, optional = true }
serde = { workspace = true, optional = true, features = ["derive", "rc"] }
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
# For `DroppedFile`, which hands web apps a file handle instead of its contents.
web-sys = { workspace = true, features = ["File"] }

View File

@@ -1,7 +1,7 @@
use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText};
use core::fmt::Debug;
use emath::Vec2;
use epaint::text::TextWrapMode;
use std::fmt::Debug;
/// Args passed when sizing an [`super::Atom`]
pub struct IntoSizedArgs {
@@ -90,7 +90,7 @@ impl Clone for AtomKind<'_> {
}
impl Debug for AtomKind<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AtomKind::Empty => write!(f, "AtomKind::Empty"),
AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"),

View File

@@ -2,11 +2,11 @@ use crate::{
AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense,
SizedAtom, SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState,
};
use core::ops::{Deref, DerefMut};
use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2};
use epaint::text::TextWrapMode;
use epaint::{Color32, Galley};
use smallvec::SmallVec;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
/// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y).
@@ -557,7 +557,7 @@ impl<'atom> SizedAtomLayout<'atom> {
F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
*kind = f(core::mem::take(kind));
}
}

View File

@@ -1,6 +1,6 @@
use crate::{Atom, AtomKind, Image, WidgetText};
use core::ops::{Deref, DerefMut};
use std::borrow::Cow;
use std::ops::{Deref, DerefMut};
/// A list of [`Atom`]s.
///
@@ -41,7 +41,7 @@ impl<'a> Atoms<'a> {
///
/// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead.
pub fn extend_left(&mut self, mut atoms: Self) {
std::mem::swap(&mut atoms.0, &mut self.0);
core::mem::swap(&mut atoms.0, &mut self.0);
self.0.extend(atoms.0);
}
@@ -128,7 +128,7 @@ impl<'a> Atoms<'a> {
pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) {
self.iter_mut()
.for_each(|atom| *atom = f(std::mem::take(atom)));
.for_each(|atom| *atom = f(core::mem::take(atom)));
}
pub fn map_kind<F>(&mut self, mut f: F)
@@ -136,7 +136,7 @@ impl<'a> Atoms<'a> {
F: FnMut(AtomKind<'a>) -> AtomKind<'a>,
{
for kind in self.iter_kinds_mut() {
*kind = f(std::mem::take(kind));
*kind = f(core::mem::take(kind));
}
}

View File

@@ -23,18 +23,18 @@ use super::CacheTrait;
/// ```
#[derive(Default)]
pub struct CacheStorage {
caches: ahash::HashMap<std::any::TypeId, Box<dyn CacheTrait>>,
caches: ahash::HashMap<core::any::TypeId, Box<dyn CacheTrait>>,
}
impl CacheStorage {
pub fn cache<Cache: CacheTrait + Default>(&mut self) -> &mut Cache {
let cache = self
.caches
.entry(std::any::TypeId::of::<Cache>())
.entry(core::any::TypeId::of::<Cache>())
.or_insert_with(|| Box::<Cache>::default());
#[expect(clippy::unwrap_used)]
(cache.as_mut() as &mut dyn std::any::Any)
(cache.as_mut() as &mut dyn core::any::Any)
.downcast_mut::<Cache>()
.unwrap()
}
@@ -60,8 +60,8 @@ impl Clone for CacheStorage {
}
}
impl std::fmt::Debug for CacheStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for CacheStorage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"FrameCacheStorage[{} caches with {} elements]",

View File

@@ -1,6 +1,6 @@
/// A cache, storing some value for some length of time.
#[expect(clippy::len_without_is_empty)]
pub trait CacheTrait: 'static + Send + Sync + std::any::Any {
pub trait CacheTrait: 'static + Send + Sync + core::any::Any {
/// Call once per frame to evict cache.
fn update(&mut self);

View File

@@ -48,7 +48,7 @@ impl<Value, Computer> FrameCache<Value, Computer> {
/// or recompute and store in the cache.
pub fn get<Key>(&mut self, key: Key) -> &Value
where
Key: Copy + std::hash::Hash,
Key: Copy + core::hash::Hash,
Computer: ComputerMut<Key, Value>,
{
let hash = crate::util::hash(key);

View File

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

View File

@@ -1,4 +1,4 @@
use std::fmt::Write as _;
use core::fmt::Write as _;
#[derive(Clone)]
struct Frame {
@@ -239,7 +239,7 @@ fn test_shorten_path() {
),
("/weird/path/file.rs", "/weird/path/file.rs"),
] {
use std::str::FromStr as _;
use core::str::FromStr as _;
let before = std::path::PathBuf::from_str(before).unwrap();
assert_eq!(shorten_source_file_path(&before), after);
}

View File

@@ -454,14 +454,12 @@ impl Area {
state.size = None;
}
state.pivot = pivot;
state.interactable = interactable;
if let Some(new_pos) = new_pos {
state.pivot_pos = Some(new_pos);
}
state.pivot_pos.get_or_insert_with(|| {
default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id))
});
state.interactable = interactable;
let size = *state.size.get_or_insert_with(|| {
sizing_pass = true;
@@ -484,6 +482,10 @@ impl Area {
size
});
// We should never be interactable during a sizing pass, since then we are shown at a different
// size which might interfere with hover state of the hovered widget causing popup feedback loops.
state.interactable = interactable && !sizing_pass;
// TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in
let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id));

View File

@@ -1,6 +1,6 @@
#[expect(unused_imports)]
use crate::{Ui, UiBuilder};
use std::sync::atomic::AtomicBool;
use core::sync::atomic::AtomicBool;
/// A tag to mark a container as closable.
///
@@ -18,11 +18,12 @@ impl ClosableTag {
/// Set close to `true`
pub fn set_close(&self) {
self.close.store(true, std::sync::atomic::Ordering::Relaxed);
self.close
.store(true, core::sync::atomic::Ordering::Relaxed);
}
/// Returns `true` if [`ClosableTag::set_close`] has been called.
pub fn should_close(&self) -> bool {
self.close.load(std::sync::atomic::Ordering::Relaxed)
self.close.load(core::sync::atomic::Ordering::Relaxed)
}
}

View File

@@ -342,7 +342,7 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) {
let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75);
let rect = rect.expand(visuals.expansion);
let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()];
use std::f32::consts::TAU;
use core::f32::consts::TAU;
let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0));
for p in &mut points {
*p = rect.center() + rotation * (*p - rect.center());

View File

@@ -143,12 +143,12 @@ pub struct Frame {
#[test]
fn frame_size() {
assert_eq!(
std::mem::size_of::<Frame>(),
core::mem::size_of::<Frame>(),
32,
"Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it."
);
assert!(
std::mem::size_of::<Frame>() <= 64,
core::mem::size_of::<Frame>() <= 64,
"Frame is getting way too big!"
);
}

View File

@@ -18,14 +18,24 @@
use emath::GuiRounding as _;
use crate::{
Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect,
Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, Margin, NumExt as _,
Order, Rangef, Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp,
};
fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 {
ctx.animate_bool_responsive(id, is_expanded)
}
/// [`Id`] of a panel's resize-handle widget.
///
/// A panel registers its handle under this same id whether it is open,
/// mid-slide, or fully collapsed — that is what lets one uninterrupted drag
/// collapse the panel and pull it back open. [`Panel::show_switched`] points
/// both of its panels at one shared handle the same way.
fn resize_widget_id(id_source: Id) -> Id {
id_source.with("__resize")
}
/// State regarding panels.
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
@@ -126,6 +136,22 @@ impl PanelSide {
}
}
/// The component of `margin` on the panel's _resizable_ edge,
/// i.e. the edge facing the rest of the ui, where the separator line goes.
fn resize_margin(self, mut margin: Margin) -> i8 {
*self.resize_margin_mut(&mut margin)
}
/// Mutable version of [`Self::resize_margin`].
fn resize_margin_mut(self, margin: &mut Margin) -> &mut i8 {
match self {
Self::Left => &mut margin.right,
Self::Right => &mut margin.left,
Self::Top => &mut margin.bottom,
Self::Bottom => &mut margin.top,
}
}
/// Resize by keeping `self` side fixed, and moving the opposite side.
fn set_rect_size(self, rect: &mut Rect, size: f32) {
match self {
@@ -182,6 +208,7 @@ pub struct Panel {
id: Id,
frame: Option<Frame>,
resizable: bool,
drag_to_open: bool,
show_separator_line: bool,
/// _Outer_ size (including [`Frame`] margin & border):
@@ -267,6 +294,7 @@ impl Panel {
id: id.into(),
frame: None,
resizable: true,
drag_to_open: true,
show_separator_line: true,
default_outer_size,
outer_size_range,
@@ -296,8 +324,39 @@ impl Panel {
self
}
/// Can a fully collapsed panel be dragged back open?
///
/// Default: `true`.
///
/// When enabled, a panel that [`Self::show_collapsible`] has collapsed all
/// the way still leaves a thin grab handle at its fixed edge. The handle is
/// invisible until hovered, at which point it lights up like a normal resize
/// handle. Dragging it outward past [`Self::min_size`] — or double-clicking
/// it — reopens the panel.
///
/// This is the counterpart to drag-to-collapse, and like it requires
/// [`Self::resizable`] to be `true`.
#[inline]
pub fn drag_to_open(mut self, drag_to_open: bool) -> Self {
self.drag_to_open = drag_to_open;
self
}
/// Show a separator line, even when not interacting with it?
///
/// The separator line sits on the panel's inner edge, i.e. the edge facing the rest of the ui.
/// It is painted _outside_ the [`Frame`]'s outline, in room the panel reserves for it in the
/// frame's [`Frame::outer_margin`], so that going from the panel contents outwards you get:
///
/// contents | [`Frame::inner_margin`] | [`Frame::stroke`] | separator line | [`Frame::outer_margin`]
///
/// Turning this off removes that reserved room too, so the panel gets no permanent gap along
/// that edge.
///
/// A `resizable` panel still shows a line while hovered or dragged, regardless of this setting.
/// With this setting off there is no room reserved for it, so that transient line is painted
/// just outside the frame's outline, overlapping the [`Frame::outer_margin`].
///
/// Default: `true`.
#[inline]
pub fn show_separator_line(mut self, show_separator_line: bool) -> Self {
@@ -386,6 +445,9 @@ impl Panel {
/// to `true` if the user drags the handle outward while the panel is closed.
/// When [`Self::resizable`] is `true`, double-clicking the resize edge also
/// flips `*is_expanded`.
///
/// A fully collapsed panel keeps a thin grab handle at its fixed edge, so the
/// user can drag it back open. See [`Self::drag_to_open`] to opt out.
pub fn show_collapsible<R>(
self,
ui: &mut Ui,
@@ -395,10 +457,11 @@ impl Panel {
let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded);
if how_expanded == 0.0 {
// Panel is fully closed. If the user is still dragging the resize handle
// from a previous frame, keep its widget id alive so they can drag the
// panel back out without releasing.
self.keep_drag_alive_for_reopen(ui, is_expanded);
// Panel is fully closed, but we still leave a grab handle at its fixed
// edge so the user can drag it back open.
if self.resizable && self.drag_to_open {
self.collapsed_resize_handle(ui, is_expanded);
}
// Make sure the ids of the next widgets are the same whether we show the panel or not:
ui.skip_ahead_auto_ids(1);
@@ -407,7 +470,7 @@ impl Panel {
// Don't lose the drag during the slide-back-open animation:
let drag_in_progress = ui
.read_response(self.id.with("__resize"))
.read_response(self.resize_id())
.is_some_and(|r| r.dragged());
let panel = if how_expanded < 1.0 {
@@ -520,20 +583,11 @@ impl Panel {
// Is the resize handle currently being dragged?
let drag_in_progress = ui
.read_response(resize_id_source.with("__resize"))
.read_response(resize_widget_id(resize_id_source))
.is_some_and(|r| r.dragged());
let animation_id = expanded_panel.id.with("animation");
// While the user is dragging, snap the animation to the target so the
// drag (which sets `outer_size` directly from the pointer) doesn't fight
// a simultaneous slide. Without this, drag-to-expand visibly jumps as
// the slide animation tries to grow from 0 while the pointer is already
// at the expanded size.
let how_expanded = if drag_in_progress {
ui.animate_bool_with_time(animation_id, *is_expanded, 0.0)
} else {
animate_expansion(ui, animation_id, *is_expanded)
};
let how_expanded = animate_expansion(ui, animation_id, *is_expanded);
// When expanding, the user sees the expanded content the moment animation starts.
// When collapsing, keep showing the expanded content until past the midpoint,
@@ -556,7 +610,19 @@ impl Panel {
let panel = if how_expanded < 1.0 {
// Animate the visible size from collapsed_size to expanded_size,
// so the slide picks up where the collapsed panel left off.
let expanded_size = expanded_panel.outer_size(ui);
let expanded_size = if drag_in_progress {
// During a drag the pointer sets the size, clamped to `min_size`
// — so that, not the (stale) persisted size, is where the slide
// meets the collapsed panel, whether opening or closing. Get it
// wrong and the panel jumps the gap between the two sizes in one
// frame.
expanded_panel
.outer_size_range
.min
.at_least(collapse_threshold)
} else {
expanded_panel.outer_size(ui)
};
let visible_size = lerp(collapse_threshold..=expanded_size, how_expanded);
let slide_fraction = if 0.0 < expanded_size {
visible_size / expanded_size
@@ -673,7 +739,7 @@ impl Panel {
// released size gets persisted into [`PanelState`] — without this the
// store-skipped-during-drag rule would leave the stored size at the
// pre-drag value.
let resize_id = self.resize_id_source.unwrap_or(id).with("__resize");
let resize_id = self.resize_id();
let resize_response = parent_ui.read_response(resize_id);
// Double-click on the resize edge toggles `*is_expanded` for the
@@ -831,21 +897,34 @@ impl Panel {
.store(parent_ui, id);
}
// Hide the separator once the panel is mostly slid off — at that point
// the line would just be a stray dash hovering near the parent edge.
if 0.01 < self.slide_fraction {
let stroke = if is_resizing {
parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover {
parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line {
// TODO(emilk): distinguish resizable from non-resizable
parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
} else {
Stroke::NONE
};
// The highlight follows the pointer all the way down to zero size, where
// `collapsed_resize_handle` picks it straight up again — so the user never
// loses sight of the edge they are dragging. The dim idle separator does
// get hidden once the panel is mostly slid off, since there it would just
// be a stray dash hovering near the parent edge.
let stroke = if is_resizing {
parent_ui.style().visuals.widgets.active.fg_stroke // highly visible
} else if resize_hover {
parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible
} else if show_separator_line && 0.01 < self.slide_fraction {
// TODO(emilk): distinguish resizable from non-resizable
parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim
} else {
Stroke::NONE
};
if 0.0 < stroke.width {
// Nudged inward, to keep the line inside the panel's own (shifted)
// rect: `parent_ui`'s painter sits below the panels that come after
// this one, so anything drawn past the fixed edge is covered by them.
// TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done
let line_pos = side.resize_pos(shifted_outer_rect) + 0.5 * side.sign() * stroke.width;
// The line goes just _outside_ the frame's outline, in the room `resolve_frame`
// reserved for it in the outer margin, i.e.:
//
// contents | `inner_margin` | outline | separator line | `outer_margin`
let outer_margin = f32::from(side.resize_margin(frame.outer_margin));
let outline_edge = side.resize_pos(shifted_outer_rect) + side.sign() * outer_margin;
let line_pos = outline_edge - 0.5 * side.sign() * stroke.width;
let cross_range = shifted_outer_rect.range_along(side.cross_axis());
if axis == 0 {
parent_ui.painter().vline(line_pos, cross_range, stroke);
@@ -857,54 +936,123 @@ impl Panel {
inner_response
}
/// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
fn resolve_frame(&self, ui: &Ui) -> Frame {
self.frame
.unwrap_or_else(|| Frame::side_top_panel(ui.style()))
/// [`Id`] of this panel's resize-handle widget.
///
/// See [`resize_widget_id`] for why open and collapsed panels must share it.
fn resize_id(&self) -> Id {
resize_widget_id(self.resize_id_source.unwrap_or(self.id))
}
/// Panel is fully closed. If the user is still dragging the resize handle
/// from the frame the panel closed on, keep its widget id registered so the
/// drag survives, and reopen if they drag back past the minimum size.
fn keep_drag_alive_for_reopen(&self, ui: &Ui, is_expanded: &mut bool) {
let resize_id = self.id.with("__resize");
let Some(resize_response) = ui.read_response(resize_id) else {
return;
};
if !resize_response.dragged() {
return;
}
let Some(pointer) = resize_response.interact_pointer_pos() else {
return;
};
/// The configured [`Frame`], or the default side/top panel frame for this [`Ui`].
fn resolve_frame(&self, ui: &Ui) -> Frame {
let mut frame = self
.frame
.unwrap_or_else(|| Frame::side_top_panel(ui.style()));
if self.show_separator_line {
// Reserve room for the separator line in the frame's _outer_ margin, so the line
// lands just outside the frame's outline instead of painting on top of it:
//
// contents | `inner_margin` | outline | separator line | `outer_margin`
//
// We deliberately don't do this for a `resizable` panel that has opted out of the
// separator line: the line it shows while hovered/dragged is a transient affordance,
// and reserving room for it would leave a permanently visible gap.
let widgets = &ui.style().visuals.widgets;
let stroke_width = widgets.noninteractive.bg_stroke.width.round() as i8;
let margin_side = self.side.resize_margin_mut(&mut frame.outer_margin);
*margin_side = (*margin_side).saturating_add(stroke_width);
}
frame
}
/// The grab handle of a fully collapsed panel: a thin strip along the panel's
/// fixed edge, invisible until hovered.
///
/// Dragging it outward past the minimum size — or double-clicking it —
/// reopens the panel. Registering it under the same id as the expanded
/// panel's resize handle also keeps an in-progress drag-to-collapse gesture
/// alive, so the user can drag the panel straight back out without releasing.
fn collapsed_resize_handle(&self, ui: &Ui, is_expanded: &mut bool) {
let side = self.side;
let axis = side.axis();
// Re-register the resize widget at the (now collapsed) fixed edge so its
// id stays alive in egui's interaction state.
let available_rect = ui.available_rect_before_wrap();
let fixed_edge_pos = self.side.fixed_pos(available_rect);
let cross_range = available_rect.range_along(self.side.cross_axis());
let resize_rect = if self.side.axis() == 0 {
let fixed_edge_pos = side.fixed_pos(available_rect);
let cross_range = available_rect.range_along(side.cross_axis());
// The strip lies just _inside_ the fixed edge, so it never reaches
// outside the area the panel is allowed to occupy.
let mut resize_rect = if axis == 0 {
Rect::from_x_y_ranges(Rangef::point(fixed_edge_pos), cross_range)
} else {
Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos))
};
let grab = ui.style().interaction.resize_grab_radius_side;
let resize_rect = resize_rect.expand2(grab * self.side.axis_unit());
ui.interact(resize_rect, resize_id, Sense::drag());
side.set_rect_size(
&mut resize_rect,
ui.style().interaction.resize_grab_radius_side,
);
// Keep the resize cursor while the user is still holding the drag.
// Otherwise the cursor would snap back to the default the moment the
// panel closed, even though the gesture is still ongoing.
ui.set_cursor_icon(self.cursor_icon(0.0));
let resize_id = self.resize_id();
let response = ui.interact(resize_rect, resize_id, Sense::click_and_drag());
// Signed distance from the fixed edge to the pointer along the panel's
// axis. Only counts as "pulled outward" while positive — going past the
// fixed edge gives a negative value, NOT a mirrored positive one (no
// `.abs()`), so dragging past the screen edge can't spuriously reopen.
let dragged_size = -self.side.sign() * (pointer[self.side.axis()] - fixed_edge_pos);
if self.outer_size_range.min < dragged_size {
if response.double_clicked() {
*is_expanded = true;
}
if response.hovered() || response.dragged() {
// Advertise that the panel can be pulled out. Also keeps the resize
// cursor for a drag that started before the panel closed, instead of
// snapping back to the default mid-gesture.
ui.set_cursor_icon(self.cursor_icon(0.0));
}
if response.dragged()
&& let Some(pointer) = response.interact_pointer_pos()
{
// Signed distance from the fixed edge to the pointer along the panel's
// axis. Only counts as "pulled outward" while positive — going past the
// fixed edge gives a negative value, NOT a mirrored positive one (no
// `.abs()`), so dragging past the screen edge can't spuriously reopen.
//
// We require the full minimum size, so the panel never jumps ahead of
// the pointer: it opens exactly when the drag reaches the size it will
// open at, and follows the pointer from there.
let dragged_size = -side.sign() * (pointer[axis] - fixed_edge_pos);
if self.outer_size_range.min < dragged_size {
*is_expanded = true;
}
}
// Invisible until hovered, so the handle doesn't read as a stray line at
// the edge of the screen.
let stroke = if response.dragged() {
ui.style().visuals.widgets.active.fg_stroke
} else if response.hovered() {
ui.style().visuals.widgets.hovered.fg_stroke
} else {
Stroke::NONE
};
if 0.0 < stroke.width {
// The collapsed panel occupies no space of its own, so the line has to
// go _inside_ the area the following panels use — which means painting
// in a layer above them, or they would cover it.
// TODO(emilk): use the panel's own layer once https://github.com/emilk/egui/issues/1516 is done
let painter = ui
.ctx()
.layer_painter(LayerId::new(Order::Middle, resize_id))
.with_clip_rect(resize_rect);
// Nudge the line inward so it isn't half-clipped by the edge.
let line_pos = fixed_edge_pos - 0.5 * side.sign() * stroke.width;
if axis == 0 {
painter.vline(line_pos, cross_range, stroke);
} else {
painter.hline(cross_range, line_pos, stroke);
}
}
}
/// Get the current _outer_ width or height of the panel (from previous frame),
@@ -939,7 +1087,7 @@ impl Panel {
// Use `resize_id_source` so collapsed/expanded panels in
// `show_switched` share one resize widget.
let resize_id = self.resize_id_source.unwrap_or(self.id).with("__resize");
let resize_id = self.resize_id();
let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount);
ui.interact(resize_rect, resize_id, Sense::click_and_drag())
}

View File

@@ -1,4 +1,4 @@
use std::iter::once;
use core::iter::once;
use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2};
@@ -179,7 +179,9 @@ pub struct Popup<'a> {
/// Default width passed to the Area
width: Option<f32>,
sizing_pass: bool,
sense: Sense,
interactable: bool,
layout: Layout,
frame: Option<Frame>,
style: StyleModifier,
@@ -201,7 +203,9 @@ impl<'a> Popup<'a> {
alternative_aligns: None,
gap: 0.0,
width: None,
sizing_pass: false,
sense: Sense::click(),
interactable: true,
layout: Layout::default(),
frame: None,
style: StyleModifier::default(),
@@ -369,6 +373,15 @@ impl<'a> Popup<'a> {
self
}
/// If `false`, the pointer goes straight through the popup and it's widgets to whatever is behind it.
///
/// Default: `true`.
#[inline]
pub fn interactable(mut self, interactable: bool) -> Self {
self.interactable = interactable;
self
}
/// Set the sense of the popup.
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
@@ -390,6 +403,19 @@ impl<'a> Popup<'a> {
self
}
/// Force the popup's underlying [`Area`] to run an invisible sizing pass.
///
/// Popups automatically run a sizing pass when they open or reopen. Set this to `true` for
/// one frame when the contents of an already open popup change and its cached size may no
/// longer fit. Do not leave it enabled continuously, because the popup would remain invisible.
///
/// Default: `false`.
#[inline]
pub fn sizing_pass(mut self, sizing_pass: bool) -> Self {
self.sizing_pass = sizing_pass;
self
}
/// Set the id of the Area.
#[inline]
pub fn id(mut self, id: Id) -> Self {
@@ -472,12 +498,12 @@ impl<'a> Popup<'a> {
RectAlign::find_best_align(
#[expect(clippy::iter_on_empty_collections)]
#[expect(clippy::or_fun_call)]
std::iter::chain(
core::iter::chain(
once(self.rect_align),
self.alternative_aligns
// Need the empty slice so the iters have the same type so we can unwrap_or
.map(|a| std::iter::chain(a.iter().copied(), [].iter().copied()))
.unwrap_or(std::iter::chain(
.map(|a| core::iter::chain(a.iter().copied(), [].iter().copied()))
.unwrap_or(core::iter::chain(
self.rect_align.symmetries().iter().copied(),
RectAlign::MENU_ALIGNS.iter().copied(),
)),
@@ -545,7 +571,9 @@ impl<'a> Popup<'a> {
alternative_aligns: _,
gap,
width,
sizing_pass,
sense,
interactable,
layout,
frame,
style,
@@ -570,8 +598,9 @@ impl<'a> Popup<'a> {
.pivot(pivot)
.fixed_pos(anchor)
.sense(sense)
.interactable(interactable)
.layout(layout)
.sizing_pass(!was_open_last_frame)
.sizing_pass(sizing_pass || !was_open_last_frame)
.info(info.unwrap_or_else(|| {
UiStackInfo::new(kind.into()).with_tag_value(
MenuConfig::MENU_CONFIG_TAG,

View File

@@ -289,16 +289,16 @@ impl Resize {
Rect::from_min_size(position, state.desired_size)
};
let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin);
let mut content_clip_rect = inner_rect;
// If we pull the resize handle to shrink, we want to TRY to shrink it.
// After laying out the contents, we might be much bigger.
// In those cases we don't want the clip_rect to be smaller, because
// then we will clip the contents of the region even thought the result gets larger. This is simply ugly!
// So we use the memory of last_content_size to make the clip rect large enough.
content_clip_rect.max = content_clip_rect.max.max(
inner_rect.min + state.last_content_size + Vec2::splat(ui.visuals().clip_rect_margin),
);
content_clip_rect.max = content_clip_rect
.max
.max(inner_rect.min + state.last_content_size);
content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region

View File

@@ -2,7 +2,7 @@
#![expect(clippy::needless_range_loop)]
use std::ops::{Add, AddAssign, BitOr, BitOrAssign};
use core::ops::{Add, AddAssign, BitOr, BitOrAssign};
use emath::GuiRounding as _;
use epaint::{Color32, Direction, Margin, Shape};
@@ -810,12 +810,11 @@ impl ScrollArea {
{
// Clip the content, but only when we really need to:
let clip_rect_margin = ui.visuals().clip_rect_margin;
let mut content_clip_rect = ui.clip_rect();
for d in 0..2 {
if direction_enabled[d] {
content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin;
content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin;
content_clip_rect.min[d] = inner_rect.min[d];
content_clip_rect.max[d] = inner_rect.max[d];
} else {
// Nice handling of forced resizing beyond the possible:
content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d];
@@ -931,7 +930,7 @@ impl ScrollArea {
let saved_scroll_target = content_ui
.ctx()
.pass_state_mut(|state| std::mem::take(&mut state.scroll_target));
.pass_state_mut(|state| core::mem::take(&mut state.scroll_target));
Prepared {
id,
@@ -986,7 +985,7 @@ impl ScrollArea {
ui: &mut Ui,
row_height_sans_spacing: f32,
total_rows: usize,
add_contents: impl FnOnce(&mut Ui, std::ops::Range<usize>) -> R,
add_contents: impl FnOnce(&mut Ui, core::ops::Range<usize>) -> R,
) -> ScrollAreaOutput<R> {
let spacing = ui.spacing().item_spacing;
let row_height_with_spacing = row_height_sans_spacing + spacing.y;
@@ -1082,17 +1081,9 @@ impl Prepared {
let content_size = content_ui.min_size();
let scroll_delta = content_ui
.ctx()
.pass_state_mut(|state| std::mem::take(&mut state.scroll_delta));
let mut had_explicit_scroll_adjustment = Vec2b::FALSE;
for d in 0..2 {
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
let mut delta = -scroll_delta.0[d];
let mut animation = scroll_delta.1;
// We always take both scroll targets regardless of which scroll axes are enabled. This
// is to avoid them leaking to other scroll areas.
let scroll_target = content_ui
@@ -1100,6 +1091,17 @@ impl Prepared {
.pass_state_mut(|state| state.scroll_target[d].take());
if direction_enabled[d] {
let (scroll_delta, scroll_animation) = content_ui.ctx().pass_state_mut(|state| {
(
core::mem::take(&mut state.scroll_delta.0[d]),
state.scroll_delta.1,
)
});
// PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it.
let mut delta = -scroll_delta;
let mut animation = scroll_animation;
if let Some(target) = scroll_target {
let pass_state::ScrollTarget {
range,
@@ -1133,8 +1135,8 @@ impl Prepared {
0.0
};
delta += delta_update;
animation = animation_update;
delta += delta_update;
}
if delta != 0.0 {
@@ -1158,10 +1160,10 @@ impl Prepared {
}
ui.request_repaint();
}
}
if delta != 0.0 {
had_explicit_scroll_adjustment[d] = true;
if delta != 0.0 {
had_explicit_scroll_adjustment[d] = true;
}
}
}
@@ -1306,8 +1308,6 @@ impl Prepared {
// * When one ScrollArea is nested inside another, and the outer
// is scrolled so that the scroll-bars of the inner ScrollArea (us)
// is outside the clip rectangle.
// Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that.
// clip_rect_margin is quite a hack. It would be nice to get rid of it.
max_cross = ui.clip_rect().max[1 - d] - outer_margin;
}
@@ -1575,9 +1575,7 @@ fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset:
let overflow = content_size - inner_rect.size();
let paint_rect = inner_rect
.intersect(ui.min_rect())
.expand(ui.visuals().clip_rect_margin);
let paint_rect = inner_rect.intersect(ui.min_rect());
// Top fade: animate opacity based on how far we've scrolled down.
if 0.0 < offset.y {

View File

@@ -129,7 +129,15 @@ impl Tooltip<'_> {
});
let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count);
popup = popup.anchor(state.bounding_rect).id(tooltip_area_id);
// Tooltips without interactive contents should not be interactable (hover should pass
// through to the widget below).
let interactable = Self::had_interactive_widgets(popup.ctx(), tooltip_area_id);
popup = popup
.anchor(state.bounding_rect)
.id(tooltip_area_id)
.interactable(interactable);
let response = popup.show(|ui| {
// By default, the text in tooltips aren't selectable.
@@ -192,6 +200,20 @@ impl Tooltip<'_> {
widget_id.with(tooltip_count)
}
/// Did this tooltip contain anything the user can interact with, last pass?
///
/// Most tooltips are just text. Those should not react to the pointer at all,
/// or they would steal the hover from the widget they belong to.
fn had_interactive_widgets(ctx: &Context, tooltip_id: Id) -> bool {
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
ctx.viewport(|vp| {
vp.prev_pass
.widgets
.get_layer(tooltip_layer_id)
.any(|w| w.enabled && w.sense.interactive())
})
}
/// Should we show a tooltip for this response?
///
/// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that
@@ -247,15 +269,9 @@ impl Tooltip<'_> {
// Check if we should automatically stay open:
let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id);
let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id);
let tooltip_has_interactive_widget = allow_interactive_tooltip
&& response.ctx.viewport(|vp| {
vp.prev_pass
.widgets
.get_layer(tooltip_layer_id)
.any(|w| w.enabled && w.sense.interactive())
});
&& Self::had_interactive_widgets(&response.ctx, tooltip_id);
if tooltip_has_interactive_widget {
// We keep the tooltip open if hovered,

View File

@@ -84,6 +84,7 @@ pub struct Window<'a> {
open: Option<&'a mut bool>,
area: Area,
frame: Option<Frame>,
title_frame: Option<Frame>,
resize: Resize,
scroll: ScrollArea,
collapsible: bool,
@@ -106,6 +107,7 @@ impl<'a> Window<'a> {
open: None,
area,
frame: None,
title_frame: None,
resize: Resize::default()
.with_stroke(false)
.min_size([96.0, 32.0])
@@ -265,6 +267,13 @@ impl<'a> Window<'a> {
self
}
/// Change the background color, margins, etc. of the title
#[inline]
pub fn title_frame(mut self, frame: Frame) -> Self {
self.title_frame = Some(frame);
self
}
/// Set minimum width of the window.
#[inline]
pub fn min_width(mut self, min_width: f32) -> Self {
@@ -549,6 +558,7 @@ impl Window<'_> {
mut open,
area,
frame,
title_frame,
resize,
scroll,
collapsible,
@@ -616,10 +626,12 @@ impl Window<'_> {
let style = ctx.global_style();
// We get or create the Frame for the title and content
let window_frame = frame.unwrap_or_else(|| Frame::window(&style));
let window_title_frame = title_frame.unwrap_or(window_frame);
// We apply the window margin by using the `ScrollArea::content_margin`.
let window_margin = window_frame.inner_margin;
let window_content_margin = window_frame.inner_margin;
let window_frame = window_frame.inner_margin(0.0);
let is_explicitly_closed = matches!(open, Some(false));
@@ -711,7 +723,7 @@ impl Window<'_> {
title_ui(
ui,
title,
window_frame.inner_margin(window_margin),
window_title_frame,
&mut collapsing,
collapsible,
on_top,
@@ -725,12 +737,12 @@ impl Window<'_> {
.show_body_unindented(ui, |ui| {
if scroll.is_any_scroll_enabled() {
scroll
.content_margin(window_margin)
.content_margin(window_content_margin)
.show(ui, add_contents)
.inner
} else {
crate::Frame::NONE
.inner_margin(window_margin)
.inner_margin(window_content_margin)
.show(ui, add_contents)
.inner
}
@@ -909,7 +921,7 @@ impl SideResponse {
}
}
impl std::ops::BitAnd for SideResponse {
impl core::ops::BitAnd for SideResponse {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
@@ -920,7 +932,7 @@ impl std::ops::BitAnd for SideResponse {
}
}
impl std::ops::BitOrAssign for SideResponse {
impl core::ops::BitOrAssign for SideResponse {
fn bitor_assign(&mut self, rhs: Self) {
*self = Self {
hover: self.hover || rhs.hover,

View File

@@ -1,6 +1,7 @@
#![warn(missing_docs)] // Let's keep `Context` well-documented.
use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};
use core::{cell::RefCell, panic::Location, time::Duration};
use std::{borrow::Cow, sync::Arc};
use emath::GuiRounding as _;
use epaint::{
@@ -32,7 +33,7 @@ use crate::{
load::{self, Bytes, Loaders, SizedTexture},
memory::{Options, Theme},
os::OperatingSystem,
output::FullOutput,
output::{FullOutput, LogicOutput},
pass_state::PassState,
plugin::{self, TypedPluginHandle},
resize, response, scroll_area, theme,
@@ -99,7 +100,7 @@ impl ContextImpl {
fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) {
let viewport = self.viewports.entry(viewport_id).or_default();
std::mem::swap(
core::mem::swap(
&mut viewport.repaint.prev_causes,
&mut viewport.repaint.causes,
);
@@ -244,6 +245,12 @@ pub struct ViewportState {
// ----------------------
// Cross-frame statistics:
pub num_multipass_in_row: usize,
/// The last theme we sent to the native window via [`ViewportCommand::SetTheme`],
/// used to avoid sending redundant commands.
///
/// See [`crate::Options::sync_window_theme`].
pub(crate) last_sent_window_theme: Option<crate::SystemTheme>,
}
/// What called [`Context::request_repaint`] or [`Context::request_discard`]?
@@ -259,14 +266,14 @@ pub struct RepaintCause {
pub reason: Cow<'static, str>,
}
impl std::fmt::Debug for RepaintCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for RepaintCause {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}:{} {}", self.file, self.line, self.reason)
}
}
impl std::fmt::Display for RepaintCause {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for RepaintCause {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}:{} {}", self.file, self.line, self.reason)
}
}
@@ -456,7 +463,7 @@ impl ContextImpl {
self.memory.begin_pass(&new_raw_input, &all_viewport_ids);
viewport.input = std::mem::take(&mut viewport.input).begin_pass(
viewport.input = core::mem::take(&mut viewport.input).begin_pass(
new_raw_input,
viewport.repaint.requested_immediate_repaint_prev_pass(),
pixels_per_point,
@@ -469,7 +476,13 @@ impl ContextImpl {
viewport.this_pass.begin_pass();
{
let mut layers: Vec<LayerId> = viewport.prev_pass.widgets.layer_ids().collect();
// Areas that are not interactable are click-through: skip them in the hit-test.
let mut layers: Vec<LayerId> = viewport
.prev_pass
.widgets
.layer_ids()
.filter(|layer_id| self.memory.areas().is_interactable(*layer_id))
.collect();
layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b));
viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() {
@@ -644,7 +657,7 @@ impl ContextImpl {
}
fn all_viewport_ids(&self) -> ViewportIdSet {
std::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect()
core::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect()
}
/// The current active viewport
@@ -712,13 +725,13 @@ impl ContextImpl {
#[derive(Clone)]
pub struct Context(Arc<RwLock<ContextImpl>>);
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Context {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Context").finish_non_exhaustive()
}
}
impl std::cmp::PartialEq for Context {
impl core::cmp::PartialEq for Context {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
@@ -728,7 +741,7 @@ impl Default for Context {
fn default() -> Self {
let ctx_impl = ContextImpl {
embed_viewports: true,
viewports: std::iter::once((ViewportId::ROOT, ViewportState::default())).collect(),
viewports: core::iter::once((ViewportId::ROOT, ViewportState::default())).collect(),
..Default::default()
};
let ctx = Self(Arc::new(RwLock::new(ctx_impl)));
@@ -778,6 +791,7 @@ impl Context {
/// ui.label("Hello egui!");
/// });
/// // handle full_output
/// # full_output.drop_without_applying_deltas();
/// ```
#[must_use]
pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput {
@@ -837,7 +851,7 @@ impl Context {
self.write(|ctx| {
let viewport = ctx.viewport_for(viewport_id);
viewport.output.num_completed_passes =
std::mem::take(&mut output.platform_output.num_completed_passes);
core::mem::take(&mut output.platform_output.num_completed_passes);
output.platform_output.request_discard_reasons.clear();
});
@@ -878,6 +892,57 @@ impl Context {
output
}
/// Run app logic without showing any ui.
///
/// Use this instead of [`Self::run_ui`] when nothing will be shown,
/// e.g. because the window is minimized or occluded,
/// but you still want to let the app tick its logic
/// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]).
///
/// No pass is run, so `f` must not show any ui.
/// This means everything egui knows about the ui is left untouched:
/// no widget state is garbage-collected, no animation advances,
/// and nothing loses focus.
///
/// Of `new_input`, only the window state ([`RawInput::viewports`] and
/// [`RawInput::focused`]) is used, so that `f` can tell that the window is hidden.
/// The ui input (events, time, …) is _not_ interpreted, and is left for the next
/// call to [`Self::run_ui`]: [`Self::input`] is otherwise still that of the last pass.
///
/// The returned [`LogicOutput`] is what [`FullOutput`] would have carried:
/// anything `f` asked the integration to do.
/// There is nothing to paint.
#[must_use]
pub fn run_logic(&self, new_input: &RawInput, logic: impl FnOnce(&Self)) -> LogicOutput {
profiling::function_scope!();
let viewport_id = new_input.viewport_id;
self.write(|ctx| {
// Consume any outstanding repaint request, so that a new request from `logic`
// reaches the integration instead of being considered already served:
ctx.begin_pass_repaint_logic(viewport_id);
// Tell `logic` about the windows, but leave the ui input alone:
let raw = &mut ctx.viewport_for(viewport_id).input.raw;
raw.viewport_id = viewport_id;
raw.viewports = new_input.viewports.clone();
raw.focused = new_input.focused;
});
logic(self);
self.write(|ctx| LogicOutput {
platform_output: core::mem::take(&mut ctx.viewport_for(viewport_id).output),
viewport_commands: ctx
.viewports
.iter_mut()
.filter(|(_, viewport)| !viewport.commands.is_empty())
.map(|(&id, viewport)| (id, core::mem::take(&mut viewport.commands)))
.collect(),
})
}
/// An alternative to calling [`Self::run_ui`].
///
/// It is usually better to use [`Self::run_ui`], because
@@ -895,6 +960,7 @@ impl Context {
///
/// let full_output = ctx.end_pass();
/// // handle full_output
/// # full_output.drop_without_applying_deltas();
/// ```
pub fn begin_pass(&self, mut new_input: RawInput) {
profiling::function_scope!();
@@ -1698,11 +1764,11 @@ impl Context {
.get(&id)
.map(|v| v.repaint.cumulative_frame_nr)
.unwrap_or_else(|| {
if cfg!(debug_assertions) {
panic!("cumulative_frame_nr_for failed to find the viewport {id:?}");
} else {
0
}
debug_assert!(
false,
"cumulative_frame_nr_for failed to find the viewport {id:?}"
);
0
})
})
}
@@ -1813,7 +1879,7 @@ impl Context {
/// See [`Self::request_repaint_after`] for details.
#[track_caller]
pub fn request_repaint_after_secs(&self, seconds: f32) {
if let Ok(duration) = std::time::Duration::try_from_secs_f32(seconds) {
if let Ok(duration) = core::time::Duration::try_from_secs_f32(seconds) {
self.request_repaint_after(duration);
}
}
@@ -1996,7 +2062,7 @@ impl Context {
&self,
f: impl FnOnce(&mut T) -> R,
) -> Option<R> {
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
let plugin = self.read(|ctx| ctx.plugins.get(core::any::TypeId::of::<T>()));
plugin.map(|plugin| f(plugin.lock().typed_plugin_mut()))
}
@@ -2008,13 +2074,13 @@ impl Context {
if let Some(plugin) = self.plugin_opt() {
plugin
} else {
panic!("Plugin of type {:?} not found", std::any::type_name::<T>());
panic!("Plugin of type {:?} not found", core::any::type_name::<T>());
}
}
/// Get a handle to the plugin of type `T`, if it was registered.
pub fn plugin_opt<T: plugin::Plugin>(&self) -> Option<TypedPluginHandle<T>> {
let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::<T>()));
let plugin = self.read(|ctx| ctx.plugins.get(core::any::TypeId::of::<T>()));
plugin.map(TypedPluginHandle::new)
}
@@ -2430,6 +2496,8 @@ impl Context {
}
}
self.sync_window_theme();
#[cfg(debug_assertions)]
self.debug_painting();
@@ -2441,11 +2509,43 @@ impl Context {
output
}
/// Keep the native window theme in sync with the egui [`crate::ThemePreference`],
/// if [`crate::Options::sync_window_theme`] is enabled.
///
/// Sends a [`ViewportCommand::SetTheme`] to the current viewport whenever the
/// derived theme changes, so the native window decorations match the egui theme.
fn sync_window_theme(&self) {
if !self.options(|o| o.sync_window_theme) {
return;
}
use crate::{SystemTheme, ThemePreference};
let window_theme = match self.options(|o| o.theme_preference) {
ThemePreference::System => SystemTheme::SystemDefault,
ThemePreference::Dark => SystemTheme::Dark,
ThemePreference::Light => SystemTheme::Light,
};
let changed = self.write(|ctx| {
let viewport = ctx.viewport();
if viewport.last_sent_window_theme == Some(window_theme) {
false
} else {
viewport.last_sent_window_theme = Some(window_theme);
true
}
});
if changed {
self.send_viewport_cmd(ViewportCommand::SetTheme(window_theme));
}
}
/// Called at the end of the pass.
#[cfg(debug_assertions)]
fn debug_painting(&self) {
#![expect(clippy::iter_over_hash_type)] // ok to be sloppy in debug painting
use std::fmt::Write as _;
use core::fmt::Write as _;
let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| {
let rect = widget.interact_rect;
@@ -2626,7 +2726,7 @@ impl ContextImpl {
// Inform the backend of all textures that have been updated (including font atlas).
let textures_delta = self.tex_manager.0.write().take_delta();
let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output);
let mut platform_output: PlatformOutput = core::mem::take(&mut viewport.output);
if self.memory.should_interrupt_ime()
&& let Some(ime) = &mut platform_output.ime
@@ -2686,7 +2786,7 @@ impl ContextImpl {
shapes
};
std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
core::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass);
if repaint_needed {
self.request_repaint(ended_viewport_id, RepaintCause::new());
@@ -2748,7 +2848,7 @@ impl ContextImpl {
// Let the primary immediate viewport handle the commands of its children too.
// This can make things easier for the backend, as otherwise we may get commands
// that affect a viewport while its egui logic is running.
std::mem::take(&mut viewport.commands)
core::mem::take(&mut viewport.commands)
} else {
vec![]
};
@@ -4233,13 +4333,13 @@ fn warn_if_rect_changes_id(
struct OrderedRect(Rect);
impl PartialOrd for OrderedRect {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for OrderedRect {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
let lhs = self.0;
let rhs = other.0;
lhs.min
@@ -4341,6 +4441,7 @@ mod test {
assert_eq!(num_calls, 1);
assert_eq!(output.platform_output.num_completed_passes, 1);
assert!(!output.platform_output.requested_discard());
output.drop_without_applying_deltas();
}
// A single call, with a denied request to discard:
@@ -4366,6 +4467,7 @@ mod test {
.reason,
"test"
);
output.drop_without_applying_deltas();
}
}
@@ -4386,6 +4488,7 @@ mod test {
assert_eq!(num_calls, 1);
assert_eq!(output.platform_output.num_completed_passes, 1);
assert!(!output.platform_output.requested_discard());
output.drop_without_applying_deltas();
}
// Request discard once:
@@ -4408,6 +4511,7 @@ mod test {
!output.platform_output.requested_discard(),
"The request should have been cleared when fulfilled"
);
output.drop_without_applying_deltas();
}
// Request discard twice:
@@ -4432,6 +4536,7 @@ mod test {
output.platform_output.requested_discard(),
"The unfulfilled request should be reported"
);
output.drop_without_applying_deltas();
}
}
@@ -4460,6 +4565,7 @@ mod test {
!output.platform_output.requested_discard(),
"The request should have been cleared when fulfilled"
);
output.drop_without_applying_deltas();
}
}
}

View File

@@ -1,19 +1,51 @@
use std::{path::Path, sync::Arc};
#[cfg(target_arch = "wasm32")]
use core::{future::Future, pin::Pin};
/// A file dropped into egui.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct DroppedFile {
/// Set by the `egui-winit` backend.
pub path: Option<std::path::PathBuf>,
///
/// The integration owns the concrete file handle, letting egui remain independent of windowing
/// backends and file APIs.
pub trait DroppedFile: core::fmt::Debug {
/// The path of the dropped file.
///
/// This is an absolute path on native platforms. On the web, it is a relative path containing
/// only the file name because browsers do not expose the file's local path.
fn path(&self) -> &Path;
/// Name of the file. Set by the `eframe` web backend.
pub name: String,
/// Read the file contents.
///
/// This is asynchronous because browsers can only read files asynchronously.
///
/// # Errors
///
/// Returns an error if the browser cannot read the file.
#[cfg(target_arch = "wasm32")]
fn bytes_async(&self) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, String>> + '_>>;
/// With the `eframe` web backend, this is set to the mime-type of the file (if available).
pub mime: String,
/// Read the file contents.
///
/// # Errors
///
/// Returns an error if the file cannot be read.
#[cfg(not(target_arch = "wasm32"))]
fn bytes(&self) -> Result<Vec<u8>, String>;
/// Set by the `eframe` web backend.
pub last_modified: Option<std::time::SystemTime>,
/// Set by the `eframe` web backend.
pub bytes: Option<std::sync::Arc<[u8]>>,
/// The browser file handle, if this file was dropped on the web.
#[cfg(target_arch = "wasm32")]
fn web_file(&self) -> Option<&web_sys::File> {
None
}
}
/// A shared reference to a dropped file.
#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))]
pub type DroppedFileHandle = Arc<dyn DroppedFile + Send + Sync>;
/// A shared reference to a dropped file.
///
/// This is not necessarily `Send + Sync` when wasm threads are enabled, because
/// [`web_sys::File`] is not thread-safe in that configuration.
#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
pub type DroppedFileHandle = Arc<dyn DroppedFile>;

View File

@@ -14,7 +14,7 @@ pub enum ImeEvent {
/// a non-empty preedit string indicates that the IME is active.
Preedit {
text: String,
active_range_chars: Option<std::ops::Range<usize>>,
active_range_chars: Option<core::ops::Range<usize>>,
},
/// IME composition ended with this final result.
@@ -22,6 +22,15 @@ pub enum ImeEvent {
/// The IME is considered dismissed after this event.
Commit(String),
/// Notifies when the text surrounding the cursor should be deleted.
///
/// `before_chars` and `after_chars` are the number of characters (not
/// bytes) to delete before and after the cursor, respectively.
DeleteSurrounding {
before_chars: usize,
after_chars: usize,
},
/// Notifies when the IME was disabled.
#[deprecated = "No longer used by egui"]
Disabled,

View File

@@ -16,7 +16,7 @@ mod touch;
mod viewport_info;
pub use self::{
dropped_file::DroppedFile,
dropped_file::{DroppedFile, DroppedFileHandle},
event::Event,
event_filter::EventFilter,
hovered_file::HoveredFile,

View File

@@ -37,8 +37,8 @@ pub struct Modifiers {
pub command: bool,
}
impl std::fmt::Debug for Modifiers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Modifiers {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.is_none() {
return write!(f, "Modifiers::NONE");
}
@@ -387,7 +387,7 @@ impl Modifiers {
}
}
impl std::ops::BitOr for Modifiers {
impl core::ops::BitOr for Modifiers {
type Output = Self;
#[inline]
@@ -396,7 +396,7 @@ impl std::ops::BitOr for Modifiers {
}
}
impl std::ops::BitOrAssign for Modifiers {
impl core::ops::BitOrAssign for Modifiers {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
*self = *self | rhs;

View File

@@ -1,6 +1,6 @@
use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect};
use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
use super::{DroppedFileHandle, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
/// What the integrations provides to egui at the start of each frame.
///
@@ -13,7 +13,7 @@ use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo};
///
/// Ii "points" can be calculated from native physical pixels
/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct RawInput {
/// The id of the active viewport.
@@ -65,9 +65,20 @@ pub struct RawInput {
/// Dragged files dropped into egui.
///
/// egui never reads the file contents.
#[cfg_attr(
not(target_arch = "wasm32"),
doc = "Call [`crate::DroppedFile::bytes`] to read a dropped file."
)]
#[cfg_attr(
target_arch = "wasm32",
doc = "Call [`crate::DroppedFile::bytes_async`] to read a dropped file."
)]
///
/// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has
/// been disabled in [`crate::viewport::ViewportBuilder`].
pub dropped_files: Vec<DroppedFile>,
#[cfg_attr(feature = "serde", serde(skip))]
pub dropped_files: Vec<DroppedFileHandle>,
/// The native window has the keyboard focus (i.e. is receiving key presses).
///
@@ -84,7 +95,7 @@ impl Default for RawInput {
fn default() -> Self {
Self {
viewport_id: ViewportId::ROOT,
viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(),
viewports: core::iter::once((ViewportId::ROOT, Default::default())).collect(),
screen_rect: None,
max_texture_side: None,
time: None,
@@ -123,9 +134,9 @@ impl RawInput {
max_texture_side: self.max_texture_side.take(),
time: self.time,
predicted_dt: self.predicted_dt,
events: std::mem::take(&mut self.events),
events: core::mem::take(&mut self.events),
hovered_files: self.hovered_files.clone(),
dropped_files: std::mem::take(&mut self.dropped_files),
dropped_files: core::mem::take(&mut self.dropped_files),
focused: self.focused,
system_theme: self.system_theme,
}

View File

@@ -10,7 +10,7 @@ use crate::emath::Rect;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct SafeAreaInsets(pub MarginF32);
impl std::ops::Sub<SafeAreaInsets> for Rect {
impl core::ops::Sub<SafeAreaInsets> for Rect {
type Output = Self;
fn sub(self, rhs: SafeAreaInsets) -> Self::Output {

View File

@@ -117,7 +117,7 @@ impl ViewportInfo {
Self {
parent: self.parent,
title: self.title.clone(),
events: std::mem::take(&mut self.events),
events: core::mem::take(&mut self.events),
native_pixels_per_point: self.native_pixels_per_point,
monitor_size: self.monitor_size,
inner_rect: self.inner_rect,
@@ -209,7 +209,7 @@ impl ViewportInfo {
}
#[expect(clippy::ref_option)]
fn opt_as_str<T: std::fmt::Debug>(v: &Option<T>) -> String {
fn opt_as_str<T: core::fmt::Debug>(v: &Option<T>) -> String {
v.as_ref().map_or(String::new(), |v| format!("{v:?}"))
}
});

View File

@@ -1,6 +1,6 @@
//! All the data egui returns to the backend at the end of each frame.
use std::ops::Range;
use core::ops::Range;
use epaint::text::CharIndex;
@@ -16,7 +16,7 @@ pub struct FullOutput {
/// Texture changes since last frame (including the font texture).
///
/// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting,
/// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting,
/// and free any texture in [`crate::TexturesDelta::free`] _after_ painting.
///
/// It is assumed that all egui viewports share the same painter and texture namespace.
@@ -68,6 +68,28 @@ impl FullOutput {
}
}
}
/// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas,
/// this is a helper to clear the deltas.
pub fn drop_without_applying_deltas(mut self) {
self.textures_delta.clear();
}
}
/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown.
///
/// There is nothing to paint, but the app may still have asked the integration to do things,
/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`].
#[derive(Clone, Default)]
pub struct LogicOutput {
/// Non-rendering related output.
pub platform_output: PlatformOutput,
/// The commands sent with [`crate::Context::send_viewport_cmd`] and friends.
///
/// Note that this contains no information about which viewports exist:
/// the integration should leave its viewports as they are.
pub viewport_commands: OrderedViewportIdMap<Vec<crate::ViewportCommand>>,
}
/// Information about text being edited.
@@ -76,6 +98,9 @@ impl FullOutput {
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct IMEOutput {
/// IME's purpose.
pub purpose: crate::IMEPurpose,
/// Where the [`crate::TextEdit`] is located on screen.
pub rect: crate::Rect,
@@ -217,7 +242,7 @@ impl PlatformOutput {
/// Take everything ephemeral (everything except `cursor_icon` and
/// `cursor_image` currently)
pub fn take(&mut self) -> Self {
let taken = std::mem::take(self);
let taken = core::mem::take(self);
self.cursor_icon = taken.cursor_icon; // sticky between frames
self.cursor_image = taken.cursor_image.clone(); // sticky between frames
taken
@@ -302,8 +327,8 @@ pub struct CustomCursorImage {
pub hotspot: [u16; 2],
}
impl std::fmt::Debug for CustomCursorImage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for CustomCursorImage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("CustomCursorImage")
.field("size", &self.size)
.field("hotspot", &self.hotspot)
@@ -519,8 +544,8 @@ impl OutputEvent {
}
}
impl std::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for OutputEvent {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Clicked(wi) => write!(f, "Clicked({wi:?})"),
Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"),
@@ -566,8 +591,8 @@ pub struct WidgetInfo {
pub hint_text: Option<String>,
}
impl std::fmt::Debug for WidgetInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WidgetInfo {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self {
typ,
enabled,

View File

@@ -1,4 +1,5 @@
use std::{any::Any, sync::Arc};
use core::any::Any;
use std::sync::Arc;
/// A wrapper around `dyn Any`, used for passing custom user data
/// to [`crate::ViewportCommand::Screenshot`].
@@ -30,8 +31,8 @@ impl PartialEq for UserData {
impl Eq for UserData {}
impl std::hash::Hash for UserData {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl core::hash::Hash for UserData {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.data.as_ref().map(Arc::as_ptr).hash(state);
}
}
@@ -57,7 +58,7 @@ impl<'de> serde::Deserialize<'de> for UserData {
impl serde::de::Visitor<'_> for UserDataVisitor {
type Value = UserData;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str("a None value")
}

View File

@@ -26,7 +26,7 @@ pub fn print(ctx: &Context, text: impl Into<WidgetText>) {
return;
}
let location = std::panic::Location::caller();
let location = core::panic::Location::caller();
let location = format!("{}:{}", location.file(), location.line());
let plugin = ctx.plugin::<DebugTextPlugin>();
@@ -58,7 +58,7 @@ impl Plugin for DebugTextPlugin {
}
fn on_end_pass(&mut self, ui: &mut Ui) {
let entries = std::mem::take(&mut self.entries);
let entries = core::mem::take(&mut self.entries);
Self::paint_entries(ui, entries);
}
}

View File

@@ -1,4 +1,5 @@
use std::{any::Any, sync::Arc};
use core::any::Any;
use std::sync::Arc;
use crate::{Context, CursorIcon, Plugin, Ui};

View File

@@ -75,6 +75,11 @@ pub(crate) struct GridLayout {
curr_state: State,
initial_available: Rect,
/// Are we inside an enclosing sizing pass (e.g. [`crate::Resize`] measuring
/// the minimum content width)? If so we must not remember the (narrow) sizes
/// we measure during it.
sizing_pass: bool,
// Options:
num_columns: Option<usize>,
spacing: Vec2,
@@ -90,6 +95,10 @@ pub(crate) struct GridLayout {
impl GridLayout {
pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option<State>) -> Self {
let is_first_frame = prev_state.is_none();
// An outer sizing pass, we should render as small as possible.
let sizing_pass = ui.is_sizing_pass();
let prev_state = prev_state.unwrap_or_default();
// TODO(emilk): respect current layout
@@ -110,6 +119,7 @@ impl GridLayout {
prev_state,
curr_state: State::default(),
initial_available,
sizing_pass,
num_columns: None,
spacing: ui.spacing().item_spacing,
@@ -180,7 +190,11 @@ impl GridLayout {
}
pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect {
let width = self.prev_state.col_width(self.col).unwrap_or(0.0);
let width = if self.sizing_pass {
0.0
} else {
self.prev_state.col_width(self.col).unwrap_or(0.0)
};
let height = self.prev_row_height(self.row);
let size = child_size.max(vec2(width, height));
Rect::from_min_size(cursor.min, size).round_ui()

View File

@@ -1,6 +1,6 @@
// TODO(emilk): have separate types `PositionId` and `UniqueId`. ?
use std::num::NonZeroU64;
use core::num::NonZeroU64;
use crate::{AsIdSalt, IdSalt};
@@ -8,9 +8,9 @@ use crate::{AsIdSalt, IdSalt};
///
/// This is all types implementing `Hash` and `Debug`,
/// which includes things like string, integers, tuples of those, etc.
pub trait AsId: std::hash::Hash + std::fmt::Debug {}
pub trait AsId: core::hash::Hash + core::fmt::Debug {}
impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
impl<T: core::hash::Hash + core::fmt::Debug> AsId for T {}
/// egui tracks widgets frame-to-frame using [`Id`]s.
///
@@ -41,6 +41,13 @@ impl<T: std::hash::Hash + std::fmt::Debug> AsId for T {}
/// This is niche-optimized to that `Option<Id>` is the same size as `Id`.
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(
feature = "serde",
expect(
clippy::unsafe_derive_deserialize,
reason = "`from_high_entropy_bits` is only `unsafe` about entropy, not memory safety"
)
)]
pub struct Id(NonZeroU64);
impl nohash_hasher::IsEnabled for Id {}
@@ -75,7 +82,7 @@ impl Id {
/// Generate a child [`Id`] by salting the parent [`Id`] with the given argument.
pub fn with(self, salt: impl AsIdSalt) -> Self {
use std::hash::{BuildHasher as _, Hasher as _};
use core::hash::{BuildHasher as _, Hasher as _};
let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher();
hasher.write_u64(self.value());
hasher.write_u64(IdSalt::new(&salt).value());
@@ -124,8 +131,8 @@ impl Id {
}
}
impl std::fmt::Debug for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Id {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if *self == Self::NULL {
return write!(f, "Id::NULL");
}
@@ -204,8 +211,8 @@ mod id_source {
#[test]
fn id_size() {
assert_eq!(std::mem::size_of::<Id>(), 8);
assert_eq!(std::mem::size_of::<Option<Id>>(), 8);
assert_eq!(core::mem::size_of::<Id>(), 8);
assert_eq!(core::mem::size_of::<Option<Id>>(), 8);
}
#[cfg(test)]

View File

@@ -1,12 +1,12 @@
use std::num::NonZeroU64;
use core::num::NonZeroU64;
/// Types that can be converted to an [`IdSalt`].
///
/// This is all types implementing `Hash` and `Debug`,
/// which includes things like string, integers, tuples of those, etc.
pub trait AsIdSalt: std::hash::Hash + std::fmt::Debug {}
pub trait AsIdSalt: core::hash::Hash + core::fmt::Debug {}
impl<T: std::hash::Hash + std::fmt::Debug> AsIdSalt for T {}
impl<T: core::hash::Hash + core::fmt::Debug> AsIdSalt for T {}
/// Uniquely identifies a child widget within a parent widget.
///
@@ -57,8 +57,8 @@ impl IdSalt {
}
}
impl std::fmt::Debug for IdSalt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for IdSalt {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
#[cfg(debug_assertions)]
if let Some(source) = id_salt_source::get(*self) {
return write!(f, "IdSalt::new({source})");

View File

@@ -13,10 +13,8 @@ use crate::{
},
input_state::wheel_state::WheelState,
};
use std::{
collections::{BTreeMap, HashSet},
time::Duration,
};
use core::time::Duration;
use std::collections::{BTreeMap, HashSet};
pub use crate::Key;
pub use touch_state::MultiTouchInfo;

View File

@@ -1,4 +1,5 @@
use std::{collections::BTreeMap, fmt::Debug};
use core::fmt::Debug;
use std::collections::BTreeMap;
use crate::{
Event, RawInput, TouchId, TouchPhase,
@@ -305,7 +306,7 @@ impl TouchState {
impl Debug for TouchState {
// This outputs less clutter than `#[derive(Debug)]`:
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for (id, touch) in &self.active_touches {
f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?;
}

View File

@@ -197,7 +197,24 @@ pub(crate) fn interact(
// This widget is sensitive to both clicks and drags.
// When the mouse first is pressed, it could be either,
// so we postpone the decision until we know.
input.pointer.is_decidedly_dragging()
//
// …unless the pointer has left the widget: a click has to be
// released on the widget, so once the pointer is outside there is
// nothing left to wait for.
//
// Deciding here means a thin drag handle (narrower than
// `max_click_dist`) doesn't spend the decision window as neither
// hovered nor dragged, which would make its highlight blink out.
// The hit-test picks up widgets within `interact_radius`, so
// `hits.click` can name such a handle even when the pointer is a
// few points outside it.
//
// A widget on top might "steal" the click hit, but then the pointer is still inside
// us, and pressing that button must not start a drag. So we check both.
let pointer_is_inside = hits.contains_pointer.iter().any(|w| w.id == widget.id);
let could_still_be_clicked =
pointer_is_inside || hits.click.is_some_and(|hit| hit.id == widget.id);
input.pointer.is_decidedly_dragging() || !could_still_be_clicked
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.senses_drag()
@@ -262,7 +279,7 @@ pub(crate) fn interact(
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);
let top_interactive_order = click_order.max(drag_order);
let mut hovered: IdSet = std::iter::chain(&hits.click, &hits.drag)
let mut hovered: IdSet = core::iter::chain(&hits.click, &hits.drag)
.map(|w| w.id)
.collect();

View File

@@ -96,8 +96,8 @@ impl LayerId {
}
}
impl std::fmt::Debug for LayerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for LayerId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self { order, id } = self;
write!(f, "LayerId {{ {order:?} {id:?} }}")
}

View File

@@ -1,4 +1,4 @@
use emath::GuiRounding as _;
use emath::{GuiRounding as _, fast_midpoint};
use crate::{
Align, Direction,
@@ -477,12 +477,12 @@ impl Layout {
// Make sure it isn't negative:
if avail.max.x < avail.min.x {
let x = 0.5 * (avail.min.x + avail.max.x);
let x = fast_midpoint(avail.min.x, avail.max.x);
avail.min.x = x;
avail.max.x = x;
}
if avail.max.y < avail.min.y {
let y = 0.5 * (avail.min.y + avail.max.y);
let y = fast_midpoint(avail.min.y, avail.max.y);
avail.min.y = y;
avail.max.y = y;
}

View File

@@ -474,7 +474,7 @@ pub use self::{
Key, UserData,
input::*,
output::{
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand,
self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand,
PlatformOutput, UserAttentionType, WidgetInfo,
},
},
@@ -680,18 +680,20 @@ pub enum WidgetType {
pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| {
let output = ctx.run_ui(Default::default(), |ui| {
run_ui(ui.ctx());
});
output.drop_without_applying_deltas();
}
/// For use in tests; especially doctests.
pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) {
let ctx = Context::default();
ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time)
let _ = ctx.run_ui(Default::default(), |ui| {
let output = ctx.run_ui(Default::default(), |ui| {
add_contents(ui);
});
output.drop_without_applying_deltas();
}
pub fn accesskit_root_id() -> Id {

View File

@@ -55,12 +55,11 @@
mod bytes_loader;
mod texture_loader;
use std::{
borrow::Cow,
use core::{
fmt::{Debug, Display},
ops::Deref,
sync::Arc,
};
use std::{borrow::Cow, sync::Arc};
use ahash::HashMap;
@@ -108,13 +107,13 @@ impl LoadError {
detected_format.as_ref().map_or(0, |s| s.len())
}
Self::Loading(message) => message.len(),
_ => std::mem::size_of::<Self>(),
_ => core::mem::size_of::<Self>(),
}
}
}
impl Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NoImageLoaders => f.write_str(
"No image loaders are installed. If you're trying to load some images \
@@ -136,9 +135,9 @@ impl Display for LoadError {
}
}
impl std::error::Error for LoadError {}
impl core::error::Error for LoadError {}
pub type Result<T, E = LoadError> = std::result::Result<T, E>;
pub type Result<T, E = LoadError> = core::result::Result<T, E>;
/// Given as a hint for image loading requests.
///
@@ -209,7 +208,7 @@ pub enum Bytes {
}
impl Debug for Bytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(),
Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(),
@@ -307,6 +306,19 @@ macro_rules! generate_loader_id {
}
pub use crate::generate_loader_id;
/// Does the given URI end with the given file extension?
///
/// The comparison ignores ASCII case and any `#fragment` at the end of the URI,
/// so `has_extension("cat.GIF#frame=2", "gif")` is `true`.
///
/// This is useful when implementing an [`ImageLoader`].
pub fn has_extension(uri: &str, extension: &str) -> bool {
let path = uri.split('#').next().unwrap_or(uri);
std::path::Path::new(path)
.extension()
.is_some_and(|found| found.eq_ignore_ascii_case(extension))
}
pub type BytesLoadResult = Result<BytesPoll>;
/// Represents a loader capable of loading raw unstructured bytes from somewhere,
@@ -387,7 +399,7 @@ pub type ImageLoadResult = Result<ImagePoll>;
/// An `ImageLoader` decodes raw bytes into a [`ColorImage`].
///
/// Implementations are expected to cache at least each `URI`.
pub trait ImageLoader: std::any::Any {
pub trait ImageLoader: core::any::Any {
/// Unique ID of this loader.
///
/// To reduce the chance of collisions, include `module_path!()` as part of this ID.
@@ -640,3 +652,14 @@ impl Loaders {
}
}
}
#[test]
fn test_has_extension() {
assert!(has_extension("cat.svg", "svg"));
assert!(has_extension("cat.SVG", "svg"));
assert!(has_extension("http://example.com/cat.gif#frame=2", "gif"));
assert!(!has_extension("cat.svg.png", "svg"));
assert!(!has_extension("svg", "svg"));
assert!(!has_extension("cat.jpeg", "jpg"));
assert!(!has_extension("cat.svg?v=1", "svg"));
}

View File

@@ -150,5 +150,5 @@ impl TextureLoader for DefaultTextureLoader {
}
fn is_svg(uri: &str) -> bool {
uri.ends_with(".svg")
super::has_extension(uri, "svg")
}

View File

@@ -1,6 +1,6 @@
#![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs
use std::num::NonZeroUsize;
use core::num::NonZeroUsize;
use ahash::{HashMap, HashSet};
use epaint::emath::TSTransform;
@@ -216,6 +216,18 @@ pub struct Options {
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) system_theme: Option<Theme>,
/// If `true`, egui will keep the native window theme in sync with
/// [`Self::theme_preference`] by sending a [`crate::ViewportCommand::SetTheme`]
/// to the root viewport whenever the preference changes.
///
/// This makes the native window decorations (title bar, borders, …) match the
/// theme selected inside egui.
///
/// Set this to `false` if you want to manage the native window theme yourself.
///
/// This is `true` by default.
pub sync_window_theme: bool,
/// Global zoom factor of the UI.
///
/// This is used to calculate the `pixels_per_point`
@@ -318,6 +330,7 @@ impl Default for Options {
theme_preference: Default::default(),
fallback_theme: Theme::Dark,
system_theme: None,
sync_window_theme: true,
zoom_factor: 1.0,
zoom_with_keyboard: true,
quit_shortcuts: vec![crate::KeyboardShortcut::new(
@@ -381,6 +394,7 @@ impl Options {
theme_preference,
fallback_theme: _,
system_theme: _,
sync_window_theme,
zoom_factor,
zoom_with_keyboard,
quit_shortcuts: _, // not shown in ui
@@ -429,6 +443,8 @@ impl Options {
.show(ui, |ui| {
theme_preference.radio_buttons(ui);
ui.checkbox(sync_window_theme, "Sync window theme with egui theme");
let style = std::sync::Arc::make_mut(match theme {
Theme::Dark => dark_style,
Theme::Light => light_style,
@@ -926,7 +942,7 @@ impl Memory {
if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) {
matches!(
self.areas().compare_order(layer_id, modal_layer),
std::cmp::Ordering::Equal | std::cmp::Ordering::Greater
core::cmp::Ordering::Equal | core::cmp::Ordering::Greater
)
} else {
true
@@ -966,7 +982,7 @@ impl Memory {
if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame)
&& matches!(
self.areas().compare_order(layer_id, current),
std::cmp::Ordering::Less
core::cmp::Ordering::Less
)
{
return;
@@ -1194,6 +1210,11 @@ impl Areas {
self.areas.get_mut(&id)
}
/// Can the user interact with this layer or it's widgets, or do clicks go straight through it?
pub(crate) fn is_interactable(&self, layer_id: LayerId) -> bool {
self.get(layer_id.id).is_none_or(|area| area.interactable)
}
/// All layers back-to-front, top is last.
pub(crate) fn order(&self) -> &[LayerId] {
&self.order
@@ -1202,12 +1223,12 @@ impl Areas {
/// Compare the order of two layers, based on the order list from last frame.
///
/// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list.
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering {
pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> core::cmp::Ordering {
// Sort by layer `order` first and use `order_map` to resolve disputes.
// If `order_map` only contains one layer ID, then the other one will be
// lower because `None < Some(x)`.
match a.order.cmp(&b.order) {
std::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)),
core::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)),
cmp => cmp,
}
}
@@ -1255,7 +1276,7 @@ impl Areas {
}
pub fn visible_layer_ids(&self) -> ahash::HashSet<LayerId> {
std::iter::chain(
core::iter::chain(
&self.visible_areas_last_frame,
&self.visible_areas_current_frame,
)
@@ -1344,7 +1365,7 @@ impl Areas {
..
} = self;
std::mem::swap(visible_areas_last_frame, visible_areas_current_frame);
core::mem::swap(visible_areas_last_frame, visible_areas_current_frame);
visible_areas_current_frame.clear();
order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer)));
@@ -1353,7 +1374,7 @@ impl Areas {
// For all layers with sublayers, put the sublayers directly after the parent layer:
// (it doesn't matter in which order we replace parents with their children)
#[expect(clippy::iter_over_hash_type)]
for (parent, children) in std::mem::take(sublayers) {
for (parent, children) in core::mem::take(sublayers) {
let mut moved_layers = vec![parent]; // parent first…
order.retain(|l| {
@@ -1462,14 +1483,14 @@ fn order_map_total_ordering() {
let mut i = 0;
for &[a, b] in layers.array_windows() {
assert!(a.order <= b.order, "does not follow LayerId.order");
if areas.compare_order(a, b) != std::cmp::Ordering::Equal {
if areas.compare_order(a, b) != core::cmp::Ordering::Equal {
i += 1;
}
equivalence_classes.push(i);
}
assert_eq!(layers.len(), equivalence_classes.len());
for (&l1, c1) in std::iter::zip(&layers, &equivalence_classes) {
for (&l2, c2) in std::iter::zip(&layers, &equivalence_classes) {
for (&l1, c1) in core::iter::zip(&layers, &equivalence_classes) {
for (&l2, c2) in core::iter::zip(&layers, &equivalence_classes) {
assert_eq!(
c1.cmp(c2),
areas.compare_order(l1, l2),

View File

@@ -280,7 +280,7 @@ impl Painter {
);
}
pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect {
pub fn error(&self, pos: Pos2, text: impl core::fmt::Display) -> Rect {
let color = self.ctx.global_style().visuals.error_fg_color;
self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}"))
}
@@ -416,7 +416,7 @@ impl Painter {
/// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`.
pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into<Stroke>) {
use crate::emath::Rot2;
let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0);
let rot = Rot2::from_angle(core::f32::consts::TAU / 10.0);
let tip_length = vec.length() / 4.0;
let tip = origin + vec;
let dir = vec.normalized();

View File

@@ -10,7 +10,7 @@ use std::sync::Arc;
/// Plugins should not hold a reference to the [`Context`], since this would create a cycle
/// (which would prevent the [`Context`] from being dropped).
#[expect(unused_variables)]
pub trait Plugin: Send + Sync + std::any::Any + 'static {
pub trait Plugin: Send + Sync + core::any::Any + 'static {
/// Plugin name.
///
/// Used when profiling.
@@ -60,14 +60,14 @@ pub(crate) struct PluginHandle {
/// Use [`Self::lock`] to access the plugin.
pub struct TypedPluginHandle<P: Plugin> {
handle: Arc<Mutex<PluginHandle>>,
_type: std::marker::PhantomData<P>,
_type: core::marker::PhantomData<P>,
}
impl<P: Plugin> TypedPluginHandle<P> {
pub(crate) fn new(handle: Arc<Mutex<PluginHandle>>) -> Self {
Self {
handle,
_type: std::marker::PhantomData,
_type: core::marker::PhantomData,
}
}
@@ -77,7 +77,7 @@ impl<P: Plugin> TypedPluginHandle<P> {
pub fn lock(&self) -> TypedPluginGuard<'_, P> {
TypedPluginGuard {
guard: self.handle.lock(),
_type: std::marker::PhantomData,
_type: core::marker::PhantomData,
}
}
}
@@ -85,12 +85,12 @@ impl<P: Plugin> TypedPluginHandle<P> {
/// A guard that provides access to a [`Plugin`].
pub struct TypedPluginGuard<'a, P: Plugin> {
guard: MutexGuard<'a, PluginHandle>,
_type: std::marker::PhantomData<P>,
_type: core::marker::PhantomData<P>,
}
impl<P: Plugin> TypedPluginGuard<'_, P> {}
impl<P: Plugin> std::ops::Deref for TypedPluginGuard<'_, P> {
impl<P: Plugin> core::ops::Deref for TypedPluginGuard<'_, P> {
type Target = P;
fn deref(&self) -> &Self::Target {
@@ -98,7 +98,7 @@ impl<P: Plugin> std::ops::Deref for TypedPluginGuard<'_, P> {
}
}
impl<P: Plugin> std::ops::DerefMut for TypedPluginGuard<'_, P> {
impl<P: Plugin> core::ops::DerefMut for TypedPluginGuard<'_, P> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.guard.typed_plugin_mut()
}
@@ -111,7 +111,7 @@ impl PluginHandle {
}))
}
fn plugin_type_id(&self) -> std::any::TypeId {
fn plugin_type_id(&self) -> core::any::TypeId {
(*self.plugin).type_id()
}
@@ -120,13 +120,13 @@ impl PluginHandle {
}
fn typed_plugin<P: Plugin + 'static>(&self) -> &P {
(self.plugin.as_ref() as &dyn std::any::Any)
(self.plugin.as_ref() as &dyn core::any::Any)
.downcast_ref::<P>()
.expect("PluginHandle: plugin is not of the expected type")
}
pub fn typed_plugin_mut<P: Plugin + 'static>(&mut self) -> &mut P {
(self.plugin.as_mut() as &mut dyn std::any::Any)
(self.plugin.as_mut() as &mut dyn core::any::Any)
.downcast_mut::<P>()
.expect("PluginHandle: plugin is not of the expected type")
}
@@ -135,7 +135,7 @@ impl PluginHandle {
/// User-registered plugins.
#[derive(Clone, Default)]
pub(crate) struct Plugins {
plugins: HashMap<std::any::TypeId, Arc<Mutex<PluginHandle>>>,
plugins: HashMap<core::any::TypeId, Arc<Mutex<PluginHandle>>>,
plugins_ordered: PluginsOrdered,
}
@@ -215,7 +215,7 @@ impl Plugins {
true
}
pub fn get(&self, type_id: std::any::TypeId) -> Option<Arc<Mutex<PluginHandle>>> {
pub fn get(&self, type_id: core::any::TypeId) -> Option<Arc<Mutex<PluginHandle>>> {
self.plugins.get(&type_id).cloned()
}
}

View File

@@ -1,4 +1,5 @@
use std::{any::Any, sync::Arc};
use core::any::Any;
use std::sync::Arc;
use crate::{
Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui,
@@ -77,7 +78,7 @@ pub struct Response {
#[test]
fn test_response_size() {
assert_eq!(
std::mem::size_of::<Response>(),
core::mem::size_of::<Response>(),
88,
"Keep Response small, because we create them often, and we want to keep it lean and fast"
);
@@ -309,6 +310,12 @@ impl Response {
///
/// In contrast to [`Self::contains_pointer`], this will be `false` whenever some other widget is being dragged.
/// `hovered` is always `false` for disabled widgets.
///
/// While a widget is being clicked or dragged it is the only hovered widget,
/// so this stays `true` even after the pointer moves off it. Together with
/// how [`Self::dragged`] resolves a press that leaves the widget, that means
/// `hovered() || dragged()` holds for a whole press-drag-release gesture,
/// which is what you want for highlighting something like a drag handle.
#[inline(always)]
pub fn hovered(&self) -> bool {
self.flags.contains(Flags::HOVERED)
@@ -403,11 +410,21 @@ impl Response {
/// To find out which button(s), use [`Self::dragged_by`].
///
/// If the widget is only sensitive to drags, this is `true` as soon as the pointer presses down on it.
/// If the widget also senses clicks, this won't be true until the pointer has moved a bit,
/// or the user has pressed down for long enough.
///
/// If the widget also senses clicks, the press could be either, so the
/// decision is postponed until whichever of these comes first:
/// * the pointer moves further than [`crate::InputOptions::max_click_dist`],
/// * it is held longer than [`crate::InputOptions::max_click_duration`],
/// * or it leaves the widget — a click has to be released on the widget, so
/// once the pointer is outside, the gesture can only be a drag. This is what
/// keeps a handle thinner than `max_click_dist` from spending the decision
/// window as neither hovered nor dragged.
///
/// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details.
///
/// If you want to avoid the delay, use [`Self::is_pointer_button_down_on`] instead.
/// While the decision is pending the pointer is still on the widget, so
/// [`Self::hovered`] is `true` throughout. If you want neither the delay nor
/// the distinction, use [`Self::is_pointer_button_down_on`].
///
/// If the widget is NOT sensitive to drags, this will always be `false`.
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
@@ -571,6 +588,9 @@ impl Response {
/// even when dragging outside the widget.
///
/// This could also be thought of as "is this widget being interacted with?".
///
/// Unlike [`Self::dragged`], this is `true` from the press frame onwards, with
/// no click-versus-drag decision window.
#[inline(always)]
pub fn is_pointer_button_down_on(&self) -> bool {
self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON)
@@ -1093,7 +1113,7 @@ impl Response {
/// ```
///
/// Now `draw_vec2(ui, foo).hovered` is true if either [`DragValue`](crate::DragValue) were hovered.
impl std::ops::BitOr for Response {
impl core::ops::BitOr for Response {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
@@ -1114,7 +1134,7 @@ impl std::ops::BitOr for Response {
/// if response.hovered() { ui.label("You hovered at least one of the widgets"); }
/// # });
/// ```
impl std::ops::BitOrAssign for Response {
impl core::ops::BitOrAssign for Response {
fn bitor_assign(&mut self, rhs: Self) {
*self = self.union(rhs);
}

View File

@@ -22,8 +22,8 @@ bitflags::bitflags! {
}
}
impl std::fmt::Debug for Sense {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Sense {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Sense {{")?;
if self.senses_click() {
write!(f, " click")?;

View File

@@ -1,11 +1,12 @@
//! egui theme (spacing, colors, etc).
use core::ops::RangeInclusive;
use emath::Align;
use epaint::{
CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions,
text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting},
};
use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc};
use std::{collections::BTreeMap, sync::Arc};
use crate::{
ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode,
@@ -47,8 +48,8 @@ impl NumberFormatter {
}
}
impl std::fmt::Debug for NumberFormatter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for NumberFormatter {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("NumberFormatter")
}
}
@@ -93,8 +94,8 @@ pub enum TextStyle {
Name(std::sync::Arc<str>),
}
impl std::fmt::Display for TextStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for TextStyle {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Small => "Small".fmt(f),
Self::Body => "Body".fmt(f),
@@ -192,8 +193,8 @@ impl From<TextStyle> for FontSelection {
#[derive(Clone, Default)]
pub struct StyleModifier(Option<Arc<dyn Fn(&mut Style) + Send + Sync>>);
impl std::fmt::Debug for StyleModifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for StyleModifier {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("StyleModifier")
}
}
@@ -419,6 +420,9 @@ pub struct Spacing {
/// Default width of a [`crate::TextEdit`].
pub text_edit_width: f32,
/// Additional vertical spacing between lines of text.
pub extra_text_line_spacing: f32,
/// Checkboxes, radio button and collapsing headers have an icon at the start.
/// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
pub icon_width: f32,
@@ -1074,10 +1078,12 @@ pub struct Visuals {
/// How the text cursor acts.
pub text_cursor: TextCursorStyle,
/// Allow widgets to paint this much outside the scroll area rect.
/// Unused. Kept only for backwards compatibility.
///
/// Legacy. Should not be used anymore.
/// Used to allow widgets to paint this much outside the scroll area rect.
/// Setting it now has no effect.
/// Use [`crate::ScrollArea::content_margin`] instead.
#[deprecated(note = "This is now unused and has no effect")]
pub clip_rect_margin: f32,
/// Show a background behind buttons.
@@ -1456,6 +1462,7 @@ impl Default for Spacing {
slider_rail_height: 8.0,
combo_width: 100.0,
text_edit_width: 280.0,
extra_text_line_spacing: 0.0,
icon_width: 14.0,
icon_width_inner: 8.0,
icon_spacing: 4.0,
@@ -1487,6 +1494,7 @@ impl Default for Interaction {
impl Visuals {
/// Default dark theme.
#[expect(deprecated)]
pub fn dark() -> Self {
Self {
dark_mode: true,
@@ -1945,6 +1953,7 @@ impl Spacing {
slider_rail_height,
combo_width,
text_edit_width,
extra_text_line_spacing,
icon_width,
icon_width_inner,
icon_spacing,
@@ -2011,6 +2020,10 @@ impl Spacing {
ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0));
ui.end_row();
ui.label("Extra text line spacing");
ui.add(DragValue::new(extra_text_line_spacing).range(0.0..=20.0));
ui.end_row();
ui.label("Tooltip wrap width");
ui.add(DragValue::new(tooltip_width).range(0.0..=1000.0));
ui.end_row();
@@ -2263,6 +2276,7 @@ impl WidgetVisuals {
}
impl Visuals {
#[expect(deprecated)]
pub fn ui(&mut self, ui: &mut crate::Ui) {
let Self {
dark_mode,
@@ -2297,7 +2311,7 @@ impl Visuals {
text_cursor,
clip_rect_margin,
clip_rect_margin: _,
button_frame,
collapsing_header_frame,
indent_has_left_vline,
@@ -2484,8 +2498,6 @@ impl Visuals {
ui.collapsing("Misc", |ui| {
ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size"));
ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin"));
ui.checkbox(button_frame, "Button has a frame");
ui.checkbox(collapsing_header_frame, "Collapsing header has a frame");
ui.checkbox(
@@ -2684,7 +2696,7 @@ impl DebugOptions {
}
// TODO(emilk): improve and standardize
fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive<f32>) -> impl Widget + '_ {
fn two_drag_values(value: &mut Vec2, range: core::ops::RangeInclusive<f32>) -> impl Widget + '_ {
move |ui: &mut crate::Ui| {
ui.horizontal(|ui| {
ui.add(
@@ -2753,8 +2765,8 @@ impl NumericColorSpace {
}
}
impl std::fmt::Display for NumericColorSpace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Display for NumericColorSpace {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::GammaByte => write!(f, "U8"),
Self::Linear => write!(f, "F"),

View File

@@ -49,9 +49,9 @@ impl CCursorRange {
}
/// The range of selected character indices.
pub fn as_sorted_char_range(&self) -> std::ops::Range<CharIndex> {
pub fn as_sorted_char_range(&self) -> core::ops::Range<CharIndex> {
let [start, end] = self.sorted_cursors();
std::ops::Range {
core::ops::Range {
start: start.index,
end: end.index,
}

View File

@@ -47,8 +47,8 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 {
galley.pos_from_cursor(ccursor).center()
}
impl std::fmt::Debug for WidgetTextCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WidgetTextCursor {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self {
widget_id,
ccursor,
@@ -271,7 +271,7 @@ impl ViewportLabelSelectionState {
self.is_dragging = false;
}
let text_to_copy = std::mem::take(&mut self.text_to_copy);
let text_to_copy = core::mem::take(&mut self.text_to_copy);
if !text_to_copy.is_empty() {
ui.copy_text(text_to_copy);
}
@@ -773,7 +773,7 @@ mod tests {
.or_default()
.selection = Some(test_selection());
let _ = ctx.run_ui(RawInput::default(), |_| {});
let output = ctx.run_ui(RawInput::default(), |_| {});
assert!(
plugin
.lock()
@@ -782,11 +782,13 @@ mod tests {
.is_some_and(ViewportLabelSelectionState::has_selection),
"a pass in another viewport must not clear the child viewport selection"
);
output.drop_without_applying_deltas();
let _ = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
let output = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {});
assert!(
!plugin.lock().has_selection(),
"the selection must be cleared when its labels disappear from the same viewport"
);
output.drop_without_applying_deltas();
}
}

View File

@@ -294,7 +294,7 @@ pub fn char_index_from_byte_index(input: &str, byte_index: ByteIndex) -> CharInd
CharIndex(input.chars().count())
}
pub fn slice_char_range(s: &str, char_range: std::ops::Range<CharIndex>) -> &str {
pub fn slice_char_range(s: &str, char_range: core::ops::Range<CharIndex>) -> &str {
assert!(
char_range.start <= char_range.end,
"Invalid range, start must be less than end, but start = {}, end = {}",

View File

@@ -139,8 +139,8 @@ pub(crate) fn paint_ime_preedit_text_visuals(
painter: &Painter,
galley: &Arc<Galley>,
row_height: f32,
preedit_range: std::ops::Range<CCursor>,
mut relative_active_range: Option<std::ops::Range<CCursor>>,
preedit_range: core::ops::Range<CCursor>,
mut relative_active_range: Option<core::ops::Range<CCursor>>,
time_since_last_interaction: f64,
) {
/// Instead of implementing [`PartialOrd`] and [`Ord`] for [`CCursor`] to
@@ -150,7 +150,7 @@ pub(crate) fn paint_ime_preedit_text_visuals(
/// These traits are intentionally not implemented because
/// [`CCursor::prefer_next_row`] makes it difficult to define a clear
/// ordering between two [`CCursor`]s.
fn is_cursor_range_empty(range: &std::ops::Range<CCursor>) -> bool {
fn is_cursor_range_empty(range: &core::ops::Range<CCursor>) -> bool {
range.start.index == range.end.index
}

View File

@@ -1,7 +1,8 @@
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
#![expect(clippy::use_self)]
use std::{any::Any, ops::Deref, sync::Arc};
use core::{any::Any, ops::Deref};
use std::sync::Arc;
use crate::containers::menu;
use crate::widget_style::{HasClasses as _, ROOT_CLASS};
@@ -1984,7 +1985,7 @@ impl Ui {
/// but is shown to the user in fractions of one Tau (i.e. fractions of one turn).
/// The angle is NOT wrapped, so the user may select, for instance 2𝞃 (720°)
pub fn drag_angle_tau(&mut self, radians: &mut f32) -> Response {
use std::f32::consts::TAU;
use core::f32::consts::TAU;
let mut taus = *radians / TAU;
let mut response = self.add(DragValue::new(&mut taus).speed(0.01).suffix("τ"));
@@ -2599,7 +2600,7 @@ impl Ui {
let column_width = (self.available_width() - total_spacing) / (NUM_COL as f32);
let top_left = self.cursor().min;
let mut columns = std::array::from_fn(|col_idx| {
let mut columns = core::array::from_fn(|col_idx| {
let pos = top_left + vec2((col_idx as f32) * (column_width + spacing), 0.0);
let child_rect = Rect::from_min_max(
pos,

View File

@@ -1,5 +1,5 @@
use core::{any::Any, iter::FusedIterator};
use std::sync::Arc;
use std::{any::Any, iter::FusedIterator};
use crate::widget_style::Classes;
use epaint::Color32;

View File

@@ -16,15 +16,15 @@ where
}
}
impl<K, V> std::fmt::Debug for FixedCache<K, V> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl<K, V> core::fmt::Debug for FixedCache<K, V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Cache")
}
}
impl<K, V> FixedCache<K, V>
where
K: std::hash::Hash + PartialEq,
K: core::hash::Hash + PartialEq,
{
pub fn get(&self, key: &K) -> Option<&V> {
let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize;

View File

@@ -3,7 +3,8 @@
// For non-serializable types, these simply return `None`.
// This will also allow users to pick their own serialization format per type.
use std::{any::Any, sync::Arc};
use core::any::Any;
use std::sync::Arc;
// -----------------------------------------------------------------------------------------------
/// Like [`std::any::TypeId`], but can be serialized and deserialized.
@@ -14,7 +15,7 @@ pub struct TypeId(u64);
impl TypeId {
#[inline]
pub fn of<T: Any + 'static>() -> Self {
std::any::TypeId::of::<T>().into()
core::any::TypeId::of::<T>().into()
}
#[inline(always)]
@@ -23,9 +24,9 @@ impl TypeId {
}
}
impl From<std::any::TypeId> for TypeId {
impl From<core::any::TypeId> for TypeId {
#[inline]
fn from(id: std::any::TypeId) -> Self {
fn from(id: core::any::TypeId) -> Self {
Self(epaint::util::hash(id))
}
}
@@ -113,8 +114,8 @@ impl Clone for Element {
}
}
impl std::fmt::Debug for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for Element {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match &self {
Self::Value { value, .. } => f
.debug_struct("Element::Value")
@@ -314,7 +315,7 @@ fn from_ron_str<T: serde::de::DeserializeOwned>(ron: &str) -> Option<T> {
Err(_err) => {
log::warn!(
"egui: Failed to deserialize {} from memory: {}, ron error: {:?}",
std::any::type_name::<T>(),
core::any::type_name::<T>(),
_err,
ron
);
@@ -578,7 +579,7 @@ impl IdTypeMap {
pub fn remove_temp<T: 'static + Default>(&mut self, id: Id) -> Option<T> {
let key = RawKey::new::<T>(id);
let mut element = self.map.remove(&key)?;
Some(std::mem::take(element.get_mut_temp()?))
Some(core::mem::take(element.get_mut_temp()?))
}
/// Remove a temporary value given a raw key.

View File

@@ -67,8 +67,8 @@ pub struct Undoer<State> {
flux: Option<Flux<State>>,
}
impl<State> std::fmt::Debug for Undoer<State> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl<State> core::fmt::Debug for Undoer<State> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let Self { undos, redos, .. } = self;
f.debug_struct("Undoer")
.field("undo count", &undos.len())

View File

@@ -71,9 +71,8 @@
use std::sync::Arc;
use epaint::{Pos2, Vec2};
use crate::{AsId, Context, Id, Ui};
use epaint::{Pos2, Vec2};
// ----------------------------------------------------------------------------
@@ -121,13 +120,13 @@ pub struct ViewportId(pub Id);
// We implement `PartialOrd` and `Ord` so we can use `ViewportId` in a `BTreeMap`,
// which allows predicatable iteration order, frame-to-frame.
impl PartialOrd for ViewportId {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ViewportId {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.0.value().cmp(&other.0.value())
}
}
@@ -139,8 +138,8 @@ impl Default for ViewportId {
}
}
impl std::fmt::Debug for ViewportId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for ViewportId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.0.short_debug_format().fmt(f)
}
}
@@ -199,8 +198,8 @@ impl IconData {
}
}
impl std::fmt::Debug for IconData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for IconData {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("IconData")
.field("width", &self.width)
.field("height", &self.height)
@@ -1276,7 +1275,7 @@ pub struct ViewportOutput {
/// but if you haven't, you can use this instead.
///
/// If the duration is zero, schedule a repaint immediately.
pub repaint_delay: std::time::Duration,
pub repaint_delay: core::time::Duration,
}
impl ViewportOutput {

View File

@@ -1,5 +1,5 @@
use core::fmt::Formatter;
use epaint::text::{IntoTag, TextFormat, VariationCoords};
use std::fmt::Formatter;
use std::{borrow::Cow, sync::Arc};
use crate::{
@@ -539,8 +539,8 @@ pub enum WidgetText {
Galley(Arc<Galley>),
}
impl std::fmt::Debug for WidgetText {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for WidgetText {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
let text = self.text();
match self {
Self::Text(_) => write!(f, "Text({text:?})"),
@@ -558,6 +558,25 @@ impl Default for WidgetText {
}
impl WidgetText {
/// Override the font size.
///
/// For [`Self::Galley`], this does nothing because it has already been laid out.
#[must_use]
pub fn size(self, size: f32) -> Self {
match self {
Self::Text(text) => RichText::new(text).size(size).into(),
Self::RichText(text) => Self::RichText(Arc::new(Arc::unwrap_or_clone(text).size(size))),
Self::LayoutJob(job) => {
let mut job = Arc::unwrap_or_clone(job);
for section in &mut job.sections {
section.format.font_id.size = size;
}
Self::LayoutJob(Arc::new(job))
}
Self::Galley(galley) => Self::Galley(galley),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
match self {
@@ -750,14 +769,19 @@ impl WidgetText {
.visuals
.override_text_color
.unwrap_or(crate::Color32::PLACEHOLDER);
// We want the style overrides to take precedence over the fallback font
let font_id = FontSelection::default().resolve_with_fallback(style, fallback_font);
let line_height = ctx
.fonts_mut(|f| f.row_height(&font_id) + style.spacing.extra_text_line_spacing);
let mut layout_job = LayoutJob::simple_format(
text,
TextFormat {
// We want the style overrides to take precedence over the fallback font
font_id: FontSelection::default()
.resolve_with_fallback(style, fallback_font),
font_id,
color,
valign: default_valign,
line_height: Some(line_height),
..Default::default()
},
);

View File

@@ -3,8 +3,8 @@ use crate::{
Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget,
WidgetInfo, emath, text,
};
use core::{cmp::Ordering, ops::RangeInclusive};
use emath::Vec2;
use std::{cmp::Ordering, ops::RangeInclusive};
// ----------------------------------------------------------------------------
@@ -25,6 +25,24 @@ fn set(get_set_value: &mut GetSetValue<'_>, value: f64) {
(get_set_value)(Some(value));
}
// ----------------------------------------------------------------------------
/// What the user has typed into a [`DragValue`] that is being edited as text.
///
/// Stored in [`crate::Memory::data`] between frames, because the text can be
/// something that doesn't (yet) parse to a number, e.g. `"1."` or `"-"`.
#[derive(Clone, Default)]
struct EditState {
/// The text the user is editing.
text: String,
/// The value of the [`DragValue`] the last time we stored `text`.
///
/// If the value has changed since then it was changed by something other than
/// this widget, and `text` is stale and must not be written back to the value.
value: f64,
}
/// A numeric value that you can change by dragging the number. More compact than a [`crate::Slider`].
///
/// ```
@@ -466,7 +484,7 @@ impl Widget for DragValue<'_> {
});
if ui.memory_mut(|mem| mem.gained_focus(id)) {
ui.data_mut(|data| data.remove::<String>(id));
ui.data_mut(|data| data.remove::<EditState>(id));
}
let old_value = get(&mut get_set_value);
@@ -524,7 +542,7 @@ impl Widget for DragValue<'_> {
if old_value != value {
set(&mut get_set_value, value);
ui.data_mut(|data| data.remove::<String>(id));
ui.data_mut(|data| data.remove::<EditState>(id));
}
let value_text = match custom_formatter {
@@ -538,8 +556,13 @@ impl Widget for DragValue<'_> {
let text_style = ui.style().drag_value_text_style.clone();
if ui.memory(|mem| mem.lost_focus(id)) && !ui.input(|i| i.key_pressed(Key::Escape)) {
let value_text = ui.data_mut(|data| data.remove_temp::<String>(id));
if let Some(value_text) = value_text {
let edit_state = ui.data_mut(|data| data.remove_temp::<EditState>(id));
// Ignore the text if the value was changed by something else while we were editing it,
// or we would revert that change.
if let Some(value_text) = edit_state
.filter(|edit_state| edit_state.value == old_value)
.map(|edit_state| edit_state.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.as_ref(), &value_text);
@@ -552,9 +575,12 @@ impl Widget for DragValue<'_> {
}
let mut response = if is_kb_editing {
// Keep editing the text from last frame, unless the value was changed by
// something else in the meantime, in which case the text is stale.
let mut value_text = ui
.data_mut(|data| data.remove_temp::<String>(id))
.unwrap_or_else(|| value_text.clone());
.data_mut(|data| data.remove_temp::<EditState>(id))
.filter(|edit_state| edit_state.value == old_value)
.map_or_else(|| value_text.clone(), |edit_state| edit_state.text);
let response = ui.add(
TextEdit::singleline(&mut value_text)
.clip_text(false)
@@ -589,7 +615,13 @@ impl Widget for DragValue<'_> {
set(&mut get_set_value, parsed_value);
}
}
ui.data_mut(|data| data.insert_temp(id, value_text));
// Remember the value the text belongs to, so that next frame we can tell
// whether the value was changed by us or by something else.
let edit_state = EditState {
text: value_text,
value: get(&mut get_set_value),
};
ui.data_mut(|data| data.insert_temp(id, edit_state));
response
} else {
atoms.map_atoms(|atom| {
@@ -631,7 +663,7 @@ impl Widget for DragValue<'_> {
}
if response.clicked() {
ui.data_mut(|data| data.remove::<String>(id));
ui.data_mut(|data| data.remove::<EditState>(id));
ui.memory_mut(|mem| mem.request_focus(id));
select_all_text(ui, id, response.id, &value_text);
} else if response.dragged() {
@@ -780,7 +812,7 @@ mod tests {
macro_rules! total_assert_eq {
($a:expr, $b:expr) => {
assert!(
matches!($a.total_cmp(&$b), std::cmp::Ordering::Equal),
matches!($a.total_cmp(&$b), core::cmp::Ordering::Equal),
"{} != {}",
$a,
$b

View File

@@ -1,4 +1,5 @@
use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration};
use core::{slice::Iter, time::Duration};
use std::{borrow::Cow, sync::Arc};
use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2};
use epaint::{
@@ -607,8 +608,8 @@ pub enum ImageSource<'a> {
},
}
impl std::fmt::Debug for ImageSource<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl core::fmt::Debug for ImageSource<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f),
ImageSource::Texture(st) => st.id.fmt(f),
@@ -933,7 +934,7 @@ fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize {
/// Checks if uri is a gif file
fn is_gif_uri(uri: &str) -> bool {
uri.ends_with(".gif") || uri.contains(".gif#")
crate::load::has_extension(uri, "gif")
}
/// Checks if bytes are gifs
@@ -943,7 +944,7 @@ pub fn has_gif_magic_header(bytes: &[u8]) -> bool {
/// Checks if uri is a webp file
fn is_webp_uri(uri: &str) -> bool {
uri.ends_with(".webp") || uri.contains(".webp#")
crate::load::has_extension(uri, "webp")
}
/// Checks if bytes are webp

View File

@@ -6,6 +6,12 @@
use crate::{Response, Ui};
/// A dynamically dispatched [`Widget`].
///
/// [`Widget`] is not dyn compatible because [`Widget::ui`] takes `self` by value.
/// This alias uses a closure, which implements [`Widget`].
pub type BoxedWidget<'a> = Box<dyn FnOnce(&mut Ui) -> Response + 'a>;
mod button;
mod checkbox;
pub mod color_picker;
@@ -63,6 +69,20 @@ pub trait Widget {
///
/// Tip: you can `impl Widget for &mut YourObject { }`.
fn ui(self, ui: &mut Ui) -> Response;
/// Box this widget for dynamic dispatch.
#[inline]
fn boxed<'a>(self) -> BoxedWidget<'a>
where
Self: Sized + 'a,
{
Box::new(move |ui: &mut Ui| ui.add(self))
}
}
#[test]
fn widgets_can_be_boxed() {
let _: BoxedWidget<'static> = Button::new("boxed").boxed();
}
/// This enables functions that return `impl Widget`, so that you can

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