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

Merge main (v0.34.0) into feat/harfrust-text-shaping

This commit is contained in:
gcailly
2026-03-26 16:15:40 +01:00
35 changed files with 731 additions and 116 deletions

View File

@@ -6,6 +6,10 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.0 - 2026-03-26
Nothing new
## 0.33.3 - 2025-12-11
Nothing new

View File

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

View File

@@ -170,12 +170,16 @@ objc2-app-kit = { workspace = true, default-features = false, features = [
"std",
"NSApplication",
"NSBitmapImageRep",
"NSButton",
"NSControl",
"NSGraphics",
"NSImage",
"NSImageRep",
"NSMenu",
"NSMenuItem",
"NSResponder",
"NSView",
"NSWindow",
] }
# windows:

View File

@@ -190,6 +190,9 @@ pub use web::{WebLogger, WebRunner};
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
mod native;
#[cfg(target_os = "macos")]
pub use native::macos::WindowChromeMetrics;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))]
pub use native::run::EframeWinitApplication;

View File

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

View File

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

View File

@@ -6,6 +6,20 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.0 - 2026-03-26
### ⭐ Added
* Add error message when calling `.render()` without `.update_buffers()` [#8005](https://github.com/emilk/egui/pull/8005) by [@emilk](https://github.com/emilk)
### 🔧 Changed
* Put the `capture` module behind a feature flag, make the `egui` dependency optional [#7698](https://github.com/emilk/egui/pull/7698) by [@StT191](https://github.com/StT191)
* Attach stencil buffer [#7702](https://github.com/emilk/egui/pull/7702) by [@jgraef](https://github.com/jgraef)
* Update wgpu to 28.0.0 [#7853](https://github.com/emilk/egui/pull/7853) by [@SuchAFuriousDeath](https://github.com/SuchAFuriousDeath)
* Update to wgpu 29 [#7990](https://github.com/emilk/egui/pull/7990) by [@cwfitzgerald](https://github.com/cwfitzgerald)
### 🐛 Fixed
* Fix wgpu memory leak leading to panic when window is minimized (#7434) [#7928](https://github.com/emilk/egui/pull/7928) by [@landaire](https://github.com/landaire)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -5,6 +5,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.34.0 - 2026-03-26
* Add `is_scrolling`/`is_smooth_scrolling` util, checking for active scroll action [#7669](https://github.com/emilk/egui/pull/7669) by [@IsseW](https://github.com/IsseW)
* Fix backspacing leaving last character in IME prediction not removed on macOS native and Safari [#7810](https://github.com/emilk/egui/pull/7810) by [@umajho](https://github.com/umajho)
* Much improved IME [#7967](https://github.com/emilk/egui/pull/7967) by [@umajho](https://github.com/umajho)
* Allow fallback from smithay to arboard when getting clipboard [#7976](https://github.com/emilk/egui/pull/7976) by [@wizzeh](https://github.com/wizzeh)
## 0.33.3 - 2025-12-11
Nothing new

View File

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

View File

@@ -1545,6 +1545,11 @@ impl Context {
crate::debug_text::print(self, text);
}
/// Current time in seconds, relative to some unknown epoch.
pub fn time(&self) -> f64 {
self.input(|i| i.time)
}
/// What operating system are we running on?
///
/// When compiling natively, this is
@@ -1933,7 +1938,7 @@ impl Context {
}
}
/// Callbacks
/// Plugins
impl Context {
/// Call the given callback at the start of each pass of each viewport.
///
@@ -2945,6 +2950,15 @@ impl Context {
self.egui_wants_keyboard_input()
}
/// Is the currently focused widget a text edit?
pub fn text_edit_focused(&self) -> bool {
if let Some(id) = self.memory(|mem| mem.focused()) {
crate::text_edit::TextEditState::load(self, id).is_some()
} else {
false
}
}
/// Highlight this widget, to make it look like it is hovered, even if it isn't.
///
/// If you call this after the widget has been fully rendered,

View File

@@ -1,7 +1,7 @@
use emath::GuiRounding as _;
use crate::{
Align,
Align, Direction,
emath::{Align2, NumExt as _, Pos2, Rect, Vec2, pos2, vec2},
};
const INFINITY: f32 = f32::INFINITY;
@@ -87,36 +87,6 @@ impl Region {
// ----------------------------------------------------------------------------
/// Layout direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Direction {
LeftToRight,
RightToLeft,
TopDown,
BottomUp,
}
impl Direction {
#[inline(always)]
pub fn is_horizontal(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => true,
Self::TopDown | Self::BottomUp => false,
}
}
#[inline(always)]
pub fn is_vertical(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => false,
Self::TopDown | Self::BottomUp => true,
}
}
}
// ----------------------------------------------------------------------------
/// The layout of a [`Ui`][`crate::Ui`], e.g. "vertical & centered".
///
/// ```

View File

@@ -449,7 +449,7 @@ pub use emath::{
remap_clamp, vec2,
};
pub use epaint::{
ClippedPrimitive, ColorImage, CornerRadius, ImageData, Margin, Mesh, PaintCallback,
ClippedPrimitive, ColorImage, CornerRadius, Direction, ImageData, Margin, Mesh, PaintCallback,
PaintCallbackInfo, Shadow, Shape, Stroke, StrokeKind, TextureHandle, TextureId, mutex,
text::{FontData, FontDefinitions, FontFamily, FontId, FontTweak},
textures::{TextureFilter, TextureOptions, TextureWrapMode, TexturesDelta},
@@ -478,7 +478,7 @@ pub use self::{
drag_and_drop::DragAndDrop,
epaint::text::TextWrapMode,
grid::Grid,
id::{Id, IdMap},
id::{Id, IdMap, IdSet},
input_state::{InputOptions, InputState, MultiTouchInfo, PointerState, SurrenderFocusOn},
layers::{LayerId, Order},
layout::*,

View File

@@ -586,6 +586,8 @@ pub struct ScrollStyle {
/// This is only for floating scroll bars.
/// Solid scroll bars are always opaque.
pub interact_handle_opacity: f32,
pub fade: ScrollFadeStyle,
}
impl Default for ScrollStyle {
@@ -616,6 +618,8 @@ impl ScrollStyle {
dormant_handle_opacity: 0.0,
active_handle_opacity: 0.6,
interact_handle_opacity: 1.0,
fade: Default::default(),
}
}
@@ -699,6 +703,8 @@ impl ScrollStyle {
dormant_handle_opacity,
active_handle_opacity,
interact_handle_opacity,
fade,
} = self;
ui.horizontal(|ui| {
@@ -772,6 +778,52 @@ impl ScrollStyle {
ui.label("Inner margin");
});
}
ui.separator();
fade.ui(ui);
}
}
/// Controls if and how to fade out the sides of a [`crate::ScrollArea`]
/// to indicate there is more there if you scroll.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct ScrollFadeStyle {
/// Opacity of the fade effect at the outer edge, in 0.0-1.0.
///
/// Set to 0.0 to disable the fade effect.
pub strength: f32,
/// Size of the fade-area (height for vertical scrolling,
/// width for horizontal scrolling).
pub size: f32,
}
impl Default for ScrollFadeStyle {
fn default() -> Self {
Self {
strength: 0.5,
size: 20.0,
}
}
}
impl ScrollFadeStyle {
pub fn ui(&mut self, ui: &mut Ui) {
let Self { strength, size } = self;
ui.horizontal(|ui| {
ui.add(DragValue::new(strength).speed(0.01).range(0.0..=1.0));
ui.label("Fade strength");
});
if 0.0 < *strength {
ui.horizontal(|ui| {
ui.add(DragValue::new(size).range(0.0..=64.0));
ui.label("Fade size");
});
}
}
}

View File

@@ -487,6 +487,12 @@ impl Ui {
&mut self.style_mut().visuals
}
/// Is this [`Ui`] in a tooltip?
#[inline]
pub fn is_tooltip(&self) -> bool {
self.layer_id().order == Order::Tooltip
}
/// Get a reference to this [`Ui`]'s [`UiStack`].
#[inline]
pub fn stack(&self) -> &Arc<UiStack> {

View File

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

View File

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

View File

@@ -371,6 +371,12 @@ impl<'a> Button<'a> {
AtomLayoutResponse::empty(prepared.response)
};
if let Some(cursor) = ui.visuals().interact_cursor
&& response.response.hovered()
{
ui.ctx().set_cursor_icon(cursor);
}
response.response.widget_info(|| {
if let Some(text) = &text {
WidgetInfo::labeled(WidgetType::Button, ui.is_enabled(), text)

View File

@@ -27,7 +27,7 @@ impl crate::Demo for StripDemo {
impl crate::View for StripDemo {
fn ui(&mut self, ui: &mut egui::Ui) {
let dark_mode = ui.visuals().dark_mode;
let faded_color = ui.visuals().window_fill();
let faded_color = ui.stack().bg_color();
let faded_color = |color: Color32| -> Color32 {
use egui::Rgba;
let t = if dark_mode { 0.95 } else { 0.8 };

View File

@@ -5,6 +5,14 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.0 - 2026-03-26
* Fix media type with optional parameters [#7739](https://github.com/emilk/egui/pull/7739) by [@leungkkf](https://github.com/leungkkf)
* Fix: CodeTheme functions - ui and add builder [#7684](https://github.com/emilk/egui/pull/7684) by [@Its-Just-Nans](https://github.com/Its-Just-Nans)
* Update selected dependencies [#7920](https://github.com/emilk/egui/pull/7920) by [@oscargus](https://github.com/oscargus)
* Add `DatePickerButton::reverse_years/year_scroll_to` [#7978](https://github.com/emilk/egui/pull/7978) by [@Deuracell](https://github.com/Deuracell)
* Replace `chrono` with `jiff` [#8008](https://github.com/emilk/egui/pull/8008) by [@emilk](https://github.com/emilk)
## 0.33.3 - 2025-12-11
* Bump `ehttp` to 0.6.0 [#7757](https://github.com/emilk/egui/pull/7757) by [@jprochazk](https://github.com/jprochazk)

View File

@@ -656,14 +656,13 @@ impl TableState {
}
fn store(self, ui: &egui::Ui, state_id: egui::Id) {
#![expect(clippy::needless_return)]
#[cfg(feature = "serde")]
{
return ui.data_mut(|d| d.insert_persisted(state_id, self));
ui.data_mut(|d| d.insert_persisted(state_id, self));
}
#[cfg(not(feature = "serde"))]
{
return ui.data_mut(|d| d.insert_temp(state_id, self));
ui.data_mut(|d| d.insert_temp(state_id, self));
}
}

View File

@@ -6,6 +6,10 @@ Changes since the last release can be found at <https://github.com/emilk/egui/co
## 0.34.0 - 2026-03-26
Nothing new
## 0.33.3 - 2025-12-11
Nothing new

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.34.0 - 2026-03-26
* Turn `HarnessBuilder::with_options` into a proper builder method [#7697](https://github.com/emilk/egui/pull/7697) by [@emilk](https://github.com/emilk)
* Paint mouse cursor in kittest snapshot images [#7721](https://github.com/emilk/egui/pull/7721) by [@emilk](https://github.com/emilk)
* Add `kittest.toml` config file [#7643](https://github.com/emilk/egui/pull/7643) by [@lucasmerlin](https://github.com/lucasmerlin)
* Close debug_open_snapshot temp file before viewing it [#7841](https://github.com/emilk/egui/pull/7841) by [@yuriks](https://github.com/yuriks)
## 0.33.3 - 2025-12-11
* Enforce consistent snapshot updates [#7744](https://github.com/emilk/egui/pull/7744) by [@lucasmerlin](https://github.com/lucasmerlin)
* `kittest`: add drag-and-drop helpers [#7690](https://github.com/emilk/egui/pull/7690) by [@emilk](https://github.com/emilk)

View File

@@ -6,6 +6,10 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.0 - 2026-03-26
Nothing new
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -5,6 +5,25 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.0 - 2026-03-26
### ⭐ Added
* Replace ab_glyph with Skrifa + vello_cpu; enable font hinting [#7694](https://github.com/emilk/egui/pull/7694) by [@valadaptive](https://github.com/valadaptive)
* Add font variations API [#7859](https://github.com/emilk/egui/pull/7859) by [@valadaptive](https://github.com/valadaptive)
* Allow rotation of rectangles and ellipses [#7682](https://github.com/emilk/egui/pull/7682) by [@RyanBluth](https://github.com/RyanBluth)
### 🔧 Changed
* Apply preferred font weight when loading variable fonts [#7790](https://github.com/emilk/egui/pull/7790) by [@pmnxis](https://github.com/pmnxis)
* Make `Galley::pos_from_layout_cursor` `pub` [#7864](https://github.com/emilk/egui/pull/7864) by [@dionb](https://github.com/dionb)
### 🐛 Fixed
* Fixes the overly aggressive overflow elision in `truncate()` and similar for os scaling other than 100% [#7867](https://github.com/emilk/egui/pull/7867) by [@RndUsr123](https://github.com/RndUsr123)
* Fix text color when selecting newline character [#7951](https://github.com/emilk/egui/pull/7951) by [@emilk](https://github.com/emilk)
* Fix galley width calculation being off due to subpixel binning [#7972](https://github.com/emilk/egui/pull/7972) by [@lucasmerlin](https://github.com/lucasmerlin)
### 🚀 Performance
* Avoid cloning `Row`s during `Galley::concat` [#7649](https://github.com/emilk/egui/pull/7649) by [@afishhh](https://github.com/afishhh)
## 0.33.3 - 2025-12-11
Nothing new

View File

@@ -0,0 +1,27 @@
/// A cardinal direction, one of [`LeftToRight`](Direction::LeftToRight), [`RightToLeft`](Direction::RightToLeft), [`TopDown`](Direction::TopDown), [`BottomUp`](Direction::BottomUp).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum Direction {
LeftToRight,
RightToLeft,
TopDown,
BottomUp,
}
impl Direction {
#[inline(always)]
pub fn is_horizontal(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => true,
Self::TopDown | Self::BottomUp => false,
}
}
#[inline(always)]
pub fn is_vertical(self) -> bool {
match self {
Self::LeftToRight | Self::RightToLeft => false,
Self::TopDown | Self::BottomUp => true,
}
}
}

View File

@@ -27,6 +27,7 @@ mod brush;
pub mod color;
mod corner_radius;
mod corner_radius_f32;
mod direction;
pub mod image;
mod margin;
mod margin_f32;
@@ -50,6 +51,7 @@ pub use self::{
color::ColorMode,
corner_radius::CornerRadius,
corner_radius_f32::CornerRadiusF32,
direction::Direction,
image::{AlphaFromCoverage, ColorImage, ImageData, ImageDelta},
margin::Margin,
margin_f32::*,

View File

@@ -23,6 +23,18 @@ pub struct Vertex {
pub color: Color32, // 32 bit
}
impl Vertex {
/// An untextured vertex
#[inline]
pub fn untextured(pos: Pos2, color: Color32) -> Self {
Self {
pos,
uv: WHITE_UV,
color,
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg(all(feature = "unity", not(feature = "_override_unity")))]
@@ -159,11 +171,7 @@ impl Mesh {
self.texture_id == TextureId::default(),
"Mesh has an assigned texture"
);
self.vertices.push(Vertex {
pos,
uv: WHITE_UV,
color,
});
self.vertices.push(Vertex::untextured(pos, color));
}
/// Add a triangle.

View File

@@ -5,7 +5,7 @@ use std::sync::Arc;
use emath::{Align2, Pos2, Rangef, Rect, TSTransform, Vec2, pos2};
use crate::{
Color32, CornerRadius, Mesh, Stroke, StrokeKind, TextureId,
Color32, CornerRadius, Direction, Mesh, Stroke, StrokeKind, TextureId, Vertex,
stroke::PathStroke,
text::{FontId, FontsView, Galley},
};
@@ -297,6 +297,32 @@ impl Shape {
Self::Rect(RectShape::stroke(rect, corner_radius, stroke, stroke_kind))
}
/// Paints a gradient rectangle that transitions from `color_from` to `color_to`
/// along the given `direction`.
///
/// For example, [`Direction::TopDown`] paints `color_from` at the top edge fading
/// to `color_to` at the bottom edge.
#[inline]
pub fn gradient_rect(rect: Rect, direction: Direction, [from, to]: [Color32; 2]) -> Self {
let (left_top, right_top, left_bottom, right_bottom) = match direction {
Direction::TopDown => (from, from, to, to),
Direction::BottomUp => (to, to, from, from),
Direction::LeftToRight => (from, to, from, to),
Direction::RightToLeft => (to, from, to, from),
};
Self::from(Mesh {
indices: vec![0, 1, 2, 2, 1, 3],
vertices: vec![
Vertex::untextured(rect.left_top(), left_top),
Vertex::untextured(rect.right_top(), right_top),
Vertex::untextured(rect.left_bottom(), left_bottom),
Vertex::untextured(rect.right_bottom(), right_bottom),
],
texture_id: Default::default(),
})
}
#[expect(clippy::needless_pass_by_value)]
pub fn text(
fonts: &mut FontsView<'_>,

View File

@@ -10,8 +10,8 @@ use emath::{GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2, pos2, remap,
use crate::{
CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape,
EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke,
StrokeKind, TextShape, TextureId, Vertex, WHITE_UV, color::ColorMode, emath,
stroke::PathStroke, texture_atlas::PreparedDisc,
StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke,
texture_atlas::PreparedDisc,
};
// ----------------------------------------------------------------------------
@@ -809,11 +809,8 @@ fn fill_closed_path(feathering: f32, path: &mut [PathPoint], fill_color: Color32
} else {
out.reserve_triangles(n as usize);
let idx = out.vertices.len() as u32;
out.vertices.extend(path.iter().map(|p| Vertex {
pos: p.pos,
uv: WHITE_UV,
color: fill_color,
}));
out.vertices
.extend(path.iter().map(|p| Vertex::untextured(p.pos, fill_color)));
for i in 2..n {
out.add_triangle(idx, idx + i - 1, idx + i);
}

View File

@@ -5,6 +5,10 @@ This file is updated upon each release.
Changes since the last release can be found at <https://github.com/emilk/egui/compare/latest...HEAD> or by running the `scripts/generate_changelog.py` script.
## 0.34.0 - 2026-03-26
* Fix emoji icon font [#7940](https://github.com/emilk/egui/pull/7940) by [@Jhynjhiruu](https://github.com/Jhynjhiruu)
## 0.33.3 - 2025-12-11
Nothing new