From db76c5ca3bb9f5d3b32847bcd6d9435240e31c8c Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 5 Jan 2026 08:22:54 +0100 Subject: [PATCH 01/14] Fix new `move_to_top` flag breaking widget order (#7825) * closes https://github.com/emilk/egui/issues/7812 * related https://github.com/emilk/egui/pull/7805 That pr introduced a bug that caused a mismatch in the `by_layer` / `by_id` widget rects. This should fix it by updating the index of all following widgets. Not super pretty and efficient, but I'm not sure if there's a better way. Maybe we could also just leave a "tombstone" / duplicate there in the by_layer map so we don't need to update the indexes? --- crates/egui/src/widget_rect.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index e03423623..a84dde519 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -164,6 +164,8 @@ impl WidgetRects { let InteractOptions { move_to_top } = options; + let mut shift_layer_index_after = None; + let layer_widgets = by_layer.entry(layer_id).or_default(); match by_id.entry(widget_rect.id) { @@ -187,6 +189,7 @@ impl WidgetRects { if existing.layer_id == widget_rect.layer_id { if move_to_top { layer_widgets.remove(*idx_in_layer); + shift_layer_index_after = Some(*idx_in_layer); *idx_in_layer = layer_widgets.len(); layer_widgets.push(*existing); } else { @@ -200,6 +203,16 @@ impl WidgetRects { } } } + + if let Some(shift_start) = shift_layer_index_after { + #[expect(clippy::needless_range_loop)] + for i in shift_start..layer_widgets.len() { + let w = &layer_widgets[i]; + if let Some((idx_in_by_id, _)) = by_id.get_mut(&w.id) { + *idx_in_by_id = i; + } + } + } } pub fn set_info(&mut self, id: Id, info: WidgetInfo) { From 9f1f3fca38f71da36851673efdf379e275f58962 Mon Sep 17 00:00:00 2001 From: valadaptive <79560998+valadaptive@users.noreply.github.com> Date: Mon, 5 Jan 2026 03:32:58 -0500 Subject: [PATCH 02/14] Remove `font_weight` API (#7811) * Closes N/A * [x] I have followed the instructions in the PR template This appears to have snuck in as part of https://github.com/emilk/egui/pull/7790, which claimed to only be a bugfix but introduced a new `font_weight` method. I believe there's no way to access the method from *public* code since it's only defined on `FontsImpl`, not the public-facing `FontsView`. It's also not used *privately* in epaint, meaning it's completely dead code. Even if we *do* want some sort of future API for getting a font's weight, it requires more consideration. For instance, this API will return the default weight for variable fonts, which is not documented anywhere and might not be what we want. --- crates/epaint/src/text/font.rs | 9 --------- crates/epaint/src/text/fonts.rs | 20 -------------------- 2 files changed, 29 deletions(-) diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 80fb1245b..150aca34a 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -337,9 +337,6 @@ pub struct FontFace { font: FontCell, tweak: FontTweak, - /// The font weight (100-900) if available from the font file. - weight: Option, - /// Variable font location (for weight axis, etc.) location: skrifa::instance::Location, glyph_info_cache: ahash::HashMap, @@ -436,18 +433,12 @@ impl FontFace { name, font, tweak, - weight, location, glyph_info_cache: Default::default(), glyph_alloc_cache: Default::default(), }) } - /// Get the font weight (100-900) if available from the font file. - pub fn weight(&self) -> Option { - self.weight - } - /// Code points that will always be replaced by the replacement character. /// /// See also [`invisible_char`]. diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 8ca78064d..19876b571 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -861,26 +861,6 @@ impl FontsImpl { atlas: &mut self.atlas, } } - - /// Get the weight of a font by name, if available. - /// - /// Returns the weight value (100-900) read from the font file's OS/2 table, - /// or `None` if the font is not found or doesn't contain weight information. - /// - /// # Example - /// ``` - /// # use epaint::text::{FontDefinitions, FontsImpl}; - /// # use epaint::TextOptions; - /// let fonts_impl = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); - /// if let Some(weight) = fonts_impl.font_weight("Hack") { - /// println!("Hack font weight: {}", weight); - /// } - /// ``` - pub fn font_weight(&self, font_name: &str) -> Option { - let key = self.fonts_by_name.get(font_name)?; - let font_face = self.fonts_by_id.get(key)?; - font_face.weight() - } } // ---------------------------------------------------------------------------- From 457f8f446743f8b89b64e88d3244be759e7b7af2 Mon Sep 17 00:00:00 2001 From: Bayley Foster Date: Mon, 5 Jan 2026 19:27:00 +1030 Subject: [PATCH 03/14] plugin: export `TypedPluginGuard` and `TypedPluginHandle` (#7780) We have made this patch internally on our fork as we use `TypedPluginGuard` and `TypedPluginHandle` internally * [x] I have followed the instructions in the PR template --- crates/egui/src/lib.rs | 2 +- crates/egui/src/plugin.rs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 4f310ba8f..bc90f0cf9 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -413,7 +413,7 @@ pub mod os; mod painter; mod pass_state; pub(crate) mod placer; -mod plugin; +pub mod plugin; pub mod response; mod sense; pub mod style; diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index 3967e737d..d480cc770 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -55,6 +55,9 @@ pub(crate) struct PluginHandle { plugin: Box, } +/// A typed handle to a registered [`Plugin`]. +/// +/// Use [`Self::lock`] to access the plugin. pub struct TypedPluginHandle { handle: Arc>, _type: std::marker::PhantomData

, @@ -68,6 +71,9 @@ impl TypedPluginHandle

{ } } + /// Lock the plugin for access. + /// + /// Returns a guard that dereferences to the plugin. pub fn lock(&self) -> TypedPluginGuard<'_, P> { TypedPluginGuard { guard: self.handle.lock(), @@ -76,6 +82,7 @@ impl TypedPluginHandle

{ } } +/// A guard that provides access to a [`Plugin`]. pub struct TypedPluginGuard<'a, P: Plugin> { guard: MutexGuard<'a, PluginHandle>, _type: std::marker::PhantomData

, From 0a5cf3156e96584cfc87fd94dd27f462ecd76467 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 5 Jan 2026 03:44:21 -0700 Subject: [PATCH 04/14] Improve docstring for egui_extras features (#7813) It's nice to have * [x] I have followed the instructions in the PR template --- crates/egui/src/load.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index fd15322fb..99909eca1 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -3,7 +3,7 @@ //! If you just want to display some images, [`egui_extras`](https://crates.io/crates/egui_extras/) //! will get you up and running quickly with its reasonable default implementations of the traits described below. //! -//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature. +//! 1. Add [`egui_extras`](https://crates.io/crates/egui_extras/) as a dependency with the `all_loaders` feature (`cargo add egui_extras -F all_loaders`). //! 2. Add a call to [`egui_extras::install_image_loaders`](https://docs.rs/egui_extras/latest/egui_extras/fn.install_image_loaders.html) //! in your app's setup code. //! 3. Use [`Ui::image`][`crate::ui::Ui::image`] with some [`ImageSource`][`crate::ImageSource`]. From 06e491c5ec669c2301e5ce610f26d2da80d40e31 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 5 Jan 2026 04:11:14 -0700 Subject: [PATCH 05/14] feat: add documentation to structs (#7800) feat: add documentation to structs --- crates/egui/src/data/input.rs | 7 +++++++ crates/egui/src/input_state/mod.rs | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 61f6ae00c..787867569 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -608,6 +608,13 @@ pub const NUM_POINTER_BUTTONS: usize = 5; /// /// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`]. /// +/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function +/// +/// ```rust +/// # let ctx = egui::Context::default(); +/// let modifiers = ctx.input(|i| i.modifiers); +/// ``` +/// /// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers /// as on mac that is how you type special characters, /// so those key presses are usually not reported to egui. diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index bec4bec59..eaa4d3eb7 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -970,6 +970,15 @@ impl PointerEvent { } /// Mouse or touch state. +/// +/// To access the methods of [`PointerState`] you can use the [`crate::Context::input`] function +/// +/// ```rust +/// # let ctx = egui::Context::default(); +/// let latest_pos = ctx.input(|i| i.pointer.latest_pos()); +/// let is_pointer_down = ctx.input(|i| i.pointer.any_down()); +/// ``` +/// #[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct PointerState { From f9bf0ee6c43c3136cbde0084e8a09bf09e89fdac Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 5 Jan 2026 12:50:52 +0100 Subject: [PATCH 06/14] Don't focus Areas, Windows and ScrollAreas (#7827) Currently, tabbing through egui demo app, there are a lot of widgets that have invisible focus. Tabbing into a window for example takes 10 (!) presses of the tab key before the first widget within the window is focused. Before that, the focus moves to each resize handle, the scroll area and the scroll bar. At that point a user might think the focus is entirely broken. This pr removes the focusable sense from all these elements. Anything that can be focused should somehow indicate that it currently has focus, or the user could get frustrated. It also adds a debug flag to always show the focused widget, so it's easier to debug these cases --- crates/egui/src/containers/area.rs | 4 ++-- crates/egui/src/containers/scroll_area.rs | 4 ++-- crates/egui/src/containers/window.rs | 8 ++++---- crates/egui/src/context.rs | 9 ++++++++- crates/egui/src/sense.rs | 10 +++++++--- crates/egui/src/style.rs | 14 ++++++++++++++ 6 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 3b6d4006e..4333cf73a 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -505,9 +505,9 @@ impl Area { let interact_id = layer_id.id.with("move"); let sense = sense.unwrap_or_else(|| { if movable { - Sense::drag() + Sense::DRAG } else if interactable { - Sense::click() // allow clicks to bring to front + Sense::CLICK // allow clicks to bring to front } else { Sense::hover() } diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index afe1239c7..8f46223d2 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -810,7 +810,7 @@ impl ScrollArea { // or we will steal input from the widgets we contain. let content_response_option = state .interact_rect - .map(|rect| ui.interact(rect, id.with("area"), Sense::drag())); + .map(|rect| ui.interact(rect, id.with("area"), Sense::DRAG)); if content_response_option .as_ref() @@ -1276,7 +1276,7 @@ impl Prepared { }; let sense = if scroll_source.scroll_bar && ui.is_enabled() { - Sense::click_and_drag() + Sense::CLICK | Sense::DRAG } else { Sense::hover() }; diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 6dc7927ae..c6b739589 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -937,7 +937,7 @@ fn move_and_resize_window(ctx: &Context, id: Id, interaction: &ResizeInteraction fn do_resize_interaction( ctx: &Context, possible: PossibleInteractions, - _accessibility_parent: Id, + accessibility_parent: Id, layer_id: LayerId, outer_rect: Rect, window_frame: Frame, @@ -957,14 +957,14 @@ fn do_resize_interaction( let rect = outer_rect.shrink(window_frame.stroke.width / 2.0); let side_response = |rect, id| { - ctx.register_accesskit_parent(id, _accessibility_parent); + ctx.register_accesskit_parent(id, accessibility_parent); let response = ctx.create_widget( WidgetRect { layer_id, id, rect, interact_rect: rect, - sense: Sense::drag(), + sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable enabled: true, }, true, @@ -1324,7 +1324,7 @@ impl TitleBar { let id = ui.unique_id().with("__window_title_bar"); if ui - .interact(double_click_rect, id, Sense::click()) + .interact(double_click_rect, id, Sense::CLICK) .double_clicked() && collapsible { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index f8996b8a3..e28048edf 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2432,7 +2432,8 @@ impl Context { if let Some(widget) = self.write(|ctx| ctx.viewport().this_pass.widgets.get(id).copied()) { - paint_widget(&widget, text, color); + let text = format!("{text} - {id:?}"); + paint_widget(&widget, &text, color); } }; @@ -2541,6 +2542,12 @@ impl Context { } } + if self.global_style().debug.show_focused_widget + && let Some(focused_id) = self.memory(|mem| mem.focused()) + { + paint_widget_id(focused_id, "focused", Color32::PURPLE); + } + if let Some(debug_rect) = self.pass_state_mut(|fs| fs.debug_rect.take()) { debug_rect.paint(&self.debug_painter()); } diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index 464db311f..c3b3af7f2 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -53,19 +53,23 @@ impl Sense { Self::FOCUSABLE } - /// Sense clicks and hover, but not drags. + /// Sense clicks and hover, but not drags, and make the widget focusable. + /// + /// Use [`Sense::CLICK`] if you don't want the widget to be focusable. #[inline] pub fn click() -> Self { Self::CLICK | Self::FOCUSABLE } - /// Sense drags and hover, but not clicks. + /// Sense drags and hover, but not clicks. Make the widget focusable. + /// + /// Use [`Sense::DRAG`] if you don't want the widget to be focusable #[inline] pub fn drag() -> Self { Self::DRAG | Self::FOCUSABLE } - /// Sense both clicks, drags and hover (e.g. a slider or window). + /// Sense both clicks, drags and hover (e.g. a slider or window), and make the widget focusable. /// /// Note that this will introduce a latency when dragging, /// because when the user starts a press egui can't know if this is the start diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index f94b9d345..a555b9ace 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1303,6 +1303,13 @@ pub struct DebugOptions { /// /// See [`emath::GuiRounding`] for more. pub show_unaligned: bool, + + /// Highlight the currently focused widget. + /// + /// This is useful when some widget has a invisible focus (e.g. when a widget is using + /// `Sense::click()` when it should be using `Sense::CLICK`) and you need to find which one it + /// is. + pub show_focused_widget: bool, } #[cfg(debug_assertions)] @@ -1319,6 +1326,7 @@ impl Default for DebugOptions { show_interactive_widgets: false, show_widget_hits: false, show_unaligned: cfg!(debug_assertions), + show_focused_widget: false, } } } @@ -2480,6 +2488,7 @@ impl DebugOptions { show_interactive_widgets, show_widget_hits, show_unaligned, + show_focused_widget, } = self; { @@ -2514,6 +2523,11 @@ impl DebugOptions { "Show rectangles not aligned to integer point coordinates", ); + ui.checkbox( + show_focused_widget, + "Highlight which widget has keyboard focus", + ); + ui.vertical_centered(|ui| reset_button(ui, self, "Reset debug options")); } } From 6d416fab2e151ac181d1be024399f75b8aef9ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Mon, 5 Jan 2026 19:51:09 +0800 Subject: [PATCH 07/14] Fix backspacing leaving last character in IME prediction not removed on macOS native and Safari (#7810) * Closes N/A * [x] I have followed the instructions in the PR template ## Before the fix | Platform | Screenshot | | - | - | | macOS native | ![before-macos15-apple_shuangpin](https://github.com/user-attachments/assets/8397b236-7adf-4eca-9eb6-337e42c9efae) | | Safari | ![before-safari26](https://github.com/user-attachments/assets/1f4162a2-ccb7-4b42-960d-95aa3310f908) | ## After the fix | Platform | Screenshot | | - | - | | macOS native | ![after-macos15-apple_shuangpin](https://github.com/user-attachments/assets/8f50d43c-21bc-4c47-a7fb-86d0543c5088) | | Safari | ![after-safari26](https://github.com/user-attachments/assets/be4a69cd-8a0e-4512-865b-d6ebed2fd6c7) | (The font used in the screenshots is [GNU Unifont](https://unifoundry.com/unifont/index.html), licensed under [OFL-1.1.txt](https://unifoundry.com/OFL-1.1.txt).) --- crates/egui-winit/src/lib.rs | 138 +++++++++++++------ crates/egui/src/widgets/text_edit/builder.rs | 98 ++++++++----- 2 files changed, 159 insertions(+), 77 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 7cbaec624..a16865d17 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -352,45 +352,7 @@ impl State { } WindowEvent::Ime(ime) => { - // on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. - // So no need to check is_mac_cmd. - // - // How winit produce `Ime::Enabled` and `Ime::Disabled` differs in MacOS - // and Windows. - // - // - On Windows, before and after each Commit will produce an Enable/Disabled - // event. - // - On MacOS, only when user explicit enable/disable ime. No Disabled - // after Commit. - // - // We use input_method_editor_started to manually insert CompositionStart - // between Commits. - match ime { - winit::event::Ime::Enabled => { - if cfg!(target_os = "linux") { - // This event means different things in X11 and Wayland, but we can just - // ignore it and enable IME on the preedit event. - // See - } else { - self.ime_event_enable(); - } - } - winit::event::Ime::Preedit(text, Some(_cursor)) => { - self.ime_event_enable(); - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); - } - winit::event::Ime::Commit(text) => { - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); - self.ime_event_disable(); - } - winit::event::Ime::Disabled | winit::event::Ime::Preedit(_, None) => { - self.ime_event_disable(); - } - } + self.on_ime(ime); EventResponse { repaint: true, @@ -564,6 +526,104 @@ impl State { } } + /// ## NOTE + /// + /// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. + /// So no need to check `is_mac_cmd`. + /// + /// ### How events are emitted by [`winit`] across different setups in various situations + /// + /// This is done by uncommenting the code block at the top of this method + /// and checking console outputs. + /// + /// winit version: 0.30.12. + /// + /// #### Setups + /// + /// - `a-macos15-apple_shuangpin`: macOS 15.7.3 `aarch64`, IME: builtin Chinese Shuangpin - Simplified. (Demo app shows: renderer: `wgpu`, backend: `Metal`.) + /// - `b-debian13_gnome48_wayland-fcitx5_shuangpin`: Debian 13 `aarch64`, Gnome 48, Wayland, IME: Fcitx5 with fcitx5-chinese-addons's Shuangpin. (Demo app shows: renderer: `wgpu`, backend: `Gl`.) + /// - `c-windows11-ms_pinyin`: Windows11 23H2 `x86_64`, IME: builtin Microsoft Pinyin. (Demo app shows: renderer: `wgpu`, backend: `Vulkan` & `Dx12`, others: `Dx12` & `Gl`.) + /// + /// #### Situation: pressed space to select the first candidate "测试" + /// + /// | Setup | Events in Order | + /// | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | + /// | a-macos15-apple_shuangpin | `Predict("", None)` -> `Commit("测试")` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", None)` -> `Commit("测试")` -> `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Predict("测试", Some(…))` -> `Predict("", None)` -> `Commit("测试")` -> `Disabled` | + /// + /// #### Situation: pressed backspace to delete the last character in the prediction + /// + /// | Setup | Events in Order | + /// | a-macos15-apple_shuangpin | `Predict("", None)` | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | `Predict("", Some(0, 0))` -> `Predict("", None)` -> `Commit("")` -> `Disabled` | + /// + /// #### Situation: clicked somewhere else while there is an active composition with the prediction "ce" + /// + /// | Setup | Events in Order | + /// | ------------------------------------------- | ------------------------------------------------------------------------------------------------- | + /// | a-macos15-apple_shuangpin | nothing emitted | + /// | b-debian13_gnome48_wayland-fcitx5_shuangpin | `Predict("", Some(0, 0))` (duplicate) -> `Predict("", None)` (duplicate until `TextEdit` blurred) | + /// | c-windows11-ms_pinyin | nothing emitted | + fn on_ime(&mut self, ime: &winit::event::Ime) { + // // code for inspecting ime events emitted by winit: + // { + // static LAST_IME: std::sync::Mutex> = + // std::sync::Mutex::new(None); + // static IS_LAST_DUPLICATE: std::sync::atomic::AtomicBool = + // std::sync::atomic::AtomicBool::new(false); + // let mut last_ime_guard = LAST_IME.lock().unwrap(); + // if { last_ime_guard.as_ref().cloned() }.as_ref() != Some(ime) { + // println!("IME={ime:?}"); + // *last_ime_guard = Some(ime.clone()); + // IS_LAST_DUPLICATE.store(false, std::sync::atomic::Ordering::Relaxed); + // } else if !IS_LAST_DUPLICATE.load(std::sync::atomic::Ordering::Relaxed) { + // println!("IME=(duplicate)"); + // IS_LAST_DUPLICATE.store(true, std::sync::atomic::Ordering::Relaxed); + // } + // } + + match ime { + winit::event::Ime::Enabled => { + if cfg!(target_os = "linux") { + // This event means different things in X11 and Wayland, but we can just + // ignore it and enable IME on the preedit event. + // See + } else { + self.ime_event_enable(); + } + } + winit::event::Ime::Preedit(text, Some(_cursor)) => { + self.ime_event_enable(); + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); + } + winit::event::Ime::Commit(text) => { + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); + self.ime_event_disable(); + } + winit::event::Ime::Disabled => { + self.ime_event_disable(); + } + winit::event::Ime::Preedit(_, None) => { + // we need to emit this on macOS, since winit doesn't emit + // `Predict("", Some(0, 0))` before this event on macOS when the + // user deletes the last character in the prediction with the + // backspace key. Without this, only `egui::ImeEvent::Disabled` + // is emitted here, leading to the last character being left in + // TextEdit in such situation. + self.egui_input + .events + .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); + self.ime_event_disable(); + } + } + } + pub fn ime_event_enable(&mut self) { if !self.has_sent_ime_enabled { self.egui_input diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 2d4f79516..fbf25babf 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1065,51 +1065,73 @@ fn events( .. } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), - Event::Ime(ime_event) => match ime_event { - ImeEvent::Enabled => { - state.ime_enabled = true; - state.ime_cursor_range = cursor_range; - None + Event::Ime(ime_event) => { + /// Empty prediction can be produced with [`ImeEvent::Preedit`] + /// or [`ImeEvent::Commit`] when user press backspace or escape + /// during IME, so this function should be called in both cases + /// to clear current text. + /// + /// Example platforms where only `ImeEvent::Preedit("")` of + /// those two events is emitted when the last character in the + /// prediction is deleted: + /// - macOS 15.7.3. + /// - Debian13 with gnome48 and wayland. + /// + /// An example platform where only `ImeEvent::Commit("")` of + /// those two events is emitted when the last character in the + /// prediction is deleted: + /// - Safari 26.2 (on macOS 15.7.3). + fn clear_prediction( + text: &mut dyn TextBuffer, + cursor_range: &CCursorRange, + ) -> CCursor { + text.delete_selected(cursor_range) } - ImeEvent::Preedit(text_mark) => { - if text_mark == "\n" || text_mark == "\r" { - None - } else { - // Empty prediction can be produced when user press backspace - // or escape during IME, so we clear current text. - let mut ccursor = text.delete_selected(&cursor_range); - let start_cursor = ccursor; - if !text_mark.is_empty() { - text.insert_text_at(&mut ccursor, text_mark, char_limit); - } - state.ime_cursor_range = cursor_range; - Some(CCursorRange::two(start_cursor, ccursor)) - } - } - ImeEvent::Commit(prediction) => { - if prediction == "\n" || prediction == "\r" { - None - } else { - state.ime_enabled = false; - if !prediction.is_empty() - && cursor_range.secondary.index - == state.ime_cursor_range.secondary.index - { - let mut ccursor = text.delete_selected(&cursor_range); - text.insert_text_at(&mut ccursor, prediction, char_limit); - Some(CCursorRange::one(ccursor)) + match ime_event { + ImeEvent::Enabled => { + state.ime_enabled = true; + state.ime_cursor_range = cursor_range; + None + } + ImeEvent::Preedit(text_mark) => { + if text_mark == "\n" || text_mark == "\r" { + None } else { - let ccursor = cursor_range.primary; + let mut ccursor = clear_prediction(text, &cursor_range); + + let start_cursor = ccursor; + if !text_mark.is_empty() { + text.insert_text_at(&mut ccursor, text_mark, char_limit); + } + state.ime_cursor_range = cursor_range; + Some(CCursorRange::two(start_cursor, ccursor)) + } + } + ImeEvent::Commit(prediction) => { + if prediction == "\n" || prediction == "\r" { + None + } else { + state.ime_enabled = false; + + let mut ccursor = clear_prediction(text, &cursor_range); + + if !prediction.is_empty() + && cursor_range.secondary.index + == state.ime_cursor_range.secondary.index + { + text.insert_text_at(&mut ccursor, prediction, char_limit); + } + Some(CCursorRange::one(ccursor)) } } + ImeEvent::Disabled => { + state.ime_enabled = false; + None + } } - ImeEvent::Disabled => { - state.ime_enabled = false; - None - } - }, + } _ => None, }; From 0566fb17a573948f6a4c5bd7a3c7b7139b1d219f Mon Sep 17 00:00:00 2001 From: Ivor Wanders Date: Mon, 5 Jan 2026 07:16:00 -0500 Subject: [PATCH 08/14] Allow specifying override_redirect x11 window attribute (#7830) --- crates/egui-winit/src/lib.rs | 6 +++++- crates/egui/src/viewport.rs | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index a16865d17..54059cbd6 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1728,6 +1728,7 @@ pub fn create_winit_window_attributes( // x11 window_type: _window_type, + override_redirect: _override_redirect, mouse_passthrough: _, // handled in `apply_viewport_builder_to_window` clamp_size_to_monitor_size: _, // Handled in `viewport_builder` in `epi_integration.rs` @@ -1827,8 +1828,8 @@ pub fn create_winit_window_attributes( #[cfg(all(feature = "x11", target_os = "linux"))] { + use winit::platform::x11::WindowAttributesExtX11 as _; if let Some(window_type) = _window_type { - use winit::platform::x11::WindowAttributesExtX11 as _; use winit::platform::x11::WindowType; window_attributes = window_attributes.with_x11_window_type(vec![match window_type { egui::X11WindowType::Normal => WindowType::Normal, @@ -1847,6 +1848,9 @@ pub fn create_winit_window_attributes( egui::X11WindowType::Dnd => WindowType::Dnd, }]); } + if let Some(override_redirect) = _override_redirect { + window_attributes = window_attributes.with_override_redirect(override_redirect); + } } #[cfg(target_os = "windows")] diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 15741c017..ef6ee6c63 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -332,6 +332,7 @@ pub struct ViewportBuilder { // X11 pub window_type: Option, + pub override_redirect: Option, } impl ViewportBuilder { @@ -663,13 +664,22 @@ impl ViewportBuilder { /// ### On X11 /// This sets the window type. - /// Maps directly to [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). + /// Maps directly to [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm/1.5/ar01s05.html#id-1.6.7). #[inline] pub fn with_window_type(mut self, value: X11WindowType) -> Self { self.window_type = Some(value); self } + /// ### On X11 + /// This sets the override-redirect flag. When this is set to true the window type should be specified. + /// Maps directly to [`Override-redirect windows`](https://specifications.freedesktop.org/wm/1.5/ar01s02.html#id-1.3.13). + #[inline] + pub fn with_override_redirect(mut self, value: bool) -> Self { + self.override_redirect = Some(value); + self + } + /// Update this `ViewportBuilder` with a delta, /// returning a list of commands and a bool indicating if the window needs to be recreated. #[must_use] @@ -706,6 +716,7 @@ impl ViewportBuilder { mouse_passthrough: new_mouse_passthrough, taskbar: new_taskbar, window_type: new_window_type, + override_redirect: new_override_redirect, } = new_vp_builder; let mut commands = Vec::new(); @@ -903,6 +914,11 @@ impl ViewportBuilder { recreate_window = true; } + if new_override_redirect.is_some() && self.override_redirect != new_override_redirect { + self.override_redirect = new_override_redirect; + recreate_window = true; + } + (commands, recreate_window) } } From f26d7d8072379a5ffa9edc53ca4fabe4b25089d8 Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 5 Jan 2026 09:54:18 -0700 Subject: [PATCH 09/14] fix: CodeTheme functions - ui and add builder (#7684) Following this MR https://github.com/emilk/egui/pull/7375 Without Syntect, the urrent theme selector is `global_theme_preference_buttons`. It should be the dark_theme variable of the local CodeTheme - same as syntect Tested with ```sh cargo run # and cargo run --features syntect ``` * [x] I have followed the instructions in the PR template thanks for this amazing library! --- crates/egui_demo_lib/src/demo/code_editor.rs | 28 ++++++++---- crates/egui_extras/src/syntax_highlighting.rs | 45 +++++++++++++------ 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index a8a82e200..869194114 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -90,15 +90,25 @@ impl crate::View for CodeEditor { }; egui::ScrollArea::vertical().show(ui, |ui| { - ui.add( - egui::TextEdit::multiline(code) - .font(egui::TextStyle::Monospace) // for cursor height - .code_editor() - .desired_rows(10) - .lock_focus(true) - .desired_width(f32::INFINITY) - .layouter(&mut layouter), - ); + let editor = egui::TextEdit::multiline(code) + .font(egui::TextStyle::Monospace) // for cursor height + .code_editor() + .desired_rows(10) + .lock_focus(true) + .desired_width(f32::INFINITY) + .layouter(&mut layouter); + let editor = if cfg!(feature = "syntect") { + editor + } else { + use egui::Color32; + let background_color = if theme.is_dark() { + Color32::BLACK + } else { + Color32::WHITE + }; + editor.background_color(background_color) + }; + ui.add(editor); }); } } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 0b9a791ea..5f41db9c8 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -228,6 +228,10 @@ impl Default for CodeTheme { } impl CodeTheme { + pub fn is_dark(&self) -> bool { + self.dark_mode + } + /// Selects either dark or light theme based on the given style. pub fn from_style(style: &egui::Style) -> Self { let font_id = style @@ -315,6 +319,24 @@ impl CodeTheme { #[cfg(feature = "syntect")] impl CodeTheme { + /// Change the font size + pub fn with_font_size(&self, font_size: f32) -> Self { + Self { + dark_mode: self.dark_mode, + syntect_theme: self.syntect_theme, + font_id: egui::FontId::monospace(font_size), + } + } + + /// Change the `font_id` of the theme + pub fn with_font_id(&self, font_id: egui::FontId) -> Self { + Self { + dark_mode: self.dark_mode, + syntect_theme: self.syntect_theme, + font_id, + } + } + fn dark_with_font_id(font_id: egui::FontId) -> Self { Self { dark_mode: true, @@ -331,10 +353,6 @@ impl CodeTheme { } } - pub fn is_dark(&self) -> bool { - self.dark_mode - } - /// Show UI for changing the color theme. pub fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal(|ui| { @@ -344,11 +362,9 @@ impl CodeTheme { ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme") .on_hover_text("Use the light mode theme"); }); - - for theme in SyntectTheme::all() { - if theme.is_dark() == self.dark_mode { - ui.radio_value(&mut self.syntect_theme, theme, theme.name()); - } + let current_theme_is_dark = self.is_dark(); + for theme in SyntectTheme::all().filter(|t| t.is_dark() == current_theme_is_dark) { + ui.radio_value(&mut self.syntect_theme, theme, theme.name()); } } } @@ -408,12 +424,13 @@ impl CodeTheme { ui.vertical(|ui| { ui.set_width(150.0); - egui::widgets::global_theme_preference_buttons(ui); - - ui.add_space(8.0); - ui.separator(); - ui.add_space(8.0); + ui.horizontal(|ui| { + ui.selectable_value(&mut self.dark_mode, true, "🌙 Dark theme") + .on_hover_text("Use the dark mode theme"); + ui.selectable_value(&mut self.dark_mode, false, "☀ Light theme") + .on_hover_text("Use the light mode theme"); + }); ui.scope(|ui| { for (tt, tt_name) in [ (TokenType::Comment, "// comment"), From f8e763378a52b90ae418fc17af3482a131629328 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jan 2026 18:33:38 +0100 Subject: [PATCH 10/14] Remove `CacheTrait::as_any_mut` (#7833) * Part of https://github.com/emilk/egui/issues/5876 No longer needed on modern MSRV --- crates/egui/src/cache/cache_storage.rs | 10 ++++++---- crates/egui/src/cache/cache_trait.rs | 4 +--- crates/egui/src/cache/frame_cache.rs | 4 ---- crates/egui/src/cache/frame_publisher.rs | 4 ---- crates/egui/src/plugin.rs | 4 ++-- 5 files changed, 9 insertions(+), 17 deletions(-) diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs index 255eca2d5..26e7d04ba 100644 --- a/crates/egui/src/cache/cache_storage.rs +++ b/crates/egui/src/cache/cache_storage.rs @@ -28,11 +28,13 @@ pub struct CacheStorage { impl CacheStorage { pub fn cache(&mut self) -> &mut Cache { - #[expect(clippy::unwrap_used)] - self.caches + let cache = self + .caches .entry(std::any::TypeId::of::()) - .or_insert_with(|| Box::::default()) - .as_any_mut() + .or_insert_with(|| Box::::default()); + + #[expect(clippy::unwrap_used)] + (cache.as_mut() as &mut dyn std::any::Any) .downcast_mut::() .unwrap() } diff --git a/crates/egui/src/cache/cache_trait.rs b/crates/egui/src/cache/cache_trait.rs index fc00a9880..54144c724 100644 --- a/crates/egui/src/cache/cache_trait.rs +++ b/crates/egui/src/cache/cache_trait.rs @@ -1,11 +1,9 @@ /// A cache, storing some value for some length of time. #[expect(clippy::len_without_is_empty)] -pub trait CacheTrait: 'static + Send + Sync { +pub trait CacheTrait: 'static + Send + Sync + std::any::Any { /// Call once per frame to evict cache. fn update(&mut self); /// Number of values currently in the cache. fn len(&self) -> usize; - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any; } diff --git a/crates/egui/src/cache/frame_cache.rs b/crates/egui/src/cache/frame_cache.rs index 6c74c58dc..4c8d25f3e 100644 --- a/crates/egui/src/cache/frame_cache.rs +++ b/crates/egui/src/cache/frame_cache.rs @@ -79,8 +79,4 @@ impl CacheTrait fn len(&self) -> usize { self.cache.len() } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } } diff --git a/crates/egui/src/cache/frame_publisher.rs b/crates/egui/src/cache/frame_publisher.rs index 0c2bc81d6..81ba34df6 100644 --- a/crates/egui/src/cache/frame_publisher.rs +++ b/crates/egui/src/cache/frame_publisher.rs @@ -54,8 +54,4 @@ where fn len(&self) -> usize { self.cache.len() } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } } diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index d480cc770..6bef04123 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -120,13 +120,13 @@ impl PluginHandle { } fn typed_plugin(&self) -> &P { - (&*self.plugin as &dyn std::any::Any) + (self.plugin.as_ref() as &dyn std::any::Any) .downcast_ref::

() .expect("PluginHandle: plugin is not of the expected type") } pub fn typed_plugin_mut(&mut self) -> &mut P { - (&mut *self.plugin as &mut dyn std::any::Any) + (self.plugin.as_mut() as &mut dyn std::any::Any) .downcast_mut::

() .expect("PluginHandle: plugin is not of the expected type") } From a9e92525c01e90417b431af9a4ea9db4d3dd6179 Mon Sep 17 00:00:00 2001 From: Belu Antonie-Gabriel Date: Wed, 7 Jan 2026 17:20:16 +0200 Subject: [PATCH 11/14] Implemented distance threshold for double/triple clicks (#7817) This change introduces a distance check to double and triple clicks. Previously, double/triple clicks were determined solely by timing, allowing clicks on different UI elements to trigger a double_clicked()/triple_clicked() event. By requiring consecutive clicks to occur within a specific radius (max_multiple_click_dist), we prevent double_clicked()/triple_clicked() events from triggering when a user clicks on two different, distant UI elements in rapid succession. * Closes * [x] I have followed the instructions in the PR template --- crates/egui/src/input_state/mod.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index eaa4d3eb7..49027c06e 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -1037,6 +1037,10 @@ pub struct PointerState { /// This could also be the trigger point for a long-touch. pub(crate) started_decidedly_dragging: bool, + /// Where did the last click originate? + /// `None` if no mouse click occurred. + last_click_pos: Option, + /// When did the pointer get click last? /// Used to check for double-clicks. last_click_time: f64, @@ -1074,6 +1078,7 @@ impl Default for PointerState { press_start_time: None, has_moved_too_much_for_a_click: false, started_decidedly_dragging: false, + last_click_pos: None, last_click_time: f64::NEG_INFINITY, last_last_click_time: f64::NEG_INFINITY, last_move_time: f64::NEG_INFINITY, @@ -1149,10 +1154,18 @@ impl PointerState { let clicked = self.could_any_button_be_click(); let click = if clicked { - let double_click = - (time - self.last_click_time) < self.options.max_double_click_delay; + let click_dist_sq = self + .last_click_pos + .map_or(0.0, |last_pos| last_pos.distance_sq(pos)); + + let double_click = (time - self.last_click_time) + < self.options.max_double_click_delay + && click_dist_sq + < self.options.max_click_dist * self.options.max_click_dist; let triple_click = (time - self.last_last_click_time) - < (self.options.max_double_click_delay * 2.0); + < (self.options.max_double_click_delay * 2.0) + && click_dist_sq + < self.options.max_click_dist * self.options.max_click_dist; let count = if triple_click { 3 } else if double_click { @@ -1163,6 +1176,7 @@ impl PointerState { self.last_last_click_time = self.last_click_time; self.last_click_time = time; + self.last_click_pos = Some(pos); Some(Click { pos, @@ -1630,6 +1644,7 @@ impl PointerState { press_start_time, has_moved_too_much_for_a_click, started_decidedly_dragging, + last_click_pos, last_click_time, last_last_click_time, pointer_events, @@ -1655,6 +1670,7 @@ impl PointerState { ui.label(format!( "started_decidedly_dragging: {started_decidedly_dragging}" )); + ui.label(format!("last_click_pos: {last_click_pos:#?}")); ui.label(format!("last_click_time: {last_click_time:#?}")); ui.label(format!("last_last_click_time: {last_last_click_time:#?}")); ui.label(format!("last_move_time: {last_move_time:#?}")); From 7fb4627dadd87d4f49419a5cace65cb10ce968ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C6=B0=C6=A1ng=20Ho=C3=A0ng=20Long?= Date: Sun, 11 Jan 2026 16:31:58 +0100 Subject: [PATCH 12/14] Make `FrameCache::get` return a reference instead of cloning the cached value (#7834) This is a breaking change. - Enables using `FrameCache` in cases where the cached value cannot be cloned. - Improves use cases where only a reference to the cached value is needed. - If the user needs an owned value, they can clone it themselves. Adding a `get_ref` method instead of changing `get` would avoid the breaking change, but I didn't want to do so because it is kind of expected for `get` to return `&V` when querying a collection. --- crates/egui/src/cache/cache_storage.rs | 2 +- crates/egui/src/cache/frame_cache.rs | 9 ++++----- crates/egui/src/memory/mod.rs | 2 +- crates/egui_extras/src/syntax_highlighting.rs | 1 + 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs index 26e7d04ba..e0aa65e8c 100644 --- a/crates/egui/src/cache/cache_storage.rs +++ b/crates/egui/src/cache/cache_storage.rs @@ -19,7 +19,7 @@ use super::CacheTrait; /// /// # let mut cache_storage = CacheStorage::default(); /// let mut cache = cache_storage.cache::>(); -/// assert_eq!(cache.get("hello"), 5); +/// assert_eq!(*cache.get("hello"), 5); /// ``` #[derive(Default)] pub struct CacheStorage { diff --git a/crates/egui/src/cache/frame_cache.rs b/crates/egui/src/cache/frame_cache.rs index 4c8d25f3e..ae39712c7 100644 --- a/crates/egui/src/cache/frame_cache.rs +++ b/crates/egui/src/cache/frame_cache.rs @@ -46,10 +46,9 @@ impl FrameCache { impl FrameCache { /// Get from cache (if the same key was used last frame) /// or recompute and store in the cache. - pub fn get(&mut self, key: Key) -> Value + pub fn get(&mut self, key: Key) -> &Value where Key: Copy + std::hash::Hash, - Value: Clone, Computer: ComputerMut, { let hash = crate::util::hash(key); @@ -58,12 +57,12 @@ impl FrameCache { std::collections::hash_map::Entry::Occupied(entry) => { let cached = entry.into_mut(); cached.0 = self.generation; - cached.1.clone() + &cached.1 } std::collections::hash_map::Entry::Vacant(entry) => { let value = self.computer.compute(key); - entry.insert((self.generation, value.clone())); - value + let inserted = entry.insert((self.generation, value)); + &inserted.1 } } } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index defb14a39..9c91c1322 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -68,7 +68,7 @@ pub struct Memory { /// # let mut ctx = egui::Context::default(); /// ctx.memory_mut(|mem| { /// let cache = mem.caches.cache::>(); - /// assert_eq!(cache.get("hello"), 5); + /// assert_eq!(*cache.get("hello"), 5); /// }); /// ``` #[cfg_attr(feature = "persistence", serde(skip))] diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 5f41db9c8..adf2c9221 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -116,6 +116,7 @@ fn highlight_inner( mem.caches .cache::() .get((&font_id, theme, code, language, settings)) + .clone() }) } From 73b7b9e225f6e57aec1bc697ee7d95a27fdd0625 Mon Sep 17 00:00:00 2001 From: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:22:14 +0000 Subject: [PATCH 13/14] Update snapshot images --- tests/egui_tests/tests/snapshots/text_edit_halign.png | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/egui_tests/tests/snapshots/text_edit_halign.png diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png new file mode 100644 index 000000000..5c56f1b19 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f60036a5af9b376cbf1fefaf8088ce41cfd0c19ec16f02b1d8b98c3e987972e +size 13276 From b3ffbca2ab01b31751d10685fc4d3ac463e4d9a9 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 13 Jan 2026 11:39:53 +0100 Subject: [PATCH 14/14] Prevent snapshot update workflow to run on main (#7842) Turns out if you don't give the gh workflow dispatch a ref it runs it on main, oops --- .github/workflows/update_kittest_snapshots.yml | 1 + tests/egui_tests/tests/snapshots/text_edit_halign.png | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 tests/egui_tests/tests/snapshots/text_edit_halign.png diff --git a/.github/workflows/update_kittest_snapshots.yml b/.github/workflows/update_kittest_snapshots.yml index 9c67c52a9..bab1192d2 100644 --- a/.github/workflows/update_kittest_snapshots.yml +++ b/.github/workflows/update_kittest_snapshots.yml @@ -14,6 +14,7 @@ jobs: update-snapshots: name: Update snapshots from artifact runs-on: ubuntu-latest + if: github.ref_name != 'main' # We never want to update snapshots directly on main permissions: contents: write diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png deleted file mode 100644 index 5c56f1b19..000000000 --- a/tests/egui_tests/tests/snapshots/text_edit_halign.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3f60036a5af9b376cbf1fefaf8088ce41cfd0c19ec16f02b1d8b98c3e987972e -size 13276