From c09a8723b43abe43cdb3478711a433bda11cd46e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 17 Mar 2026 17:08:18 +0100 Subject: [PATCH 01/89] Fix vulnerability in the branch name check workflow (#7982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before, a crafted branch name could be used to exfiltrate the github token and wreak havoc šŸ˜… --- .github/workflows/enforce_branch_name.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/enforce_branch_name.yml b/.github/workflows/enforce_branch_name.yml index 8c2b28d37..b9df4030d 100644 --- a/.github/workflows/enforce_branch_name.yml +++ b/.github/workflows/enforce_branch_name.yml @@ -4,17 +4,23 @@ on: pull_request_target: types: [opened, reopened, synchronize] +permissions: + issues: write + jobs: check-source-branch: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Check PR source branch + env: + IS_FORK: ${{ github.event.pull_request.head.repo.fork }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | # Check if PR is from a fork - if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + if [[ "$IS_FORK" == "true" ]]; then # Check if PR is from the master/main branch of a fork - if [[ "${{ github.event.pull_request.head.ref }}" == "master" || "${{ github.event.pull_request.head.ref }}" == "main" ]]; then + if [[ "$HEAD_REF" == "master" || "$HEAD_REF" == "main" ]]; then echo "ERROR: Pull requests from the master/main branch of forks are not allowed, because it prevents maintainers from contributing to your PR" echo "Please create a feature branch in your fork and submit the PR from that branch instead." exit 1 From 543e7204ba39615c4cc7ca7713e6bb8480a8d6f3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 18 Mar 2026 14:58:10 +0100 Subject: [PATCH 02/89] Update lz4_flex dependency --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 800618e4a..896a36665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2592,9 +2592,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lz4_flex" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" +checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" [[package]] name = "malloc_buf" From 265cf7ebaeba8ef319679981baf7aece5042f373 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 18 Mar 2026 15:08:06 +0100 Subject: [PATCH 03/89] Add `DebugOptions::warn_if_rect_changes_id` (#7984) If turned on (default in debug builds), if the exact same `Rect` exist in two subsequent frames but with different `Id`s, a warning is logged an a red rect is flashed on the screen. --- crates/egui/src/context.rs | 110 +++++++++++++++++++++ crates/egui/src/style.rs | 10 ++ tests/egui_tests/tests/regression_tests.rs | 45 +++++++++ 3 files changed, 165 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 3b36150e4..30818a772 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2636,6 +2636,19 @@ impl ContextImpl { } } + #[cfg(debug_assertions)] + let shapes = if self.memory.options.style().debug.warn_if_rect_changes_id { + let mut shapes = shapes; + warn_if_rect_changes_id( + &mut shapes, + &viewport.prev_pass.widgets, + &viewport.this_pass.widgets, + ); + shapes + } else { + shapes + }; + std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass); if repaint_needed { @@ -4237,6 +4250,103 @@ fn context_impl_send_sync() { assert_send_sync::(); } +/// Check if any [`Rect`] appears with different [`Id`]s between two passes. +/// +/// This helps detect cases where the same screen area is claimed by different widget ids +/// across passes, which is often a sign of id instability. +#[cfg(debug_assertions)] +fn warn_if_rect_changes_id( + out_shapes: &mut Vec, + prev_widgets: &crate::WidgetRects, + new_widgets: &crate::WidgetRects, +) { + profiling::function_scope!(); + + use std::collections::BTreeMap; + + /// A wrapper around [`Rect`] that implements [`Ord`] using the bit representation of its floats. + #[derive(Clone, Copy, PartialEq, Eq)] + struct OrderedRect(Rect); + + impl PartialOrd for OrderedRect { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for OrderedRect { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let lhs = self.0; + let rhs = other.0; + lhs.min + .x + .to_bits() + .cmp(&rhs.min.x.to_bits()) + .then(lhs.min.y.to_bits().cmp(&rhs.min.y.to_bits())) + .then(lhs.max.x.to_bits().cmp(&rhs.max.x.to_bits())) + .then(lhs.max.y.to_bits().cmp(&rhs.max.y.to_bits())) + } + } + + fn create_lookup<'a>( + widgets: impl Iterator, + ) -> BTreeMap> { + let mut lookup: BTreeMap> = BTreeMap::default(); + for w in widgets { + lookup.entry(OrderedRect(w.rect)).or_default().push(w); + } + lookup + } + + for (layer_id, new_layer_widgets) in new_widgets.layers() { + let prev = create_lookup(prev_widgets.get_layer(*layer_id)); + let new = create_lookup(new_layer_widgets.iter()); + + for (hashable_rect, new_at_rect) in new { + let Some(prev_at_rect) = prev.get(&hashable_rect) else { + continue; // this rect did not exist in the previous pass + }; + + if prev_at_rect + .iter() + .any(|w| new_at_rect.iter().any(|nw| nw.id == w.id)) + { + continue; // at least one id stayed the same, so this is not an id change + } + + // Only warn if at least one of the previous ids is gone from this layer entirely. + // If they all still exist (just at a different rect), then the rect match + // is just a coincidence caused by widgets shifting (e.g. a window being dragged). + if prev_at_rect.iter().all(|w| new_widgets.contains(w.id)) { + continue; + } + + let rect = new_at_rect[0].rect; + + log::warn!( + "Widget rect {rect:?} changed id between passes: prev ids: {:?}, new ids: {:?}", + prev_at_rect + .iter() + .map(|w| w.id.short_debug_format()) + .collect::>(), + new_at_rect + .iter() + .map(|w| w.id.short_debug_format()) + .collect::>(), + ); + out_shapes.push(ClippedShape { + clip_rect: Rect::EVERYTHING, + shape: epaint::Shape::rect_stroke( + rect, + 0, + (2.0, Color32::RED), + StrokeKind::Outside, + ), + }); + } + } +} + #[cfg(test)] mod test { use super::Context; diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 56a347f0d..5cd980f6c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1303,6 +1303,9 @@ pub struct DebugOptions { /// Show interesting widgets under the mouse cursor. pub show_widget_hits: bool, + /// Show a warning if the same `Rect` had different `Id` on the previous frame. + pub warn_if_rect_changes_id: bool, + /// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`]. /// /// See [`emath::GuiRounding`] for more. @@ -1329,6 +1332,7 @@ impl Default for DebugOptions { show_resize: false, show_interactive_widgets: false, show_widget_hits: false, + warn_if_rect_changes_id: cfg!(debug_assertions), show_unaligned: cfg!(debug_assertions), show_focused_widget: false, } @@ -2491,6 +2495,7 @@ impl DebugOptions { show_resize, show_interactive_widgets, show_widget_hits, + warn_if_rect_changes_id, show_unaligned, show_focused_widget, } = self; @@ -2522,6 +2527,11 @@ impl DebugOptions { ui.checkbox(show_widget_hits, "Show widgets under mouse pointer"); + ui.checkbox( + warn_if_rect_changes_id, + "Warn if a Rect changes Id between frames", + ); + ui.checkbox( show_unaligned, "Show rectangles not aligned to integer point coordinates", diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 9e76394cb..421b69d35 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,4 +1,5 @@ use egui::accesskit::Role; +use egui::epaint::Shape; use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image}; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -118,3 +119,47 @@ fn interact_on_ui_response_should_be_stable() { drop(harness); assert_eq!(click_count, 10, "We missed some clicks!"); } + +fn has_red_warning_rect(output: &egui::FullOutput) -> bool { + output.shapes.iter().any(|clipped| { + matches!( + &clipped.shape, + Shape::Rect(rect_shape) + if rect_shape.stroke.color == Color32::RED + ) + }) +} + +/// A button that changes its text on hover, with the Id derived from the text. +/// This is a plausible bug: the widget keeps the same rect, but its Id changes +/// between frames because the label (and thus the Id salt) changes on hover. +/// The `warn_if_rect_changes_id` debug check should catch this. +#[test] +fn warn_if_rect_changes_id() { + let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0)); + + let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| { + // Simulate a buggy widget whose Id depends on its label text, + // and the label changes on hover: + let is_hovered = ui.rect_contains_pointer(button_rect); + let label = if is_hovered { "Hovering!" } else { "Click me" }; + let id = ui.id().with(label); + let _response = ui.interact(button_rect, id, Sense::click()); + }); + + // no hover — establishes stable prev_pass + harness.step(); + assert!( + !has_red_warning_rect(harness.output()), + "Should not warn without hover" + ); + + // Move the pointer over the button + harness.hover_at(button_rect.center()); + + harness.step(); + assert!( + has_red_warning_rect(harness.output()), + "Should warn when a widget rect changes Id between passes" + ); +} From ad510257de5a03eb0889cb183a170162799c5dee Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 18 Mar 2026 15:24:19 +0100 Subject: [PATCH 04/89] Quit on Ctrl-Q (#7985) This adds `Ctrl-Q` as the default shortcut for closing the current egui `Viewport`, which also means closing the entire application if you are in the root viewport. Can be configured by `egui::Options::quit_shortcuts` On Mac, `cmd-Q` already triggers a quit, but not on all Linux:es. --- crates/egui/src/context.rs | 6 ++++++ crates/egui/src/memory/mod.rs | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 30818a772..fbc189132 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2397,6 +2397,12 @@ impl Context { crate::gui_zoom::zoom_with_keyboard(self); } + for shortcut in self.options(|o| o.quit_shortcuts.clone()) { + if self.input_mut(|i| i.consume_shortcut(&shortcut)) { + self.send_viewport_cmd(ViewportCommand::Close); + } + } + #[cfg(debug_assertions)] self.debug_painting(); diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 51ab2cde4..08b08a462 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -234,6 +234,16 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub zoom_with_keyboard: bool, + /// Keyboard shortcuts to close the application. + /// + /// Pressing any of these will send [`crate::ViewportCommand::Close`] + /// to the root viewport. + /// + /// Defaults to `Cmd-Q` (which is Ctrl-Q on Linux/Windows, Cmd-Q on Mac). + /// Set to empty to disable. + #[cfg_attr(feature = "serde", serde(skip))] + pub quit_shortcuts: Vec, + /// Controls the tessellator. pub tessellation_options: epaint::TessellationOptions, @@ -304,6 +314,10 @@ impl Default for Options { system_theme: None, zoom_factor: 1.0, zoom_with_keyboard: true, + quit_shortcuts: vec![crate::KeyboardShortcut::new( + crate::Modifiers::COMMAND, + crate::Key::Q, + )], tessellation_options: Default::default(), repaint_on_widget_change: false, @@ -363,6 +377,7 @@ impl Options { system_theme: _, zoom_factor, zoom_with_keyboard, + quit_shortcuts: _, // not shown in ui tessellation_options, repaint_on_widget_change, max_passes, From 8b2315375b6cf13291ee658a498cb45fc379f600 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 20 Mar 2026 11:29:32 +0100 Subject: [PATCH 05/89] `TextEdit` `Atom` prefix/suffix (#7587) * part of https://github.com/emilk/egui/issues/7264 * part of https://github.com/emilk/egui/issues/7445 This PR changes the layout within the TextEdit to be done with AtomLayout. It also adds a Prefix and Postfix that allows adding permanent icons / icon buttons within the textedit. Breaking changes: - Removed `TextEdit::hint_text_font`. Hint text is an atom now, so the font can be set that way Screenshot 2025-10-06 at 12 25 21 --------- Co-authored-by: Emil Ernerfeldt Co-authored-by: lucasmerlin <8009393+lucasmerlin@users.noreply.github.com> --- crates/egui/src/atomics/atom_layout.rs | 2 +- crates/egui/src/atomics/atoms.rs | 5 + crates/egui/src/widgets/text_edit/builder.rs | 510 ++++++++++-------- .../tests/snapshots/imageviewer.png | 4 +- .../tests/snapshots/demos/Clipboard Test.png | 2 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 2 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../snapshots/demos/Window Resize Test.png | 4 +- tests/egui_tests/tests/regression_tests.rs | 66 ++- .../tests/snapshots/layout/text_edit.png | 4 +- .../tests/snapshots/layout/text_edit_clip.png | 4 +- .../layout/text_edit_placeholder_clip.png | 4 +- .../layout/text_edit_prefix_suffix.png | 3 + .../snapshots/text_edit_delay_0_empty.png | 3 + .../text_edit_delay_1_h_invisible.png | 3 + .../snapshots/text_edit_delay_2_h_visible.png | 3 + .../snapshots/text_edit_delay_3_i_visible.png | 3 + .../snapshots/text_edit_delay_4_i_visible.png | 3 + .../tests/snapshots/text_edit_rtl_0.png | 4 +- .../tests/snapshots/text_edit_rtl_1.png | 4 +- .../snapshots/text_edit_scroll_0_focus.png | 3 + .../tests/snapshots/text_edit_scroll_1_5.png | 3 + .../tests/snapshots/visuals/drag_value.png | 4 +- .../tests/snapshots/visuals/text_edit.png | 4 +- .../snapshots/visuals/text_edit_clip.png | 4 +- .../visuals/text_edit_placeholder_clip.png | 4 +- .../visuals/text_edit_prefix_suffix.png | 3 + tests/egui_tests/tests/test_widgets.rs | 12 + 31 files changed, 427 insertions(+), 259 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png create mode 100644 tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png create mode 100644 tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index b78f23536..da7f672a1 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -203,7 +203,7 @@ impl<'a> AtomLayout<'a> { // If the TextWrapMode is not Extend, ensure there is some item marked as `shrink`. // If none is found, mark the first text item as `shrink`. if wrap_mode != TextWrapMode::Extend { - let any_shrink = atoms.iter().any(|a| a.shrink); + let any_shrink = atoms.any_shrink(); if !any_shrink { let first_text = atoms .iter_mut() diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index fb04ee2dd..5051a7676 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -69,6 +69,11 @@ impl<'a> Atoms<'a> { string } + /// Do any of the atoms have shrink set to `true`? + pub fn any_shrink(&self) -> bool { + self.iter().any(|a| a.shrink) + } + pub fn iter_kinds(&self) -> impl Iterator> { self.0.iter().map(|atom| &atom.kind) } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index b9fdb1cbe..50154102a 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1,15 +1,13 @@ use std::sync::Arc; use emath::{Rect, TSTransform}; -use epaint::{ - StrokeKind, - text::{Galley, LayoutJob, cursor::CCursor}, -}; +use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor}; use crate::{ - Align, Align2, Color32, Context, CursorIcon, Event, EventFilter, FontSelection, Id, ImeEvent, - Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, Shape, TextBuffer, - TextStyle, TextWrapMode, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetWithState, epaint, + Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, + Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key, + KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer, + TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint, os::OperatingSystem, output::OutputEvent, response, text_selection, @@ -67,15 +65,16 @@ type LayouterFn<'t> = &'t mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { text: &'t mut dyn TextBuffer, - hint_text: WidgetText, - hint_text_font: Option, + prefix: Atoms<'static>, + suffix: Atoms<'static>, + hint_text: Atoms<'static>, id: Option, id_salt: Option, font_selection: FontSelection, text_color: Option, layouter: Option>, password: bool, - frame: bool, + frame: Option, margin: Margin, multiline: bool, interactive: bool, @@ -120,15 +119,16 @@ impl<'t> TextEdit<'t> { pub fn multiline(text: &'t mut dyn TextBuffer) -> Self { Self { text, + prefix: Default::default(), + suffix: Default::default(), hint_text: Default::default(), - hint_text_font: None, id: None, id_salt: None, font_selection: Default::default(), text_color: None, layouter: None, password: false, - frame: true, + frame: None, margin: Margin::symmetric(4, 2), multiline: true, interactive: true, @@ -202,8 +202,22 @@ impl<'t> TextEdit<'t> { /// # }); /// ``` #[inline] - pub fn hint_text(mut self, hint_text: impl Into) -> Self { - self.hint_text = hint_text.into(); + pub fn hint_text(mut self, hint_text: impl IntoAtoms<'static>) -> Self { + self.hint_text = hint_text.into_atoms(); + self + } + + /// Add a prefix to the text edit. This will always be shown before the editable text. + #[inline] + pub fn prefix(mut self, prefix: impl IntoAtoms<'static>) -> Self { + self.prefix = prefix.into_atoms(); + self + } + + /// Add a suffix to the text edit. This will always be shown after the editable text. + #[inline] + pub fn suffix(mut self, suffix: impl IntoAtoms<'static>) -> Self { + self.suffix = suffix.into_atoms(); self } @@ -215,13 +229,6 @@ impl<'t> TextEdit<'t> { self } - /// Set a specific style for the hint text. - #[inline] - pub fn hint_text_font(mut self, hint_text_font: impl Into) -> Self { - self.hint_text_font = Some(hint_text_font.into()); - self - } - /// If true, hide the letters from view and prevent copying from the field. #[inline] pub fn password(mut self, password: bool) -> Self { @@ -290,10 +297,10 @@ impl<'t> TextEdit<'t> { self } - /// Default is `true`. If set to `false` there will be no frame showing that this is editable text! + /// Customize the [`Frame`] around the text edit. #[inline] - pub fn frame(mut self, frame: bool) -> Self { - self.frame = frame; + pub fn frame(mut self, frame: Frame) -> Self { + self.frame = Some(frame); self } @@ -423,63 +430,18 @@ impl TextEdit<'_> { /// # }); /// ``` pub fn show(self, ui: &mut Ui) -> TextEditOutput { - let is_mutable = self.text.is_mutable(); - let frame = self.frame; - let where_to_put_background = ui.painter().add(Shape::Noop); - let background_color = self - .background_color - .unwrap_or_else(|| ui.visuals().text_edit_bg_color()); - let output = self.show_content(ui); - - if frame { - let visuals = ui.style().interact(&output.response); - let frame_rect = output.response.rect.expand(visuals.expansion); - let shape = if is_mutable { - if output.response.has_focus() { - epaint::RectShape::new( - frame_rect, - visuals.corner_radius, - background_color, - ui.visuals().selection.stroke, - StrokeKind::Inside, - ) - } else { - epaint::RectShape::new( - frame_rect, - visuals.corner_radius, - background_color, - visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". - StrokeKind::Inside, - ) - } - } else { - let visuals = &ui.style().visuals.widgets.inactive; - epaint::RectShape::stroke( - frame_rect, - visuals.corner_radius, - visuals.bg_stroke, // TODO(emilk): we want to show something here, or a text-edit field doesn't "pop". - StrokeKind::Inside, - ) - }; - - ui.painter().set(where_to_put_background, shape); - } - - output - } - - fn show_content(self, ui: &mut Ui) -> TextEditOutput { let TextEdit { text, - hint_text, - hint_text_font, + prefix, + suffix, + mut hint_text, id, id_salt, font_selection, text_color, layouter, password, - frame: _, + frame, margin, multiline, interactive, @@ -492,7 +454,7 @@ impl TextEdit<'_> { clip_text, char_limit, return_key, - background_color: _, + background_color, } = self; let text_color = text_color @@ -501,18 +463,16 @@ impl TextEdit<'_> { .unwrap_or_else(|| ui.visuals().widgets.inactive.text_color()); let prev_text = text.as_str().to_owned(); - let hint_text_str = hint_text.text().to_owned(); + let hint_text_str = hint_text.text().unwrap_or_default().to_string(); let font_id = font_selection.resolve(ui.style()); let row_height = ui.fonts_mut(|f| f.row_height(&font_id)); const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this. - let available_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH); - let desired_width = desired_width.unwrap_or_else(|| ui.spacing().text_edit_width); - let wrap_width = if ui.layout().horizontal_justify() { - available_width - } else { - desired_width.min(available_width) - }; + let available_width = ui.available_width().at_least(MIN_WIDTH); + let desired_width = desired_width + .unwrap_or_else(|| ui.spacing().text_edit_width) + .at_least(min_size.x); + let allocate_width = desired_width.at_most(available_width); let font_id_clone = font_id.clone(); let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { @@ -527,27 +487,18 @@ impl TextEdit<'_> { let layouter = layouter.unwrap_or(&mut default_layouter); - let mut galley = layouter(ui, text, wrap_width); - - let desired_inner_width = if clip_text { - wrap_width // visual clipping with scroll in singleline input. - } else { - galley.size().x.max(wrap_width) - }; - let desired_height = (desired_height_rows.at_least(1) as f32) * row_height; - let desired_inner_size = vec2(desired_inner_width, galley.size().y.max(desired_height)); - let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size); - let (auto_id, outer_rect) = ui.allocate_space(desired_outer_size); - let rect = outer_rect - margin; // inner rect (excluding frame/margin). + let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height; let id = id.unwrap_or_else(|| { if let Some(id_salt) = id_salt { ui.make_persistent_id(id_salt) } else { - auto_id // Since we are only storing the cursor a persistent Id is not super important + // Since we are only storing the cursor a persistent Id is not super important + let id = ui.next_auto_id(); + ui.skip_ahead_auto_ids(1); + id } }); - let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); // On touch screens (e.g. mobile in `eframe` web), should // dragging select text, or scroll the enclosing [`ScrollArea`] (if any)? @@ -565,12 +516,215 @@ impl TextEdit<'_> { } else { Sense::hover() }; - let mut response = ui.interact(outer_rect, id, sense); - response.intrinsic_size = Some(Vec2::new(desired_width, desired_outer_size.y)); - // Don't sent `OutputEvent::Clicked` when a user presses the space bar + let mut state = TextEditState::load(ui.ctx(), id).unwrap_or_default(); + let mut cursor_range = None; + let mut prev_cursor_range = None; + + let mut text_changed = false; + let text_mutable = text.is_mutable(); + + let mut handle_events = |ui: &Ui, galley: &mut Arc, layouter, wrap_width, text| { + if interactive && ui.memory(|mem| mem.has_focus(id)) { + ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); + + let default_cursor_range = if cursor_at_end { + CCursorRange::one(galley.end()) + } else { + CCursorRange::default() + }; + prev_cursor_range = state.cursor.range(galley); + + let (changed, new_cursor_range) = events( + ui, + &mut state, + text, + galley, + layouter, + id, + wrap_width, + multiline, + password, + default_cursor_range, + char_limit, + event_filter, + return_key, + ); + + if changed { + text_changed = true; + } + cursor_range = Some(new_cursor_range); + } + }; + + // We need to calculate the galley within the atom closure, so we can calculate it based on + // the available width (in case of wrapping multiline text edits). But we show it later, + // so we can clip it to the available size. Thus, extract it from the atom closure here. + let mut get_galley = None; + let inner_rect_id = Id::new("text_edit_rect"); + let atom_response = { + let any_shrink = hint_text.any_shrink(); + // Ideally we could just do `let mut atoms = prefix` here, but that won't compile + // but due to servo/rust-smallvec#146 (also see the comment below). + let mut atoms: Atoms<'_> = Atoms::new(()); + + // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have + // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues. + for atom in prefix { + atoms.push_right(atom); + } + + if text.as_str().is_empty() && !hint_text.is_empty() { + // Add hint_text (if any): + let mut shrunk = any_shrink; + let mut first = true; + + // Since we can't set a fallback color per atom, we have to override it here. + // Sucks, since it means users won't be able to override it. + hint_text.map_texts(|t| t.color(ui.style().visuals.weak_text_color())); + + for mut atom in hint_text { + if !shrunk && matches!(atom.kind, AtomKind::Text(_)) { + // elide the hint_text if needed + atom = atom.atom_shrink(true); + shrunk = true; + } + + if first { + // The first atom in the hint text gets inner_rect_id, so we can know + // where to paint the cursor + atom = atom.atom_id(inner_rect_id); + first = false; + } + + // The hint text should be shown left top instead of centered (important for + // multi line text edits) + atoms.push_right(atom.atom_align(Align2::LEFT_TOP)); + } + + // Calculate the empty galley, so it can be read later. The available width is + // technically wrong, but doesn't matter since the galley is empty + let available_width = allocate_width - margin.sum().x; + let galley = layouter(ui, text, available_width); + + // We can't update the galley immediately here, since it would show both hint text + // and the newly typed letter. So we pass a clone instead, and accept having a frame + // delay on the very first keystroke. + let mut galley_clone = Arc::clone(&galley); + handle_events(ui, &mut galley_clone, layouter, available_width, text); + + get_galley = Some(galley); + } else { + // We need a closure here, so we can calculate the galley based on the available + // width (after adding suffix and prefix), for correct wrapping in multi line text + // edits + atoms.push_right( + AtomKind::closure(|ui, args| { + let mut galley = layouter(ui, text, args.available_size.x); + + // Handling events here allows us to update the galley immediately on + // keystrokes, avoiding frame delays, and ensuring the scroll_to within + // ScrollAreas works correctly. + handle_events(ui, &mut galley, layouter, args.available_size.x, text); + + let intrinsic_size = galley.intrinsic_size(); + let mut size = galley.size(); + size.y = size.y.at_least(min_inner_height); + if clip_text { + size.x = size.x.at_most(args.available_size.x); + } + + // We paint the galley later, so we can do clipping and offsetting + get_galley = Some(galley); + IntoSizedResult { + intrinsic_size, + sized: SizedAtomKind::Empty { size: Some(size) }, + } + }) + .atom_id(inner_rect_id) + .atom_shrink(clip_text), + ); + } + + // Ensure the suffix is always right-aligned + if !suffix.is_empty() { + atoms.push_right(Atom::grow()); + } + + // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have + // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues. + for atom in suffix { + atoms.push_right(atom); + } + + let custom_frame = frame.is_some(); + let frame = frame.unwrap_or_else(|| Frame::new().inner_margin(margin)); + + let min_height = min_inner_height + frame.total_margin().sum().y; + + // This wrap mode only affects the hint_text + let wrap_mode = if multiline { + TextWrapMode::Wrap + } else { + TextWrapMode::Truncate + }; + + let mut allocated = AtomLayout::new(atoms) + .id(id) + .min_size(Vec2::new(allocate_width, min_height)) + .max_width(allocate_width) + .sense(sense) + .frame(frame) + .align2(Align2::LEFT_TOP) + .wrap_mode(wrap_mode) + .allocate(ui); + + allocated.frame = if !custom_frame { + let visuals = ui.style().interact(&allocated.response); + let background_color = + background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color()); + + let (corner_radius, background_color, stroke) = if text_mutable { + if allocated.response.has_focus() { + ( + visuals.corner_radius, + background_color, + ui.visuals().selection.stroke, + ) + } else { + (visuals.corner_radius, background_color, visuals.bg_stroke) + } + } else { + let visuals = &ui.style().visuals.widgets.inactive; + ( + visuals.corner_radius, + Color32::TRANSPARENT, + visuals.bg_stroke, + ) + }; + allocated + .frame + .fill(background_color) + .corner_radius(corner_radius) + .inner_margin(allocated.frame.inner_margin - Margin::same(stroke.width as i8)) + .stroke(stroke) + } else { + allocated.frame + }; + + allocated.paint(ui) + }; + + let inner_rect = atom_response.rect(inner_rect_id).unwrap_or(Rect::ZERO); + let mut response = atom_response.response; + + // Our atom closure was now called, so the galley should always be available here + let mut galley = get_galley.expect("Galley should be available here"); + + // Don't send `OutputEvent::Clicked` when a user presses the space bar response.flags -= response::Flags::FAKE_PRIMARY_CLICKED; - let text_clip_rect = rect; + let text_clip_rect = inner_rect; let painter = ui.painter_at(text_clip_rect.expand(1.0)); // expand to avoid clipping cursor if interactive && let Some(pointer_pos) = response.interact_pointer_pos() { @@ -581,19 +735,19 @@ impl TextEdit<'_> { // TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac) let cursor_at_pointer = - galley.cursor_from_pos(pointer_pos - rect.min + state.text_offset); + galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset); if ui.visuals().text_cursor.preview && response.hovered() && ui.input(|i| i.pointer.is_moving()) { // text cursor preview: - let cursor_rect = TSTransform::from_translation(rect.min.to_vec2()) + let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2()) * cursor_rect(&galley, &cursor_at_pointer, row_height); text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect); } - let is_being_dragged = ui.ctx().is_being_dragged(response.id); + let is_being_dragged = ui.is_being_dragged(response.id); let did_interact = state.cursor.pointer_interaction( ui, &response, @@ -613,44 +767,15 @@ impl TextEdit<'_> { ui.set_cursor_icon(CursorIcon::Text); } - let mut cursor_range = None; - let prev_cursor_range = state.cursor.range(&galley); - if interactive && ui.memory(|mem| mem.has_focus(id)) { - ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter)); - - let default_cursor_range = if cursor_at_end { - CCursorRange::one(galley.end()) - } else { - CCursorRange::default() - }; - - let (changed, new_cursor_range) = events( - ui, - &mut state, - text, - &mut galley, - layouter, - id, - wrap_width, - multiline, - password, - default_cursor_range, - char_limit, - event_filter, - return_key, - ); - - if changed { - response.mark_changed(); - } - cursor_range = Some(new_cursor_range); + if text_changed { + response.mark_changed(); } let mut galley_pos = align - .align_size_within_rect(galley.size(), rect) - .intersect(rect) // limit pos to the response rect area + .align_size_within_rect(galley.size(), inner_rect) + .intersect(inner_rect) // limit pos to the response rect area .min; - let align_offset = rect.left_top() - galley_pos; + let align_offset = inner_rect.left_top() - galley_pos; // Visual clipping for singleline text editor with text larger than width if clip_text && align_offset.x == 0.0 { @@ -660,18 +785,18 @@ impl TextEdit<'_> { }; let mut offset_x = state.text_offset.x; - let visible_range = offset_x..=offset_x + desired_inner_size.x; + let visible_range = offset_x..=offset_x + inner_rect.width(); if !visible_range.contains(&cursor_pos) { if cursor_pos < *visible_range.start() { offset_x = cursor_pos; } else { - offset_x = cursor_pos - desired_inner_size.x; + offset_x = cursor_pos - inner_rect.width(); } } offset_x = offset_x - .at_most(galley.size().x - desired_inner_size.x) + .at_most(galley.size().x - inner_rect.width()) .at_least(0.0); state.text_offset = vec2(offset_x, align_offset.y); @@ -688,32 +813,7 @@ impl TextEdit<'_> { false }; - if ui.is_rect_visible(rect) { - if text.as_str().is_empty() && !hint_text.is_empty() { - let hint_text_color = ui.visuals().weak_text_color(); - let hint_text_font_id = hint_text_font.unwrap_or_else(|| font_id.into()); - let galley = if multiline { - hint_text.into_galley( - ui, - Some(TextWrapMode::Wrap), - desired_inner_size.x, - hint_text_font_id, - ) - } else { - hint_text.into_galley( - ui, - Some(TextWrapMode::Extend), - f32::INFINITY, - hint_text_font_id, - ) - }; - let galley_pos = align - .align_size_within_rect(galley.size(), rect) - .intersect(rect) - .min; - painter.galley(galley_pos, galley, hint_text_color); - } - + if ui.is_rect_visible(inner_rect) { let has_focus = ui.memory(|mem| mem.has_focus(id)); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { @@ -721,44 +821,6 @@ impl TextEdit<'_> { paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } - // Allocate additional space if edits were made this frame that changed the size. This is important so that, - // if there's a ScrollArea, it can properly scroll to the cursor. - // Condition `!clip_text` is important to avoid breaking layout for `TextEdit::singleline` (PR #5640) - if !clip_text - && let extra_size = galley.size() - rect.size() - && (extra_size.x > 0.0 || extra_size.y > 0.0) - { - match ui.layout().main_dir() { - crate::Direction::LeftToRight | crate::Direction::TopDown => { - ui.allocate_rect( - Rect::from_min_size(outer_rect.max, extra_size), - Sense::hover(), - ); - } - crate::Direction::RightToLeft => { - ui.allocate_rect( - Rect::from_min_size( - emath::pos2(outer_rect.min.x - extra_size.x, outer_rect.max.y), - extra_size, - ), - Sense::hover(), - ); - } - crate::Direction::BottomUp => { - ui.allocate_rect( - Rect::from_min_size( - emath::pos2(outer_rect.min.x, outer_rect.max.y - extra_size.y), - extra_size, - ), - Sense::hover(), - ); - } - } - } else { - // Avoid an ID shift during this pass if the textedit grow - ui.skip_ahead_auto_ids(1); - } - painter.galley(galley_pos, Arc::clone(&galley), text_color); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { @@ -767,7 +829,7 @@ impl TextEdit<'_> { if response.changed() || selection_changed { // Scroll to keep primary cursor in view: - ui.scroll_to_rect(primary_cursor_rect + margin, None); + ui.scroll_to_rect(primary_cursor_rect, None); } if text.is_mutable() && interactive { @@ -796,9 +858,9 @@ impl TextEdit<'_> { .layer_transform_to_global(ui.layer_id()) .unwrap_or_default(); - ui.ctx().output_mut(|o| { + ui.output_mut(|o| { o.ime = Some(crate::output::IMEOutput { - rect: to_global * rect, + rect: to_global * inner_rect, cursor_rect: to_global * primary_cursor_rect, }); }); @@ -846,24 +908,22 @@ impl TextEdit<'_> { }); } - { - let role = if password { - accesskit::Role::PasswordInput - } else if multiline { - accesskit::Role::MultilineTextInput - } else { - accesskit::Role::TextInput - }; + let role = if password { + accesskit::Role::PasswordInput + } else if multiline { + accesskit::Role::MultilineTextInput + } else { + accesskit::Role::TextInput + }; - crate::text_selection::accesskit_text::update_accesskit_for_text_widget( - ui.ctx(), - id, - cursor_range, - role, - TSTransform::from_translation(galley_pos.to_vec2()), - &galley, - ); - } + crate::text_selection::accesskit_text::update_accesskit_for_text_widget( + ui.ctx(), + id, + cursor_range, + role, + TSTransform::from_translation(galley_pos.to_vec2()), + &galley, + ); TextEditOutput { response, @@ -911,7 +971,7 @@ fn events( event_filter: EventFilter, return_key: Option, ) -> (bool, CCursorRange) { - let os = ui.ctx().os(); + let os = ui.os(); let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 8d5f688bf..a97c3ea8b 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8b23a5286e5d2dbd8d3eddac6583d981152bd791f74edfa5c712a610f795256 -size 96759 +oid sha256:73592be3cb5e2bbc1de870050b913b307e31c05df339b2fd78e9ce38c05f4cd2 +size 96758 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index 2549417be..47ad5bc7a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a53262cf5d8507d8eeae8c968767cef462b727879245085673982b850a6da670 +oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4 size 26977 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 46a5ed1f7..8c6077a1c 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7601584308bf60820506f842569a3c1daf3c15fa6e715f6b9386b5112dcc92f -size 76076 +oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12 +size 76531 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index 375b0f922..f73093e3e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fc2793506ec483c7f124b6206fb18ffb73bec29746f2d9bb5145042ddc45016 -size 114410 +oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6 +size 114409 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 843bb93b4..6b1a9946b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718203d31d8b027a7718a66c4712cf1e17b9aea2e870d755bd2c0c346529d4f4 +oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3 size 21814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 81204a347..92992cd83 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6af5adc42544171c6d85e190c853aca06784c131a373a693a6f7069d4cf1a404 -size 13698 +oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92 +size 13700 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index 78446cca9..cdc1a43dd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e8e03c2a42e195e6489659053aecb78755d3c218558cb2e9339fa7b6db59405 -size 35875 +oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4 +size 35874 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index a6d103d6d..96ca9949e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad22ea6b6e69fd71416fdae76cbd142d279f8f562e74b77e63b3989be187c57c -size 484631 +oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1 +size 483497 diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 421b69d35..d61b76af3 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,6 +1,9 @@ use egui::accesskit::Role; use egui::epaint::Shape; -use egui::{Align, Color32, Image, Label, Layout, RichText, Sense, TextWrapMode, include_image}; +use egui::style::ScrollAnimation; +use egui::{ + Align, Color32, Image, Label, Layout, RichText, ScrollArea, Sense, TextWrapMode, include_image, +}; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -63,6 +66,67 @@ fn text_edit_rtl() { } } +#[test] +fn text_edit_delay() { + let mut text = String::new(); + let mut harness = Harness::builder().with_size((200.0, 50.0)).build_ui(|ui| { + ui.style_mut().scroll_animation = ScrollAnimation::none(); + ui.add(egui::TextEdit::singleline(&mut text).hint_text("Write something")); + }); + + harness.get_by_role(Role::TextInput).focus(); + harness.step(); + harness.snapshot("text_edit_delay_0_empty"); + + harness.get_by_role(Role::TextInput).type_text("h"); + + // When the text is empty, and we show the hint text, there is a frame delay. + harness.step(); + harness.snapshot("text_edit_delay_1_h_invisible"); + + // Now it should be visible + harness.step(); + harness.snapshot("text_edit_delay_2_h_visible"); + + harness.get_by_role(Role::TextInput).type_text("i"); + + // The "i" should immediately be visible without a delay + harness.step(); + harness.snapshot("text_edit_delay_3_i_visible"); + + // The next frame should exactly match the previous one + harness.step(); + harness.snapshot("text_edit_delay_4_i_visible"); +} + +#[test] +fn text_edit_scroll() { + let mut text = "1\n2\n3\n4\n".to_owned(); + let mut harness = Harness::builder().build_ui(|ui| { + ScrollArea::vertical().max_height(40.0).show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut text) + .desired_rows(2) + .hint_text("Write something"), + ); + }); + }); + + harness.fit_contents(); + + harness.get_by_role(Role::MultilineTextInput).focus(); + harness.step(); + harness.snapshot("text_edit_scroll_0_focus"); + + harness + .get_by_role(Role::MultilineTextInput) + .type_text("5\n"); + + // When the text is empty, and we show the hint text, there is a frame delay. + harness.run(); + harness.snapshot("text_edit_scroll_1_5"); +} + #[test] fn combobox_should_have_value() { let harness = Harness::new_ui(|ui| { diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png index cfbaefd41..d5d853f5e 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30de3e9f9645206e33fa1edd841b48228e154d0ceae962c64c060a66eecd73ba -size 220452 +oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3 +size 220588 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png index 8ea5d0b7a..d87f37561 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9f36b8623d2d9c35e337e973f547166f62a5daae757c462b1482babdd42c941 -size 383051 +oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7 +size 372041 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png index 0a84f42db..578e4d9db 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2734644b2fbb6f42ddab6c65a1f5d073f1f002900bbd814c1edb6184e0a9c0 -size 362521 +oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc +size 330200 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png new file mode 100644 index 000000000..bdcab38f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179 +size 361876 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png new file mode 100644 index 000000000..58b0b13f2 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e +size 2262 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png new file mode 100644 index 000000000..c1920bcf1 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702 +size 2272 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png new file mode 100644 index 000000000..bc1ffcd08 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_2_h_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ab120260212d0f41d2956ea2d679cfed648cb188badcca7fa82e0dec9c87ec1a +size 714 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png new file mode 100644 index 000000000..324946e10 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_3_i_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde +size 775 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png new file mode 100644 index 000000000..324946e10 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_4_i_visible.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2ea9d6bef26f1dbd7dc272b4331012ad6fd1a43fded150d33ba4762d404dbde +size 775 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png index 3b87786e8..796a1e3b5 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce357224c2e1cf32f96b3d075dc070c4d14e9aaca1b8165d0ba98603dff19c1b -size 2324 +oid sha256:ed3665dfb232b8f0b1483802bfafb4605e8361d7eb977de5a58862e52ab724fa +size 2296 diff --git a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png index 5d9aada78..cd6d5a621 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png +++ b/tests/egui_tests/tests/snapshots/text_edit_rtl_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91c715ac66be329cac42ff7c7726348b0ac79d897c414bbde26bb0115781577 -size 2289 +oid sha256:5cfdd6255aba92f0253571bde4f22afc8e0fb5fe3cb882946459d623c0a5d89c +size 2982 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png new file mode 100644 index 000000000..1d0a5ed46 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a +size 1510 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png new file mode 100644 index 000000000..bdb8d1b1b --- /dev/null +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea +size 1501 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 8be9c5e9f..7cd2d2ce8 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71daf8a33d277075012bf1130d7820574fe0286080154810d8d398c005a65127 -size 9037 +oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a +size 9079 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 4de5e3bd0..4719c8ce9 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d259d113aa23089992b04f19e71c743272dda3fc9baa9612565158f15ace57e -size 8159 +oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0 +size 8127 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png index 192fa8f74..8a5999742 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1e56f1b6970c14830d8869f4d8cacfed821ec2b3aab7033b1bfd213a864da79 -size 10959 +oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd +size 10143 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png index decd09bf9..19c231b45 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:493649ea09351835147aa6cc800858939dd44beafe37adc488b63b291d58e3b3 -size 10302 +oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae +size 8838 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png new file mode 100644 index 000000000..d27f6f8c4 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc +size 10028 diff --git a/tests/egui_tests/tests/test_widgets.rs b/tests/egui_tests/tests/test_widgets.rs index 5ef98c8a8..5283b21d4 100644 --- a/tests/egui_tests/tests/test_widgets.rs +++ b/tests/egui_tests/tests/test_widgets.rs @@ -122,6 +122,18 @@ fn widget_tests() { }, &mut results, ); + test_widget( + "text_edit_prefix_suffix", + |ui| { + ui.spacing_mut().text_edit_width = 45.0; + TextEdit::singleline(&mut "Hello World".to_owned()) + .prefix("šŸ”Ž") + .suffix("!") + .clip_text(true) + .ui(ui) + }, + &mut results, + ); test_widget( "slider", From 4714aa7d311fea148e165d293d0800eda518b50e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 21 Mar 2026 21:52:52 +0100 Subject: [PATCH 06/89] Fix instable IDs following animated panels (#7994) * Found thanks to https://github.com/emilk/egui/pull/7984 If you put an animated `Panel` inside a `Ui`, then the automatic ids for following widgets would differ when the panel was collapsed or open. --- crates/egui/src/containers/panel.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index f2a4c3b67..4a83ce8d1 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -561,7 +561,11 @@ impl Panel { let how_expanded = animate_expansion(ui.ctx(), self.id.with("animation"), is_expanded); // Get either the fake or the real panel to animate - let animated_panel = self.get_animated_panel(ui.ctx(), is_expanded)?; + let Some(animated_panel) = self.get_animated_panel(ui.ctx(), is_expanded) else { + // Make sure the ids of the next widgets are the same whether we show the panel or not: + ui.skip_ahead_auto_ids(1); + return None; + }; if how_expanded < 1.0 { // Show a fake panel in this in-between animation state: From 1b2a06534232a86f8e1c26b714603ffec6ff97e6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 21 Mar 2026 21:54:26 +0100 Subject: [PATCH 07/89] Ignore rustsec --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 01377b90b..486af7745 100644 --- a/deny.toml +++ b/deny.toml @@ -34,6 +34,7 @@ ignore = [ "RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect "RUSTSEC-2024-0436", # unmaintained paste pulled via metal/wgpu, see https://github.com/gfx-rs/metal-rs/issues/349 "RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md + "RUSTSEC-2026-0049", # https://rustsec.org/advisories/RUSTSEC-2026-0049 ] [bans] From 49fad9a7b2b8f99f0081b105e7fd4f5851238ac5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 21 Mar 2026 22:47:15 +0100 Subject: [PATCH 08/89] Roll out new egui icon and logo (#7995) For the first time _ever_, egui has a logo! egui-logo Made by [Studio Gruhl](https://www.studiogruhl.com/) and paid for by [Rerun.io](https://rerun.io/). --- README.md | 10 ++-- crates/eframe/data/icon.png | Bin 17166 -> 12052 bytes crates/egui_demo_app/src/wrap_app.rs | 2 + .../egui_demo_app/tests/snapshots/clock.png | 4 +- .../tests/snapshots/custom3d.png | 4 +- .../tests/snapshots/easymarkeditor.png | 4 +- .../tests/snapshots/imageviewer.png | 4 +- crates/egui_demo_lib/data/egui-logo.svg | 21 ++++++++ crates/egui_demo_lib/data/icon.png | Bin 2642 -> 1720 bytes crates/egui_demo_lib/data/icon.svg | 5 ++ crates/egui_demo_lib/data/peace.svg | 11 ---- crates/egui_demo_lib/src/demo/about.rs | 36 ++++++++----- .../src/demo/demo_app_windows.rs | 49 ++++++++++-------- .../egui_demo_lib/src/demo/tests/svg_test.rs | 2 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 7 +-- .../tests/snapshots/demos/Clipboard Test.png | 4 +- .../tests/snapshots/demos/SVG Test.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../snapshots/widget_gallery_dark_x1.png | 4 +- .../snapshots/widget_gallery_dark_x2.png | 4 +- .../snapshots/widget_gallery_light_x1.png | 4 +- .../snapshots/widget_gallery_light_x2.png | 4 +- .../tests/snapshots/menu/closed_hovered.png | 4 +- .../tests/snapshots/menu/opened.png | 4 +- .../tests/snapshots/menu/submenu.png | 4 +- .../tests/snapshots/menu/subsubmenu.png | 4 +- .../snapshots/should_wait_for_images.png | 4 +- .../tests/snapshots/layout/atoms_image.png | 4 +- .../tests/snapshots/layout/button_image.png | 4 +- .../layout/button_image_shortcut.png | 4 +- .../tests/snapshots/visuals/button_image.png | 4 +- .../visuals/button_image_shortcut.png | 4 +- .../button_image_shortcut_selected.png | 4 +- web_demo/favicon.ico | Bin 15406 -> 2932 bytes 34 files changed, 129 insertions(+), 102 deletions(-) create mode 100644 crates/egui_demo_lib/data/egui-logo.svg create mode 100644 crates/egui_demo_lib/data/icon.svg delete mode 100644 crates/egui_demo_lib/data/peace.svg mode change 100755 => 100644 web_demo/favicon.ico diff --git a/README.md b/README.md index f4a094465..7eebd0423 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,14 @@ [![Discord](https://img.shields.io/discord/900275882684477440?label=egui%20discord)](https://discord.gg/JFcEma9bJq) +
- + -egui development is sponsored by [Rerun](https://www.rerun.io/), a startup building
-an SDK for visualizing streams of multimodal data. -
- ---- +
šŸ‘‰ [Click to run the web demo](https://www.egui.rs/#demo) šŸ‘ˆ + egui (pronounced "e-gooey") is a simple, fast, and highly portable immediate mode GUI library for Rust. egui runs on the web, natively, and [in your favorite game engine](#integrations). diff --git a/crates/eframe/data/icon.png b/crates/eframe/data/icon.png index cf1e6c3ebdb52209943a9affac3b463b2c85e717..4ce7cc588ecd1b3a0bf81a79dc04677c21e9bf98 100644 GIT binary patch literal 12052 zcmd6Ni9eKI`1YAGm@HA&O4%Y?mdKKAC?#8ph=h=>ERj@}8I?9fWKX6AMFzuphW=A;LwzDCGIFA}0I)M-# zKH?FP1^&!k`LP0jaQ-Kb=;JzS<;d`b;HGD)hfqN@``TG%_%7mXc)}E+Ymx|2ZX$%W zr;H#Jpoq|zBSHs~5EAfx^x?P`ykK!MK5_^V;M3PlpN(F@-%Y-THvS07i(~(BS2A=j z!$a0fM@0P89#a_`N}{U-n-yBl7_w zQD&OCa!cG%eREJVKJU$^n^a^OfIa{F{zVm;@ zrKF@BO+KCYx!-qGDNwlWcW=|`tn-ff^|kxEv{MFeJBaQ%vIURk=L>76I*X$f``pa3 zS~9+Nh1f-q4^A~kd;J)mjfjeB@RJ}3XherXk??zk3aU&;9 zOum^u5s{G@`!IA&Pz0Ip5lGeH6s8g&M>twcr`aD?w0P+7;h$u$qK+{I zL9L*LSNXbOO%Kn~DpgnWmRpu^v?3Anz)y~c4joEJ*QbnLr*z;c4&PTi)=-~zYJ(Lc z0DJSfKW28gv1mI9H7(lcf(38b@n%^Tm7hNs9r_|HO5@d|RW4jzoXX0~{66?f7zN9U zi9MzNot~bS0}BP@?UJT$AI`nNZLWb%1+P!_w69_PuVUs($jQkW9~#rA{o;)!PM-D1p68hkVD)Q|iE_7=%&Q{f10#8CX3Vc0mWnm$%r>A$q z!eW+0BGK}inwnN+Wn}}UL!0Z_D2F?n2O7Z)nxOu{g9Za_=kfOaZ2x6$I{ z$@n{WEUm5GdajhIt$I?%d~hXYaTXR9%Fr|QagKHV&x)Pg{QN_&^7492iRMAfwA>+w zMbsdSew^9#+BUcOomVGE;3@$}Q;+KXJ+}B`KjW6#A`VHl3(4`n%+9XkeX!#orC$36 zqS9>02j$k8C@V6uvVX0Z_PrewKAhRSA6W$k?wVRxa<~-R~W`i26VzX4FhU6bZgq z-e_oIU|_J`R`Cj-Bu03l!*Zqj{_k;N)c0G4;^(&J%7*4O?-!8~MK%Xn<(&Ne`*(8# zH{L{{n?ldrC9)1xWI%QfslxztH*X=J$gMr=@P)eWALEgeXrqwyPMv^XZkp zc<~}HQ*O3HMQCBj>g7Ck>M{>P4u5~u8NQ*fWs7ATwDzO{Pu-Y97G9Q8NYxT#|J{ z4un%ext?3)ys~|J?^r4U!lSwHT)p~h{l#l@Bcgx4MrUzL5`zec#&wHJSUy^3MJk*k z>f-8Zbm4+rxH+#gg_2HSX0FZ2|7axYDEQxh|HUTPvc@*AZ9)`l``~{DM@>wgb1Odl z@xyYoHAf&EVr2S|iAm(QWaZoXG+#U(zgtZ$QJ;d)6{kCJ*1dQDJ*x+v38(JgJFr%{gM zT&}N!m;E+v-kiw|4+5aowo6kzX{F5BO)A@%2yZ^M3xB&=Kyr5G;BF#1lND1Tc9PY6 zc73`K=%x}#Mj^L2zQoT20DjfWD|bX1MkLzGQQ3#^L#?(Lbi zf+Bs}s_7I5C4P6gNF08b(|DsMWW)y$utrW!PXQ<*s+>Gpbqfj#_BnkaF3;2}bZG2y z+L#kX`gul%(!%hQnHj&MM~^;;jO4 cY#n*(_21Ol(7{TD|sSmpDX@^Pa2j(8HcBmX@cobLZTE``CGk(t?MY{fiN z)O;SpKD8inL6S2J4gENo{--Wpw!>!E_I)ly%zq^9+S=NEm&Z@B^*>>(RxsjXW4Cte zHkJo178aGgzRMH3RcA!#Fq=^8_mDu_=31hG8Avj}VzR#*x#LN!oFuFT7f*gzx2_y7kBY@eCa{t$C?05eZ`H6T`Rtb+y zHUd6hae-A(05g>)t{t@CGR~!_z7h4Eetv$?6UY4mlJI`hYV-uA3Y8cCi@oq!u9`k4 zt*)LVMic*^&V4sIlUINzF3?Ro-f5s>VwtCy?cNexflMe*0P83@Zu1JWA>NPhvOWiES zLg2;q0$pcABcrFp>Fv&8J!d(%>m0`$)swzc`2Q#DM8w2AJUmqRZy1`HWfGgdBq=!$ z+}E6>a+@n4^)$1EY<4Fnr`_V>H}nO+U^@=@DKFlhorB|HS(z3a?kyfQ^aez(Z;NPf zjr$m~dV=VFy?`XehlDn>2DKP;O*EFUdXZ$`zklDgHwGbP`fue0H?3%`8KMEjwd6GQ zHfE$?D{RbjH8G;Rl@u1PU2F>Wi(y-V?Uc1PKYXI900j8Lfm!$g6g9EQ}8SPNL5Q8lqhr6V* zvc<1rpJSq-p9DoNK{oL(>bNj7MP{Vda=AVq(p2&s#5#7c~-ZG#w(5CwLB2V%VZxk-)b!Z8%rN{_MZBLy#Pm1g&h|F;Lp|=I`FUyCy>6qag;N z*OV@_D^4!lcD0e?PgcM9biAYJ$cIa#`g(l@055kVB9!N+w#uTWaG+8)A1+CUQ;b*U z|L*AxSxozusuq3h%8esWpFZ6!D;oiz+q6gd46&wNxQlQ5cA4bl&s_oUZkrH9s;I`r{Z<2RuX&hMlVfO)vy4VGG6#&Y9|2(9Y5FSBzP zWXP=m0S?Sz5E}K_CvygRf~GR0+^qns+F~KY`9L=R2$&-7TpjrU@=wiw&yEbfZL^Qt zB7WGg^zww%<~--0faxkMFYE-4uOFbC{e0_UvF?(AzW#O06HEX8RQ3DT^nboR*!1Ke z#`=!j7kV&N!LXONFOwP^96VuVb@-Esl(_i8pZN|7P$DZ~EjuW@B_KYa-gtf_dRJ^xk`xfQ<{WDU;Ewy( z!+Jd58u|GZ?%uOUZCP7)eJ(Ec*$KL*9Fzx%tCOY2T)!oqP3k;w#K53-ZFyE=|Ni)) z=%2Hd>4`9&n2zfpFaI}yfr6rW;Mfq`1|+oX_}faar)IdLeSG2 zN?)q0>$=|ibIu04H9p?USf3)tpE|j+(CH(gpm6MQ@XS$<-jC&4tK&t50e`KaxE>7e zARRhHFlZuqdF4j#(ze~a^MK2p;T_!EB0vJh)$u4;LRQxJ>9c2?|Lk&dawLG3*RGq} z*ho*6FFxiF*0DQqX+)&RwITNDQ!#t0-^AW*Rj*&>L{1M7yTxw$_nlp?AlC`cDKGF&KHOI=E$^2%vg0bFc-WWLAoYCcb4??ib z*39hwlWKZ)cDBj9vE}jOqEO{Emj?wVE4$sdw6siGpv1<+(2k!JmynRSb?swuZ)mCX zB!5je3NUOvh8NNaR(B?|u<3MkbU-_izis(@5bY9d{|qQ`>b!*mnQr#}e}2p{1hjXb z=MSlE8h+3FzyB_afB5hVJMD{@xcIrAva2d&z|pF1dZgE%>Zs@E!)_9nB{)z`;i8?BB^F*h<| z)7(+$QoA)HFKG`NNiwoPunNDu$@l6S8x!5qL#*=b%d^K-bpR5^-3_C6X}f&6vt~n7txvW?s^$_(1 z$=DIDw4Dh&3aVkg5zl+S!FyV>-;_^4G1v93;gLq%PL9!*tmxSKVFGd=!@!*=d1<^| z3Cn_ho%=71n7{vJROWRvjRb}L5k**W68@7Jhd8h31OdyY48ODiH z*K0c_F5471h5L2Bb^L^vCny)xCn{JF@F>N9x=-&O!rsB|$ap6$BV(lM-BpuimtubO z=-o#L0`37eG(Eb09iQQ|m8q>U!7A4_qw6xKpdzWTun=ZkL}rBMHHx97+YcPvAp;ir z^y=8hn=go8cl(wk8m&uL*K9v@d9v%J6+lSL(3I>BHSd8tIE`y_FUGq|t;>}+=bXf9 zbJ!B+_N3l4`l?cm0*u0?(Uu2`JweAj&!5-uBtbIGAGq9cL3MfLS@iRu#mRA(s-5b- zBuE;EfQHlfvdq$A1A9Xf^-0hdb$q{QahG4jh0b@T-sSUFm)}&}=g;s2dKHVJjb*? zZMOdMSUYH7eX%zS!h&J$Rl5e6GBz!(LG~fjOX=@VFQ}#lPk&-9gi1(9&M?gR-$QAO zhiU6>9NeJ=P;_cs{7nc~EPz4|OVB0-xuBuhUmKNxSc<+h*_E8;V-AEoF8||-QAeAU zESip}t25fD-WA_hR_a@L`9L%cg67u!ZjVj_Blj(p;xEsHmxhKU%u;h&k@#Lo4zqHFfh$-jHY$thlduYdVO9L(13^; z1qW+YoZ0^V%D<v(%UalEJe^sUW1Wk6b~^YL$dtj#$eoOLW+9)!WQN+6f} z#iN*QbFGq9Jhp0@ibvj{k9}O7jLSV;TnFm93#9Gl(m~W7_k)n)gv3&%b~FW}sHChcz4k+Fa;V#ddYS((={`VWJygIMP&<4e=YfqgaF$RHBJa8Ru~t9WMxZpE-Ln;` zB!cjRrI)jngp2Az*c%Ply+Yd8Ebj|xi0v_Z6a#G3(8ebDVSM~i5EA8!#igVSC(CBd zlCiNFYqPgrR@6C@)lPVACA|ku9ZnIexX-+OkmubitDMLkYG$A$97&w1{P*XpgrwwQ z2p>?@jjvoO`SSIvF~(FRB{6+qZmiWfxyZrC!5=DZtCd__dAP;NuXIZvjQPi>lG8pE z&{n4^(w{{iRTtVUEiaz{%!7YN?+q>;DPUrLjP*t3C|tVpcw=9fO~#GqHYSg?l2b4A zWww#^^uh*)hU&n167uq9kFWgR)KPL#+^3wj$Lz6|G?Xg5P=b?Xq@-dX<45RIkAi|e zlTThZhO90PwMi_*UJb|?QXDGE&ea^%bSP*!i`*I$Kp9{Q<4@Bi+ZPrUWmsgHVX}cn z$cl|CoC;@gGkrU$5Cv>Y?W`n7r-hJD*kSJPOcDtbs^0E zEkfsgJX5+jV}|lsfmZmfEnU2C=&MPTvwdR-H1OQ|$~+LxS^%#63i_{yo!^G6y_Zwkw)OpnRZnt+LaNQJL9J)s9#`ot!0Z|8JPTC>(A9EF{ z2&~?=Dvn6UA}<${Mj9dgNpWdSQ_AQoe|edr4qTmiE(ewL2HiefdRk--6n7&iHC<)~ zYBS4e?52Ob$pm!9|3F%%wyzRDF&B7#tuSNhhz^N3kIld1jRy}@U?XuIiL?kHGXH*8 zl|6lO7nsNC$3c@CIv2NQ3a^&F#KaP9P{Pj1ai??u(5{UhBmbuP__^*{E#2ZwX#&;Z z$=mtAZN}HG9SB!^Sy;HQ<^KB19>@l4Q1si@fB&Aa|NKvLV%%g#K_vL>>@LFl@zLEGD_<0;hc(vS0)9Zib*FNB}sR;*!XtzZqFs5#8QUZhk9p z{_0YBa}#eR;qp{ZLpQ9pVm5Hdqn9tMhEp)Wpn2}dm4oWq$s1kfp|==DnlJn<4z>(z<3ZBjS& zOFD&d8$S5>8nS|n$MI-{n6bq;^D)?l{x6zrPGp8um0G&K1yN( zdIW`>$=<3MtVi59-Xh3;^Cu)aNdT)1ZkFoy^uK~kKQ7+OSXPTJAT3owVI+NB4@%RX zq5J_hl=8J3Rs{56#dA*Pk?YAjaI~8xr$yde`uE#ldF5}u^vzG8oZvwm1UoL0(H|%Z z?FjBC{@w2SzwrV?>3Tm;>6AGfLLuGaeQI~^U0d)#5PuQ~I^6kp5JP;cZ1U*$1 z`Lg^?gWV6pP`+>R(RI+k>hclIA0#!5RDv=_aT3~m8%s5OCvsCHohjy90zr-?X+z!} z^d@h|^cRTHo)_8KPl*RO&UH;h5Ba1Z+P=g;jex#YnV1O$#v69!p`v!xltZpWfpFU8oIJtMRx$U=9IYi7X(|k}xY$RPGiZ z3c-T$hl2gwud{O8ijUu(Z>zI*2r&>1rYMn8Dm-8&g6x5-&z4j7q3Pqak08%E3Ptem z1xlP8GK3P)`zdgkZ6g$N?>pgrKoWjhp&B3d zQ}d^3+JSieHyB~#c9i3G=Zsu`yQ><7Ilxqx1U7~tDVJ=fRlx7pZez}Kr+)yIvo9!e zJq$GwM_%Kzv8Z$=&8*>^7;K!Jp!zkduma7{aG&%vyD2Xo6U3cOz|N3&<3f6d6H64>g zrC_;epvHmS878T!QGDAKfa4t0Y6LOya)JQnSDLXMD^-`7nRz3+`=9i#4XHI3I3EE? zdzbdL*F|Lq(^*@TqWcsiJeMaDN!tITY*yWV|b}r8jOK57Q z>R%bjNVJd5dVI~OxY4KXq4W$;-NI*DzVl-zffulaD;ipXE)Q=d6L65i zi+>Jdsp~nE_aD@W%mjiVQ~(2*ya1Zp<=ERZv$GjgDn(zA;u(j+#!oC6!x1t}%oM8S zLrPi~O5hYH5r~8_l;Lp5XBDEOnrez?(zto$*_$_(%`Gh(Yd~erV`?hKjtssEljR^s zC{|4m38*QWLwydZX)nO(_jKK3qD?X-MD9??WjM_BO&kKa?X0kS^O(P&ZCj9btQp4ZVp24Ak`(0`D2NHrSoZ%1=jT7>&&2@P!FpwGquM9|91f<({069aW zY^U*K1=zt#gYYldj54-GuAI)~dyWtW(mSf5IJShrB$Soo^f6{kGKS8KKw#VGnvw(G z*kCt_%38gm!pKYPZ8$-Y-OVLx_HAq^VQ(1X@H;rV+Xy@XdAudNbC}{xf%~sF9=!Uk z8z(wJk<68M1XnEtu+uR($*+g9Uz)&iqd*rU6-_lm4XNAMZ=S`nJxTVrK4~6{!UEDr z&tfmR5u2h4+5IJzN&*#0ns6y({HUqvIhZ2lCykf#Yv_BLEJ#Ng+)WuEW=zebGuCcn zhaBz`a3V7UMUL^si%M0LJgB@93v`vSq8!K2yfhLMDX63m)hC=CK7sR1jD!Pp@O~x$ zW@+RxnRfz`4HSIc6_-w?C&LkC$ZUfe+ zI29(C5NU3pKAfw%#}^B<8tyk_ionWV^95GDZNKM35X0(0?b;@AQxaqc8O^}ShZp}; z*LoFqOU6l>{CcuS;cv^5DFVD#AWUBSx8MFLoYC68_siHV70#SwnHa0mtKB}=&Kup&fp zotx@)sEsok#M8jr^zZe2aOexi5rfwv3T2U5R$ONV9%*IHtTO6Y5M1^FX~+(KH+?U= z9-dHucF=Dm?nNYaY}^IjK?Qu|ojZN4?8w(x99o3XB8_l;nQ1)y{MDT>(`c2#wR}sW z`DNxlaBa?&We~F-BqnBEP*AEGM$@VY(N1PNtmk{85|68rT3U~a!CLS4XO|sncYKp7 z$NSRPmRrTzba+JIowW+04HhxV%F0?_Awl4beNH5V+!&HX^!=9Rcx5=d&ySD>8R)aB zh@ocLYaefo@{GNpqP2mES*s&jd=WXT&W(}HN?GCd?XL{qzxnnvdEzu1D@s}!@8!Vt z9e}$=c5r#YeRRB{ipLmF3A4h%&7?WkZl+hTaw+Po4lO1M{c&i%YvlB=i=v{USZm+m z@$^QV;uT_vH=l@#%0p`{*;-CHs_^Mg^}xWmlM%+Hi3qp8wJWjg`xD9!!_;Twk>dxm-|$c$Gk4ZW7H zBs+WlUME30@578JN8hV2w7m|LdaF&REB zhp6_mS=$K|Nf1EGOu@AMsbq0C#w7i?5kE51=&>IG>l@O-!oo_0Hh*QpZhW0S zUGK9M36?Hb{e-p~9X~a3IW!V$05rP_-9*vW&UOt`Y7h$#JM_2kWUDe7QQ>3^&)1 zVE%IPrOLJ+y%E__755FEc-_75+BAWs)74`jx@4){G`jeFo3~{*7+vBP)o0D0$zn;QX*D{cGTn}6#a z44hXwE{$Hy>5)X$2s@0G853Sh7p*KUg@{~S6|z!N?$)NJ4NRXteS*t!pM50JQpK`n zDiQUlTzin2^(7nJmq=8zLSG_UumANF5D-}W`XL@g4E}ORjF10!nhoi=uc@gpinn{L z{jS_r)>1X)F*fy3y(UKxeYtUX%G)t!$F9VMiB87xL(d_x#DQ0IwEvQG?kf8S;z`E+ zq?F|pL{;vWR|&K}NYTVm&k~m`x|!fKcDmMvOy_LqX+?czH+)9VOUSRBP(mfmKeX3B z6fnw1|9*hGjbYR~ce=fYi?^Y&^J`K2LyqAoSq|(tQU#7w=00Z2jJ9ks4h~*pWNI+?u0tzmkpVyYp$EyUtI(U0C`Q<7X7_*%b5XFd+4E>Ud`aD{E zCWG>z_igCQXGXxHo2}MgpUDD{HY4+bsQvr#Vqy@$LZV2jb1v0m%}gz|LK8tsj9HJ8#!HWov6I%p%7hv9>nbD5Qa;+UCd}YdP@i#{_&9 z3!}4n5q|n2DmtY?(`}t!?R;=|Z@}0Oj|<&xaNiAZd7sZ34v$06RMsR@=rRH+WyLsB zL=k;R-#`LKBI9V?l9Q&~zQd8dzb8YNCVa@6dHv+J@URck*Kfgw(wIThIrQ~awQE|C zPZ*!8h9*sC7ppu;77TtI$xUCyliou>$EHYU5hOV8jR z4+^y^O)oF#P;*})FH%Q~=R#~*kk7}gdx+YL`n;V37pJs57Wi`)j6sjB?phDX8 z4>Q^PdrfZjar*p_+v@z^#KF3Mk_rk_0bHpc5^%JZTT_$0TQHBMcy>*yT#nY6{#un{ z8(dxi3Vp)QW`*n=@>EWG>mUE$ZXJtj7wXByeuH&wW$ryas*Hr~A4~qDm4}$z1vL9# d_|LtL!K$yk)S4Mj>Bh`EYG8IKPv7zS{{d>fHv9kp literal 17166 zcmdtK^+QzM7cYE8L>>9zPfS56doxl4!iod$E#u6ja1y072p?7k(`=T(*fLS+LqgtXnn_8a z5=qCFa41PZqL`)MFYEiCK+fPgI9$AIpFCUEJTuGZfGgO_um2oI(#3aQd<|Zg><}n`-P`kM7EDXsbDqUpCfu+2WmHT$xql zVEV0SYuS*)#mOGyhQsmq!f{ZH=OJzEuh^)_$n`^X=q12XfFC_|S)C`rEW#48A^S2G zvA5B4&v$5h$sC)2WV>d(it{+B>}cX-;&uq$pxYu)nGQji859Vnc8(Bc32E0)Y`wdb z8FM`_EyE8;uirNv!DvnB5m~{+SA>amo3CK-Ln=?Uit9h@!<>?KskB{xyvIvV#q8Wn zX=aNwg(C9EwlUTh(@T6x?N+id0RL7j`G}*Rp|MzR&@Z=+a!T7{9?pLb(TMHYz-259 zReyI5?c`gBm$XooV|(OP$P0ITzy${0f9QE-^_dDk+|B#s>kuVt?qK2revHpzo`fOV zL+(M*uC(=hSp~r(B#l!-ZdNH3rovtA*nm@HPC+`m(MjD=JQ?gX*-T$UHGsE2A+Cdn zl$Lz?)Jpi6qNPgY(5)U*Ve|cr$D6HPRD2fT+s4=Vk8!fg*pdap(9)B9X{;bYwhv5v zGIa-Cd)TWZF#l$UJ<0qh%e1J#^A0;-?u{Hi=O55z9GX}nr}c-FzSAuVB$INu*C`%D zfYbvE2jZrjU#PXO>f7Z#-_yISDi7Ow>+sJ@=7nyMuaHnZ0SIKbwO1QdeF@dS@kO|N zL~Po_iTd9DkkxVdlURA9YPk=U3R}BJ!PS6LPrsk4d!M~9_aIkc-GAV$JtQjX7*Yx|yH&Qi~Qzhw<(XRElaTviaDj$r`1mNm@AmDXwFQP*!XF(Bl0>u`KIumpPbQ2^nXWKi4Oi|%0JT+& z#EO+SC|#R71*~|EnDnqk*}~=$%e2vaKeuwNF9M4DP~cpAmD#0Kg40pwCx>a|HSnN) z`X)V7oyo~IZIstw1_1mMB2NXd_qfG;1W7464IKbPQTD0qh+LInL_H68sH#YNvX_vb$X(d6b z?h*iO+gY1Di{16&LxSxK0PUlO3(G8#a*sFGFkGbP} zxWapykVdg)sUpV9u`xmad!Sid=1>>~RDK@^^@!YXlwoIMzPh-Q;lRDl|2oJ@%m~0# zS3ce>pe0+6P3>U4V}Q|p+u6fLEnO8y?7I;&0I;1BE&8_+KXs%nGkBUVLe@{3YiF5^ zOezQ<0A0J9FQGRhJN+^(Pu7h1o;wB+<(%2$+0zXBfoy#r03ck0?V>bpy+yW5nEuW$~pMCu}enb<043V8iAi z`hd#~g8)l9lAfAWgMbH3a7UgSy~-cp)btBw`f|=bHJ~T`xUnTgEg$y@D*#JeATQvY zNyYqBL3rKnz9BdD^Cb!pEUa7Cly&ska=owS#<$H97g4)`dinXIBsiX(x^Z{|SD+gc z{Rbrs@$_c~XKayiF_E=6HYPx`dNH+uKj!WYT}I86{CjwYs(IYk!k*^~0Tx64PF3bT z712ZeV*|f#y10MR-|}58Htz{B0268_xg6cWil?2*8hHdW`=TZ9r_T%k?-GeJ_38_R zXBW5#@o)V1-L2!CjAru`&%vE;g#;k>r)`p?Iiv7xy8JW0>Rw!FFm-VgV*pY!&KmA0 z=@V@9vXI) zgw*3~Ycacl?GMXLo7P z-oF&uYKg*I)@9@cfK=L}DrGYWoz0WEi?h{{j+T9wdk6q%H84x=xCwnt7L#%oXZpBEIGwU&hmFsJO#!me&V@n@irIs^N|SATL6fJ zzMh!Wz{s{$VL${9j6sarbI|}8MIG@vzJKPJB{q?Hjy|6M@0>h}ARcp&5{7Ul-fUxIm?oKAsLV@39culGhh^OpCi2W#-8A(;Jwrf%R&+PpW>Qy3Hr*ARq^J;Dk*b6R0H+MlPm+p? znE8YO^!C7Y)-Oc_c!1;=e>QMy=};6-w_B?4I&c{20btVS@mu|R`b1@h@R@0U49Z~B z#4rFbwkVgX8=no7fb{7_06wXiVQr{?k9r<)aN8B5&#hVBr9DRJ?byI z@O7J9fuwd?u|G({=2)OLAtYkmPJOI}Aur>~Qt2rp4FJeRf2fCQq*wsnWr#f1XGt#L zP_)#c)T-utwGgMaoN?ZbeZ2k7;{$H+P~BG? zbsE{h+qHKHd;oN_v|m&s?&mo_ukX}Umt1#M5daC$g|m&Mv0!z}Z5lrVaNxZc;91Dg z+NL5(dgL}|B00qkBQ8Wf<@=97JGYwrdjnz#+nfnzYc)Sb)sJ6kEnIoZQU`HvNab4fIVuR z(Y1YgJP>txo0XXi`>etW?hDH&E8IbkCJ;0I2DMeDNBM5{CkW=*$Y!+~okvXs@bhtO z)7?-)=zje)`6{Cn-Zr3ZB?G=5XBPE*bOry@=lQ>L5X={ymdLm80BpynhO~4Zr;ZS@ z-^fS$(zX-ZIw3@DmA7A$o^4a1uOr4;uG6i)VpW2!STuc}xB#h?0 zH5}kd0_k-*Kt96h-&-JQ@2vnnPf>q?AEo9*QS{}IALSMo=nL^$7#LoKaO4=%$VT(} zX7AKrG(yz8Q3)=?F~cFi?;SgfcOLJfcY7+{JINFo-6LVA-QjGJ@jBULAC$OC4I|~% zWmHRL&hWb|>`UTRXhvMx8dO+*eq!2etXpQx0{6dd3cNU}h5NBROW=s}zrBK2yj_q< z68En0IX?Ksvv6~^@`c-CImtRbE{JNAe&U6DQ*62Z-^;UTp2qlv-)}KGHaZ^w>x8Yk zbVBF=jlx0DJ;j4)tL0SIv0R6yODCI#-dsm`^WnJB(sD0nODw@e237 z7s7AnVH6@Rig?r#PZYwb;w*lJcONYMTIu*qMBFk~-r|S|Tlk6)R0yZ|j_^J@OKbsY z`Cgvg(lxm4M4xO;^y-=VKGw=ti6Z%gpCGzd|7^f^M()jc5&c8fbc-ZHM_q|7yu)N} zNkt?-Q~6izMYQ0%p~Rez`;%)U@$^jT&t{y<8xJ>&4+GJ!zK{zVN?)uyqBNvkI<}iN zV^|RTU96DU3NGYCNW`1Br^h?g&b``EOr8D~^?+G>dsz+g3xi;dpp6>c&*RTxS0JEpTYN+fz|y~>O7)oqHf!fouLW8uaXnE%;u%) z&ZD2H-&_~!qR=~|Z~>T5z>{u&qIg0o-RsRaAH;>7L4#YVtDobUu3v6n>p+@lDQZWA z#O3cPQcM!&>yKudLH!)mbMO(`wvj!g9}m3SI?lWh0mD`S6ybc zlUvR~&;ckwU?8Sky~$sC*ifujUbEYhlbY~X>0+ZGW7(vi@O9=dls@sh1=D@{8!zCP zmp{;4IH2&-bhmfAB+)kJ)@u=qMX&k(=l4in0+Us<$VzNc7Ok?nRl_Z>t_c^Ho#a)Dfer5cV-`PF9V zG><ddjxE*YisXR{{jd;q$ALtEK=S!%wFW zgL)Sh#~W@R1}>|f2X!UB-u>{mT($p3S@4KU+fIAz&$k^eMM&P4w=iCf?`rfs%5_fQ zV8FQB*;_B383>C}b9NY|sP5pEJAwGpZ`3C*8ujblwqBjwH!Fi8_rEVWjd0x@Pr%x1 zaZIDmIp>A2>6SzqjiR1^KubKjTl2@xf;`KV+Se>GjH(qq7uo0RWR_n1GagH-(GNL( z&tTvH=)j?u0bIbIvqHJ2OKbXtGv&Z&pCo8|bc#IAc4u`D=$xy*`(C;It+7vWo1DI+ z?z^36h#zPk3?%|1h2NbM$RD$`_mY9)2~wu}Y(r%;R$p3^ zl;(ieryq}X%;=HQ(Yex_(3>jkT!M2xVd`(vyDah8_fqUh?VMMN4omsFkyn~Jy`&gN z4~K(vDjtkBqw5rDCp=x_{}Xo6n`cxzovT+q$Mbq9W?ZNKGZ9nIT86*FY%rIqhl+xP zvx?JV$3Y@L1nzfLI^!|Y&wzf%Pfv%a{QA6=3le?zYm0+^_#@&Y#umH3JQwFn5mw8; zqlU{({gJB3Vp7xia1xQAt)%E?SbaERVlZxxQF*m+xWN%DXnEs!bk%(cZag6}^0c9a zUOV%f@w?8rH+Wv%`wQ)3k5r;Bj@EiFPqr*}*OwUxn9DVGwFI{(7gr}LS->oGrq8dHiw78G%yz`@jPgYs6uL{xB z;S%8&&YpWk>HgX|H(*??Et%i;WxjR=x1XucL7*#YtWgH389;yzWTQI&YI(Pmue+3} zkBL}Sg*Td%8&>ytZS9sgp7$H2I->bnSoTeO=Q^rilZn}mme^C$iXCfEUo6L{ml&fQ zGV!Q3&WS z<{n{cW?cv7Wsfg7djIL4pw@7b(l)5@>S)2e4m&X;-bV)A(ot0zjfy<Ax+)tY^3t^-Y!Z%GzniOPvtGEzSE^O_cNgs` zG+hN-uk8fkGdgqLd*a1SPE|Y2iOmq#t+86S7y6<|&60Y8K^bIP*0d{PQ0HW&J!b^} z@h(RUbeT>EoeBEWgHdXml~xK;V$ICED7wPVslk}V9RbqY2~ z=CjFnzp<7di?3Ar#2e6McjjKdfX?!~BJ5S>TGRJ#({Ep0zwCd&mx%PeI+a(A&iu>5 zBl`3afHYk+PYYetbhi2~=G#6zcP({{K~E&4e-ZDr!p5yu5C$vMeU0K+rESSqP0aCn zjEv~cXLJ~AuxM*jZA}XnMI%&Z100(#p6)hoajN(pS^hS5 zqndR>RqF$^L*A$VcttRte6s2zZa8qGTH6FF?2Dp_uX)np+lP|vF6#})!)_vemGllM zDr!HENqIp%7!JcV1GoSJ>_F?Mjn_`v53ab^{^pBjbh~ltA`vE2j((Yt?EIbxoKAmn*==AlkyfO=?aoC;Gxo$%V8#K@;aPYl2 z>=h>L$KqlfsL~Vg#p@$PTe=f0IrHi|XL5mI*nw3?Euz1r#j^YU#@iGA@#!Z^&Zkwc zJ)2Ku^$GrP^EW7Qwp@qA{pe1-6YA$4aocrk=?SKO7mpX){1>w2byMaVHAa~xB3zFr z!SJCh!Kyc2zC;an;jUMr&q8AARH#D}{0?%rSZ2kY|v+XH%i0!J^q2 z@Cnb`@ZJN~u76vK!n?;P>K*0KprA=EU>E5y>+ty5%%_7g-KKod4+*`z08D*Ps*T^8 zvvoo5BzarrSv;+5kfGzNsNov>>E$Q|>Xo95FBd@VtXqSQF^lFbiZ>W}gNA|>|4w-C zC)#a~8|u36OjQ$llk=_k2H{h4NSpVswi^m7$)`ciV=e_G^EIxF`?Zfi2>tm1xxHn z3PHQ6Dy7}}Er%iYA}qKfh`c?jc<9M$5F+DPG{v+)ctJH9S1aPjQIn_d2C6 ze9y(%e16}`JZC{VC6Q&e0d9`Aq{slpBjt(pyi>wS`8?1d_9b!?7UMdKj3x=HlUNY) z*U>Q9LMguAJZfF0Nlam0M!DOwz}wC^Jp*<(`YhM2@ov`%cT0-wZGqszKP{d|#M5gZ zHbiVTB^h#mJi@-?svwJQc2(o?aBy(!HGeHGIzAPhd}zYUC)r^xdhxFG9lIv9&Ny6N z$FIQ*326#v^1&E&yy;eP9kkP#D8>&)>igw&pEEY_yYCur>4zYII}PTD$~S6i|AZU* z`)^C3$k~vrZ^>3rn7oT-z8#w9TAI6E2KlPcX1M5XtTu-I`#ZNm@r!S=&^Rx-1oS|s zMgCx(5@j=zuT*z0PVqG4rBA^1aP<~@h4Y%4(b})J4;U+dturcdSeNb?w0!#wj@h=o zN2_tXO_7l$n)5e}cHM&eP=_hFSWKbeKN1=$8Z#?UVO!rSYf0z63sIcvCws-uX4oWe zjV;s~XPr4pz57Eb#Jg9gk$2UtRkT2%2lbm7Y0GfMx>UNe|clkhZ6noKTm7@E0@gX z5QXO?w)k-E4fyT5zuzr_N{_7mWXliF&piEa3K2uT=W<~Ix&p(6VgwthMQVbSEIf(oL7RRnyZ7H5mR!r;|Fox4zN?E*8fSJCj*HJ)s>j zp3S5E_CD8PC6y>{>*C{Qxn@y|VPM{1 z4$z2>mKmu_++I6R!x0G_Mz0=IC9_$D$=Xpv6Gd*^$P}Gjv@6|A`skhU&4GEl17@U4 z@ix$Nesx_l9&_fDA$m&vQwh%#aY16&59P87*lXJJ+aX;r$GOYptYvA8H9p~xzPvKo zjZyWd%HDRQnBPZbC$d??y{}cg2xjE3@x$(N#(|4lFvzrXysotD*0kR-)S;N8St7tl zL|1dGi4gdOTUl+-2ZkgtF5oS3;?YQQ^%gSyw%^+nP-DA2*bk=;iDPC*q679%hp$DE zZ~%W0Gfg0pw$Mmt%yuj&oCc7;yD}We=6uh@O@4e1lTG~tTC)n95k{je#m2*)Vv5J+$QW?o z^#X&)GN_4FO;MqqvW=>>weEK5P8&Rh&ns(mLSfu@gRjw&F29v%)i&#dOsJ7U%?$BIIv%T)$FVkfOIq5} z&4c0Volkra6!G7Lm24&FUK4jXJ-MJm)fli1Z!s;aI~8HGNPQQquJWQAbSQQyTKQOi zpyJ-jG9jUZ)W~Dop9p0fn<~0lNienJ2UhKO-eVhaqrX%6rIuELBJO50QD%b76GRF| zF0Q$eK!AVK>*(cvL3l;e%^nE<OhDqzj0iMsbJ-Ciaz zFo=RD#iTPW=3bvwS@NQ3s^+2^1j#l=3L=F5lV!;+%f;zglu5fkr(jB@q;kZC{!Xo) z?b%dexAFIvdG<$JV3u|`Y2o|7@jprW%H0q4zSST8Wr@ih1Kp8}T`$zq3uQQa<)Vg~ z7xxhtj2`q7KY>>2DAVF9@rz}x8W5o52g-gdDsSz8R^O3gE^}Q|i&|!9_Fsddq@V^-|PqRSsS1rg> z*09!r@()U9TE6O);^w$*tH`fAMGxQPfAiNchmy9z(hwzMdSC2+#74jBOrHw0V*btg zTiFtML?&qUkCtEPMw|mxWDp)DonhvW5ikg50ZolX>D`O`{3S)1DMgI!<5cCD=lH;w zqIewDmhw(}$w7=`qs$7%4KPz#ZQ}9S&b_Y=zmpesn}H{u=L-s8W==1M{lFb$Nfw{u zpXmR6%`#QsJCw-;PUJWl(=#Rkacy+JC$f3GyU zKO&~v{s0Gfi2=H1OlV{Rv2u~o3-29%93W(xp*6o>5vAjf4dXx<)eSm2(3#gf#HjZr zMFSJ_ZqZkP(TmZ7a34^#7d3gU2Vy^cVMe?2H|IApN^U)%BHb;kn<+!_1|8SnzjcuV zL5){)-cH+-+}UU$qN-^kI-8@VzYOnqe(jLOAiR8Y2LAVHN7K{t?^Q)qkfZ8+u$%LXimVGo>$FL12lPL;66YPzk;~hn+QxE5CZPvC$}X{5E&n> z7qKdE+nr$n)9jJJX?Tww7Q2@K>46)3wFze%2Toq~I9vEu-DYan1@)h(lY7SB;V1|-@Bfw+7dSkOQs zB2n4Kfyz9fW*FhNf_RqYc!|Ly;9q{P8Oy5mB3~`-jdyL0d?>LJLPp+Z9OaH5a_>O{ z)BW{%4rsP}L$kdGiN38Q-M z|Gr@0THC+5JWI$?h?sfopfiEE zZ%@)D)HHQNU|8zQ~YnbYCaRqnt+IUJg2DxDD|Ke zB5h#ve`5BLI2@TAhf|8aDuxL8oPWb1JI+Psf&W`Iy4mr%;BspK6nwXZP}l+2|F28U zZ#8oV+arFLnKal*qECLU!x*z29nT4-v4H$jHYHHm$OLUzQv~co6se@(%{MLwjND+? z1yaBn!XYa1QqGcawWLd^R)Ojg$of0su#~2WNwlujEj4^xYFHb^H~ZNP>}Wu0Ho4Nf z^V}`=+TURM019kA`2sB9gA1g9=O0Pf>;q1Nrvi49W(^RoUwFtc1S}$jz56F3+l>BM z*@s|yXkg_<{|;y9yB57{>_Ui5-;qANxrPLlN5o@46NidN?k8FZ*Z&At*?WIS z);Q=uuVwvgLwXEH$h%aq}d@RY-8btuI$yHD5@a2tg7fz{&WHuw~u#ef`8 zCzbvlJ|xAy6Y^FEWF|@lwfhnbnQZoLF!t+=Tg;ZF`8SUftesU2EYZ_CKYn|CUi+A~ zyG{Q;jhsWrB#s1aXK$8l5U2*L>*hzR{ljIx-b${Qr1<}6bEwQW?bPg6ILy`uZ=)nS zbuf~^z@7;t*I{20kIsH}D);mmRMN%cyq-dZBSHMg%~xPG^^V0PF=9u3yt)l@iaRKY zhpqWy+*kqsrmE&{aJQn7R7LN#+)BJ`0E}wvqSD2wtC~r=Q14Fe+dGnZP&Pj+hR&N3 zp;-4N(yAs4Scv^2JRJ?8^s|8@oRF!iobuJ_PK@8p<+C=rPEw+O%T|*BljkizzuYRX zlQ%Xar*sGZW52i%KV%Oq4H`NwMn!W|gGGq=VJo)RGBg(dtk|SXXOB(^TR_z4T$6Wc z4)6y0k6QwSeR_(!4Yy#8F~OAX6`&$BKUHm;_bh?jHLHM^O;}J>CCJKeH6T>2-*~Q_?aGf&=SU=#A0Cfp;p~fbbWOkzN8!yK;B95QKF3w0e zUp&z>8Ix||#8~w zv_OR+qpIKRMY_ebdrfEQ6rye_+49fgpG09$c}P*CcOT<|ETI8w6zKWSH85t~Ou^!+ zT#VoKfdZ+B%U1?#uWqF9-wgmuU~vIZj2_VLWcQ#ZzLZ|-`jX&YK^CRsK^*f}1a$-}ky$iU z&~B3E_Hu$~IM;?Z95s8L@iS>C8NcnbW)%5z6{MuY^viJH{-SjMtuA@P@Q1mQ+^n87 z`LOM=a$H=l3;q%@L}f@_Q8D_`i_apOU-kY*oZgF0>woYy4_J;OJv|<&upUhR$_IHR zXa=sH$M4#M&GXN(fSE#TS>1vF{%f0(2Cff5I8jNJuaKE%2{}W1P>Z5EMk{z2G~?X% zUcZ_z@+l;I^s{w95}<$OQJ%gxQ99AegYxQG@lE3hUWNf1A9| z{O>6ymGi#pdteHi!bNJdl#0dNax%XoZN}^N%-6jb=cT9ALNvDHC2>d)|7gIL?&2Qt zf+`;dSKCb08-dj##}+?bx_r-RNw9Qi<8C!xX2NEm{T3A&jJ%{SM~B&#H_W{mCBn^n zf4iyQx~+?h#a?)>=+tSFV}lu`9uogvAth_6SYa!=uw|)rY0zO*Ni=(Gnml+~9+p|i z_!mrjj3@30E~P9j4WzGNyZ{>cV?LUoIA$OASTS1AU)dH-&{ zZXXtoVus^R5>vXfIroe(3gO{;te11ggO+{x>uM^uh=?tySlS@*osWn?WAWvSOcrd@ z9|3(qDP`EZWUg~CC(F~>5*Qz}Q0zYb%!VIsUDSS?F*tvXZP+b|I8bMUFBO+JM}x^= z`iZr@8`j*|G_bm7HGAjr55BScfRgAuUusmV{G`XuG=((?jEF|k0`RTC8tZVG$<-~Y zOr_}jahEkp)-ul!k|ZUNjoR z>ogXuWIiM2Fn(>4#V2_iMX%tL=P%2DbM_m0XPXwoG_87iiJ<8Ef9DTqj>g~Al=VLk z*bx1u?T*Q)nr^p4kT~3~n!-QvQV_S4#<2ON6^mh`wzjb=Z(Ebu!lGuqou_cY8`naU zf^DR;u60XU>wOMZ5_HW>n|6n-5o6Bb0(ve2z-zKD3lIF zm;9@i3~)boSM9paRf;Kz{}Zr~9| zRk^SlM?(s888S)4}6}&6N48zeSu8 zFSR7E_dmF7IMLgJ2odNvLM^@C>!OJ}>kQ4r4eqeIFnd|vLpkxtWZD90gWp$rLYlul zVAY(N!V`GxcD6g6K>x*Bw-2Uo}{Jvl(whJEB6q1QwI{N(c*!#c8XwwmV3xpi9+S z`nZM17n`H0iqiSt5O_SHn{wQnu{^LpKo?3e(v8;{ghwHiN%_FqfITE>3(037RX}Oz z0e%sD-neeB`O4~a`)@^rTw7u+DeuVRcVOw@@Hc&gVH~Y&!4wVD4A|xcm=YBUY*G33 zW6L$}Ql(W}0x*({7@MY&FITDl1?GeXrBSM!!413hMxc=E96n_Jnfj@60hLfJWjMOz zDoK%`Sg1+*V6&_>&3*3iNP&7ne~Mr)7zuu^d7ewJ5N;^#e{tw&Rl?}xzfXTd~;A^4)S^+5tdzBd>{&n`Ee&BZvj+>F2B`$%_I<5(oZVN|1% zBB44S?tdHUT%=v8EID#X;`C_<*#PJc$QXf3pcY1<1!IyokxH>_<(o7g(tR{Rd&UUD ztqkg|jHDygAZAG_=+Jn)Gco8jEhr72S>_s}N-Bu7IRBXf_6NctldYETJ?PshBl((S z^c&Q~iwCYjV3ZySQaRv1;*~mUOZDs(Ow4pl7H$%8%5^P+?^c{@7O8lGK-}vgd@gF* z1RSY#46v`z8ECfPkv87LVeV9p9iN8cuH0|3b?8GzJ*Dh#Vq)En*ST@p;5g@ue};Wt ztyc1qUL*VUkSTvEh({sI(MP!3jSr963B|4yfTKi}$OL4kR`YxLT+oZ~*iDWkM@fuq zDq7zk>l@=^#Y438S({4`9sMmasC){>s=Q!koH<(TU-n>V>Snc)PmecAk05})g%j*w zYS2}D(YUu68s?<+Jb(CtoDK3G3*&Who4ZWTHRVdK1_y0GuG*G}K3AeU>QazW1|~UW(SqMl!6h>DKT}Ic~TW>-c;%y+!c& zoQHaK+DG@L2&#jrnbjCgaLs!;#@{In;zv!T#&yc818K!+XNi~Vi*>#rO?jT)mOA?qa~=ykK3pkBMxs_Yo4nn_Z^G3#Cb{}7#et=N*xV^TOkitV9oh| zayu?EYNQNz*ZV%b@aexld*4q^)TYFw{-8j&B&@|r7k7Qi;6yfKjH+mCG78Rs057>nCYs* zH(BIh`>Alyns&B1*oaa(jgi+A8^g20swm8EV*Fk6VPVONW3hHA$5_!sEQo{}JfeS| zqifG5G*M!r@Mf&c*4)qjCjBKs#WgP(aZxn(=C|;ryY~2^$U`>0?>%%W{yCN#PMX@c z{qN;Nj8>va@qj3R=n_XpdP>;8OkB7xriM_~9;7KkZYeP8^Gkr${XnRtM z#+9xF0xjGxffo}DQj&yX)3pGK8au{J0`l>9>94iD32|VvZVd#JX$!-Pod~?&_c8}v zYCVK6g6%VAI*})UcuRDnamM={0BCyt7K#F2f@g%|F&~xXQ*Yj5Bt~{H*q9J^DS|SiJ%Z}wd{$2lo zP&6nmDQaaH-<|H?(|2TLKW;Og{Q}h2F?@oR(7`tBE{QzBD3OUx$VZNGd=G3Q5EsY} z!zIf}CMa~?63G;$$fh9xH$ci%^>RlqDIJ!gn3nPjKnw}pe2d^-05_~Gl&vt@{&}cw zj)cY~Ck4b;00Qytv6MGEU=#Y2AW01s$!&WTA#hXoYjUkWRe(?ZQ6sadPf?v$f98T) z2+p{@`pH?>xL`@=;=qJ*iK3)tx=#k~0+0)Y?4u95*F!(cy*4@o-pilJ!aF*^DVUYY z#kYaMeWzp4w>CWUii}j?;QL<`p!|>_Y0DZm!l8#MUKF<|REoq{6U$S1NQh}jib zW`wM>1f_aN zT7OUuY{5=3$Vbn_ixSe*LzxYL;r$d0BK<&EOwj~dEtg3cy zm$C^&0rY9u^H>kY+Y%MbX~4lb`w=|!^f3ee&o!wgmC?moJ5T|b@hV9zrw>t-AXrgq zT! zlqj3W=5+UxMGOA%ycSBJriJKc0Pl0mOd=*o`ctZps0018M^R(7$GM(^gqi-#-#)hT z&$$qQxmgagC(o(q0>E!+Je&O%+V-?K<=iiu#;|>1#%D2D*Z?5h@hjOvHx-k72fdX@ z`FVj`3*MvzBRc?C#*~t2iR4?a6s$O^Bm(Z4nT4Yr9pG!Xjjx=qWE=}FD~#pFC+mql zs`2Io7(sM5_3;+pRtq;F|LOz9=W7D#i&6ksIi?s(tzi0`$IV*6sXZnn#0cItNTtOF zuV_ir*@X2hVKZ%1x9+M~-d&V=)7c`RrXJkUl30d2j~(dB1-AlkV1c&ul89=4|D)n{ z=!K<}Kl7g*e0~i9-tQ1#o>M>fT@KG5UbWbTQ_*AHR@wYyq8M@jK)_7uZD%^82$dO~ zy2h$o-x5;JoW(7vI03*pZcueX-%%0*mJm#uQHw&hK~w%J8+?ubJ7f6unoSt^+(#)r-RNF&5j`S_@d^L{4xeDBroSc4e)(b=Z9i~?pE|Qx z4VLx<6e+P^5r)+HU+CHT;#$P&h~}}*UAES2Hlg618WuO(q;o0Z*ziz#t@eYW-$t z~S?$ujt^n>*cDj zY9fMqX{Ldv#hMS07uZgVt}ohq)8;fx3MP6}Bt>ND=keV~u&O=>q5=fYK3m5aDfhcI z+}xld4J@QM0Kqe#l~Xiwf;J5`3Jhb18HUh%0L|uwamL@1;Z%m}tkfTr^H>F&JFrkL z5YGSOz3NzTlh&-^>Zlw1Q8G406e8V3Ng$!l^jDJ=9R-jx2$W<$%t4#&vHWp^>K}V_ zcz(pRiCUcd)GS`@2mKQf&_GqHKdXz#`$=7~rr4UHqro&Il;(kPcX!aDA^l7;YIk4g zv&wG)t8=KdxTBX0)4TvNE%H*L)0M23PD*?;Gs^JPFvl#`x`z{81q}qSoBy#fSLBH3 zpP7?ljlz`Y;A|yP94i^A7olC->=5f(Ly;-QT@j{yEA&x^?q7e5D*H~8vFVK!HqbeN#IALq@9qZY^ zu3U-8)=M(rC?z4_V-K`uRxoG;FBW{Mv*DlGz|D|#k>;j%En0D9GD}pO6Mym&fbqWi zvio(eVDOKR+=B<9jIHHZ@9kCX-w^YH>h|n-WXm_lWxA!NcjFmTbY`MXvLKrQlQW@f zMZ|i?oHD6702_H3u99FI4X;*?XVe+#!{`hWLzqP~e&-wBDJc}`_J8#q3g{Qf#S`X+OZIp>c^~DiJ4ijQWpP-*ziE{PuRykBs=qFi*b@=C zX_JwId?eie#FI8Bg=X^TL!8MrCLq|m;K z1bxr%`aMjihB?UNEqLK4VUIuW5C47;)r2#GPR%h`*$?xkPDTmO@$0Ab2=UU;HF(bp zDn{@=a-zUSrE76ZkiafW4=-g_r7+cgAloLLmlVm!12&6z3~WG-X?5Kx&fz3NSEPQn zjYj?LM<*#Y)6C4|0NtLY9ziiXfQEHH5N~tBroP|5+{xjS-qw-63 z?-^%x^I53tkcchhU(o=Q?lq3lm%8hfuOrD|pUBSnzjH=*5s}e~x}jbhu)G0g)J(r6 zCI^ta&?eveFs<%7DAFZ+4{+j|r{=CxBs=yWweH6hpJ-`azzRAE?8wrn!_WGi1S#%& ztDMiI-L-@3t|utbmfD#%MJe}(o9iD_$*{(#WCjF&Wq`d+yxOzs_iilN-4k*3ai z9an$!(`#o{IYGy$Rhh=PWP3gf9q>1dfm5d!H*0S}McYb

77@FP3?Grxgwt@3N7?9&aB0j@$-GOJW?Kb67XDR@cNBNQ=6M%+P$rn~K2A{Y@%`#;+$1zio!r%E*p280+AFr;Jo7hgM$79-@(2IrS zpNISHEFYp9z4?4_cxfHF;jc9WUKLB7`zAO--KXLj6zy^N|G + + + + + + + + + + + + + + + + + + + + diff --git a/crates/egui_demo_lib/data/icon.png b/crates/egui_demo_lib/data/icon.png index 87f15e746e42df190f84317c76a22f036c50a234..27a6c9534b32f4704020966693d11135c3938aff 100644 GIT binary patch delta 1654 zcmV-+28sF76u1qLBpLy8Qb$4o*~u(_00004XF*Lt006O%3;baPks&pI1ONa4#|gtq ztN;K232;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Rl0~HS?EQ%Yn00005{7FPXRCwCu zn_WnhSs2HEGvgRoYEjO(yS7s7m|YaoRnbKR1F|mdE)`t`wP|>h)re?OpaoGfT@)eM zNV~aM5ZP|HpaandU6kH`*sf~`+C?(DX)+~3<2WDh*$Z`Pb-vzt&pVm(hXXU5GtZpo z|DN~!pXYoShz~39Jx~UGGxt4@rHKh(92f#Rfezqpvc?L5Rv-+B&$NVro50sEO(W;$ z%P#!lg@>WDzg{plCHz zS63%eiJ6%hVYAtPWDuaB)riGn5tEaXBGqVbZn_2=g zGc&n(@#2yQ=PVWrr%#_wEddUP1DnmZWMWz^{V}GRaXp}AL>g25&Sht3>q3H-IZ0R9 zTZjh_9z?Z2GBTpcc5iQQGP0O>cKnNkq%ggm4VDH|&(!$uLrY7A8 z7#kZCIXO9hatTXkGC6h#q-4js~kfF!cT_>F37Yo#YMilT_imoJOl++67d z`2BvdapOig$&l4*WpHqioSd9Rb;HBM+`W4@VhC%sTCv$|Y~Q{;X1F{xHAPof7Xt$W zWMyR`MB=$wUS1xD4%v)!wdMyR^Lmn>KBVDSMTbl?ivWs;Vk30aaC1stI`d^l4Ok&z?QgmJODc zma5M7qT$-LYq(slm~AR5Dk2W&WU*t%4%Vz$lYAsNH8mwQT0498EYs7|QR}yE-Ku&~ zBo+dHfdGELKY0S~-@niM_wVJMUYMBR;lqb9+cY*dQdwCk<^S&7xf7G{xCBg2PIB(t zxv2H|`T43l)!*MAd$+=5qPe*lx7&@~ZpUmkNAJAR$hvjwBG&nQKF*vu6SqHcWzcLk z)7RHWadGit`4EEP;o+F8JPXm@-cCnH2RnCv?xeiDoT8$lnC*kXAmiiX1OkCYb*omb z!eX&R{N4HU=c%u+my-;Ym6b^gfEpVc|Fdg0n?-$ny-*ZIYLMmec*M$;E9K?_Zns-H z0rRD%rba3O2M->UpA2o@yjcVS0bK|H#G^-#q&VN*-7O3TgZx$Dfq?<8U%#%~s?L>v zD_69y?+by$;ZPM$t6C3OyLPR3_wJo8BOog)OY>ZC>((uO_XWIOZ%kR#0-w)^*Xz~v z`%|Y*>Axn})YQbwmoIf8VP$*!^T*{!nLRz7P6z{E7@56i*GBrGbxr^P_zLimURP$;FJ$Co z8WB(xZCIrBwKzidJo=gjXk2@Oj;Qp+?!5Dx)I z=V@Dc761U?C0N;-^RoDb1XcLI{;lN%0Psy(VNIN)rdIQb4j!YDJ!n?jL1$eH1}Lce zJQ)*D(Ra)ep_V+VsI+sRCL7)=SmJ2At!5Cb3LlJ+1E*inkc-p<#+=*ggwn`T2C6|W zYf6oR475!a+vK)1#cFXeh^7-lE&c7$dp*9=Qc{eJMlR$W88=xUIoHD4TRX>^315#J zK|Qob`Wd1HBodRqZ;9w=6@Hd1F#o-xVEpyjqlb^_B#a{zQAQh^63GNS5{@bhYm+tQ z=7+p^?`Y!eru9@3yW3Kz3UPH#J+G<)EtTdHqL*TjaN*vw=5LARBHVvro>k&dqp#LXQe1}K={vQ3`aRs$pJC#W(V9P zz3eLb<19&8mk1Vn_2nZ!-=Ov}Oi-)JJ|E{4vj{&>29OTr_YUMVl0I$i)c*nhRU$th zUp!x<5Bkpd%^*D?8G2aUcAPo|S!9GxnMk?y5CORcX~4GvTr*Zb7+3dluW<#MC)|oP zlIQZ?i)7j{3ns_tT+ZB$?$)@iC|M9F)fA9yd6KyCX>zUk9_K}Xj0+I-IZ<65qy1zz z5*hmW=Ju3{Ga!e%sVBD$3WfJ~D4`KmKS zUsZ;FTs;g}D$(aBMnRVOFcG$3hGqX~uO30Jn?$UF=MxS3;^rE41Z&1^Y;Q|yU(#75 z*0TzG;*a5)W`h^lC{Is|$(9gwNbAnkspE<&x_Su#GN(J(d@-a*1c=mDO8`|qnSA?N zva03Nc{zD`SBFd14Yza0@~W##T3c~~8ECfNy54$cXga7bVg#J^Wow5km6U&f?s`>> zmzQGXOnbTz`jB6H`70j?c!A0%DH>1g1lZ*0%%_e%JaIPS!S8#*D02*Y1N;tcs5W>d zIsf9%qOHiy!jRdN>2r0DZ{YEZG=-Y*r=^LOYSum`Tr^r*WlhcE%v)=KmX=o34aQJt zv&a6LFcGU}(41W$3Q?JfShkgYyPd=^`oYOE+Mb>pVaICp`5|UMtVTFF-qI&F*0dzL zhqQ|p4_Wy5C~yB{?okztx|Ty)Qv&ivRtFP$`y^_0(rjRrtWMrF4H+p?#?zd~So-j&+>u@EL} zdTyWR*&~IB^CcQh+wDsAY~qgeO1_=<1egIKU-;HrV!yFqlRx}z^rc)A=ec0`f{KC$ z25SDhr@~7)`QbYB;cZpoZvPvrbLT|oIdp)%@c}r~5=8Qb_vUHWgbu5IOb6ujcVDhP z5R2_l2v1=66deqNcP+B5gs+b1O*RJ^tm_$++(X}*e6#5=&pV~?|wDz0A1 z$zGiq#cmcwmVf__O<3&7s1F)nqgFj%xM^+nTX;TzA~EX1y69zG_b6tWQiv^i50!_N zN>F%(%h1x2lCQi?$crG>cEr4spa-JM;+y)?`^F;XuMqYy;s*Z9F#Pt(#3t}qR!doa5$JRQJ*qoTgRd+tV$)TO~*ixdwTOy4b%J4$_U@bDHBW5dHmOOwXQ^fZHd z_+^r@oF47N3;m|gM8<2{)<%4}u?Za=O({Qq{GhHREsuwFe|_@+{LUk8QC3;>S);)P zucDl!ej>~ayRj4FK_x&>QYciVJFr6NPt6+?M{8vjJK6VDvd;=|iQ7syo|U~$?M{>8 za_}%Jc6lks_i=A&ZOMnp0hpPYX_g|q+=7le$oZlv16Rz-d)Yy6iQdk%RP(pSUIHSj zQ89==ffL62#(2%r3ZwG)t47};FE1ngW5y?mlU5^ZjlYK8)6)|So*)!&<=ch_gTw}NU^(P(r~ z%im%K)8Y*}Y4Yu{;;{YIZnE5P7053in24h4{_%F@=4?Y@WZpJ&dk+eQcCz~A$BDsJ z-`t?-5s~$`m-W4-sN}UZMU66g4Y^AK#T^YoHJBA3Zp3yJ4;5VQ|Av#^Y!)0WZ{5Ej zASe*pJo^;M#>R(+Kvb4y6)4J5 zX{S*#^5f^RVWKEnAf?2Y_aaXl_k6I3BR$A&?x4g{_Ta6k-iDH>V{qS$@{p05ORh17 zD!I3^s&Q;xXc=qTt)UJX4pGt?`*Ml7Gr2~)H&O98nETDBgdYvgV679CjvXJ07*aU; z0&{&V$6|I-K&suryeE?jGT0-n%Pgn2KjxK7yOr`9tCKn|ZJGJX0)%!LD$qI}`hVV20xTvQHem;U=zaX;^JRTQJ z09aU^hlJt61McGdAR)n!8w5gVxFHONyL&yr|CDbC9`=v;{A^R*5i0fzbqet(MEZo` Y0RIURdCI7FJ(2-d=62X>Q}6hH1N + + + + \ No newline at end of file diff --git a/crates/egui_demo_lib/data/peace.svg b/crates/egui_demo_lib/data/peace.svg deleted file mode 100644 index 4bf3e33a9..000000000 --- a/crates/egui_demo_lib/data/peace.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 56e2f1eaa..853ebf490 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -27,20 +27,30 @@ impl crate::View for About { fn ui(&mut self, ui: &mut egui::Ui) { use egui::special_emojis::{OS_APPLE, OS_LINUX, OS_WINDOWS}; - ui.heading("egui"); + ui.vertical_centered(|ui| { + ui.add_space(4.0); + let egui_icon = egui::include_image!("../../data/egui-logo.svg"); + ui.add( + egui::Image::new(egui_icon.clone()) + .max_height(30.0) + .tint(ui.visuals().strong_text_color()), + ); + ui.add_space(4.0); + }); + ui.label(format!( - "egui is an immediate mode GUI library written in Rust. egui runs both on the web and natively on {}{}{}. \ - On the web it is compiled to WebAssembly and rendered with WebGL.{}", + "egui is an immediate mode GUI library written in Rust. egui runs natively on {}{}{}, and \ + on the web it is compiled to WebAssembly and rendered with WebGL or WebGPU.{}", OS_APPLE, OS_LINUX, OS_WINDOWS, if cfg!(target_arch = "wasm32") { " Everything you see is rendered as textured triangles. There is no DOM, HTML, JS or CSS. Just Rust." } else {""} )); - ui.label("egui is designed to be easy to use, portable, and fast."); ui.add_space(12.0); + ui.label("egui is easy to use, portable, and fast."); - ui.heading("Immediate mode"); + ui.add_space(12.0); about_immediate_mode(ui); ui.add_space(12.0); @@ -52,12 +62,12 @@ impl crate::View for About { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; - ui.label("egui development is sponsored by "); + ui.weak("egui development is sponsored by "); ui.hyperlink_to("Rerun.io", "https://www.rerun.io/"); - ui.label(", a startup building an SDK for visualizing streams of multimodal data. "); - ui.label("For an example of a real-world egui app, see "); + ui.weak(", a startup building a data platform for robotics. "); + ui.weak("For an example of a professional egui app, run "); ui.hyperlink_to("rerun.io/viewer", "https://www.rerun.io/viewer"); - ui.label(" (runs in your browser)."); + ui.weak(" (in your browser!)."); }); ui.add_space(12.0); @@ -72,11 +82,9 @@ fn about_immediate_mode(ui: &mut egui::Ui) { ui.style_mut().spacing.interact_size.y = 0.0; // hack to make `horizontal_wrapped` work better with text. ui.horizontal_wrapped(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("Immediate mode is a GUI paradigm that lets you create a GUI with less code and simpler control flow. For example, this is how you create a "); - let _ = ui.small_button("button"); - ui.label(" in egui:"); - }); + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This is how you create a button in egui:"); + }); ui.add_space(8.0); crate::rust_view_ui( diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index d2cc17448..1c7831016 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -119,21 +119,30 @@ impl Default for DemoGroups { } impl DemoGroups { + pub fn about_egui_checkbox(&mut self, ui: &mut Ui, open: &mut BTreeSet) { + let Self { about, .. } = self; + let mut is_open = open.contains(about.name()); + ui.toggle_value(&mut is_open, about.name()); + set_open(open, about.name(), is_open); + } + pub fn checkboxes(&mut self, ui: &mut Ui, open: &mut BTreeSet) { let Self { - about, + about: _, demos, tests, } = self; - { - let mut is_open = open.contains(about.name()); - ui.toggle_value(&mut is_open, about.name()); - set_open(open, about.name(), is_open); - } - ui.separator(); + ui.vertical_centered(|ui| { + ui.strong("Demos"); + }); demos.checkboxes(ui, open); + ui.separator(); + + ui.vertical_centered(|ui| { + ui.strong("Tests"); + }); tests.checkboxes(ui, open); } @@ -267,22 +276,20 @@ impl DemoWindows { .default_size(160.0) .min_size(160.0) .show_inside(ui, |ui| { - ui.add_space(4.0); - ui.vertical_centered(|ui| { - ui.heading("āœ’ egui demos"); + ui.vertical_centered_justified(|ui| { + ui.add_space(4.0); + ui.add( + egui::Image::new(egui::include_image!("../../data/egui-logo.svg")) + .max_height(32.0) + .tint(ui.visuals().strong_text_color()), + ); + + ui.add_space(4.0); + + self.groups.about_egui_checkbox(ui, &mut self.open); }); - ui.separator(); - - use egui::special_emojis::GITHUB; - ui.hyperlink_to( - format!("{GITHUB} egui on GitHub"), - "https://github.com/emilk/egui", - ); - ui.hyperlink_to( - "@ernerfeldt.bsky.social", - "https://bsky.app/profile/ernerfeldt.bsky.social", - ); + ui.add_space(4.0); ui.separator(); diff --git a/crates/egui_demo_lib/src/demo/tests/svg_test.rs b/crates/egui_demo_lib/src/demo/tests/svg_test.rs index cd73f9150..9c188df74 100644 --- a/crates/egui_demo_lib/src/demo/tests/svg_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/svg_test.rs @@ -30,7 +30,7 @@ impl crate::View for SvgTest { fn ui(&mut self, ui: &mut egui::Ui) { let Self { color } = self; ui.color_edit_button_srgba(color); - let img_src = egui::include_image!("../../../data/peace.svg"); + let img_src = egui::include_image!("../../../data/icon.svg"); // First paint a small version, sized the same as the source… ui.add( diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index fe783af1c..6e23fca92 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -229,7 +229,7 @@ impl WidgetGallery { ui.end_row(); ui.add(doc_link_label("Image", "Image")); - let egui_icon = egui::include_image!("../../data/icon.png"); + let egui_icon = egui::include_image!("../../data/icon.svg"); ui.add(egui::Image::new(egui_icon.clone())); ui.end_row(); @@ -237,10 +237,7 @@ impl WidgetGallery { "Button with image", "Button::image_and_text", )); - if ui - .add(egui::Button::image_and_text(egui_icon, "Click me!")) - .clicked() - { + if ui.button((egui_icon, "Click me!")).clicked() { *boolean = !*boolean; } ui.end_row(); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index 47ad5bc7a..f987b948d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfccdafb7e96db488bb5bb8c0a7d25f70e63d900d6b1c2280d218aac0e70e4c4 -size 26977 +oid sha256:24f4a9745c60c0353ece5f8fc48200671dcb185f4f0b964bbe66bf4a2fe71d7a +size 27067 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png index 8aa13dbfa..4b560e20a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888ed4281c2c779b08bc1719302b9923f542026811cff8ae91e44ea1faa25783 -size 25804 +oid sha256:fadea24444c402695db6cbc9e03aef8a0ed3c5db487a324fb255d38c14f73dce +size 19804 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 277f7ab2c..2d57b2074 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d10b78f4d80d61a3352d7f2b0ed9b2d93af5f184f2487f6f2afff02a38f4608 -size 33475 +oid sha256:f6105c95470d1342f9003ab03e71243b5e18a6f225261aee94b15f8f0501572c +size 33542 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png index e4d385fce..4495bf173 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5a45307147f19f2d69a3de1f53e0a73ba4c3368eb25a66b4098fb54cb83822f -size 64203 +oid sha256:ef245aae271ccae628bb4171f7e601194c77fd18888ef2ea829bea75bd38b0e5 +size 64965 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png index 102cb3650..7c47f522d 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0102aa84db99a6da1db1de3abf67f13c3b571de00e79e7c55805dc0504658d50 -size 150111 +oid sha256:e621561567539ff24b4d22b53b65fac6cddae71d92fccd7800a90972a6de3e0e +size 151100 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png index 091948af6..520895ff5 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3991cb1f922e0c6712d045b3cd8a1d98165c0fbef7e31b15d587f244e53ec04a -size 59343 +oid sha256:e6c2d538be7971169bbc4473945e6815eac8c5dd6372bc1f1897a032b6bca12b +size 59962 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png index 881f1b0d5..90311fddc 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:355d8f08d08011635bf812aea1edeabd69e1ac3c724b521ed243f2b52e9b444b -size 145257 +oid sha256:d705af99624cd2824cd1f520fa05481ac67b8913feebae836db7b99ac60cb466 +size 145841 diff --git a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png index a82442e1e..2a4621b0e 100644 --- a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png +++ b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f4a038f9acbb12880ba6b681ef7d3ae566045c4474aa31e7c6d746c39a649fc -size 11108 +oid sha256:38ee4acc23d9c66f127d377ac8a0dd3b683a1465ca319fba092f6d3cdff8c266 +size 11166 diff --git a/crates/egui_kittest/tests/snapshots/menu/opened.png b/crates/egui_kittest/tests/snapshots/menu/opened.png index eb55bd894..c698cdb4b 100644 --- a/crates/egui_kittest/tests/snapshots/menu/opened.png +++ b/crates/egui_kittest/tests/snapshots/menu/opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2965482e0161b4ea99aa5b4ece32261dbe246f86fe43054a754fbd556c7a5896 -size 21666 +oid sha256:ac1941f5eab71bfad020132eae47e1995efa17410b7861aa9f260032e5b0472c +size 21785 diff --git a/crates/egui_kittest/tests/snapshots/menu/submenu.png b/crates/egui_kittest/tests/snapshots/menu/submenu.png index 0a78e4e6c..f277511c1 100644 --- a/crates/egui_kittest/tests/snapshots/menu/submenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/submenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7592ca6213497f686d105a2e686d0c5de364388ddd174cbe8abb425d27ddcab0 -size 28505 +oid sha256:b1f1a4dd9de1d8405c527c7f8f04b42ed9d403d0ec507bb3ff650a6896f28df0 +size 28628 diff --git a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png index 84e6ba152..dfc2b707c 100644 --- a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a1adf0903f0fc50323c2d77bbc491c950ab0dae6593c004770ea7961c2c6273 -size 33270 +oid sha256:af05a9b66340e0c128d823d3935a23bcf17cfeac02a822e7277234a9c8eb26e0 +size 33393 diff --git a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png index 9709e159e..6ceffde99 100644 --- a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png +++ b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad75a0e568e04c20d0e3b823c7e4906c39dcd0a69a086d8e30714a9e4530d031 -size 2128 +oid sha256:cfc03625c268f0ae067d2f4521a8668b47e4bc8525350d77a480840a09cd5083 +size 2046 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png index acfdb810c..765e63f05 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_image.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f65b7221ac74991c526b68ad2469f42801f6083c9acead5bc923fd856a6311d -size 368614 +oid sha256:24c85a987b0b80961b656f386f529b7538ddee59a030d02a0946d0f714ce7004 +size 368329 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index 79cda64a2..6c63fb759 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f89cc5b17821c9f30f7a086bb37668e4e7913705d42c0678fb0f42c527abb868 -size 334498 +oid sha256:8f14f770785d01b1673d1c8ca780bfff72e51992794dc7233cf5ec4ea99cb3e9 +size 350648 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index b244a86dc..9c74cd8be 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7f87fb417453a98e7059535cb68b12549d65f8da7cedf7a48e7154686931e16 -size 419858 +oid sha256:231ceab75a602eedcd11f4f4ed34f38fb9d072f5cb54e135a7e02d33d257f86b +size 433973 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index eca582ec0..6cb7241bf 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2a017c2b93d1920ae85792c13eafa2fd43f93b2e3bbaa5981ed3a43050c0995 -size 11808 +oid sha256:d53f67fb3a3717f7bc5ce99b93bc21d1d6580899dfe8e1371ff22bb416af0786 +size 12114 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index 4848b0781..b278f6c25 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42cbc8f8740f56ce45c356262d9b872e3973844ce552c6c09e3c07425c3f86b6 -size 14835 +oid sha256:e298d89e6fb434e5010d96661fca40bf119118b6b31fdd9fc13201bcd74c8ffd +size 15149 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 8c30d3145..9a1e15c20 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11fdd4bde01102e7998defcaa80c1105ec9418152314c74ee028b692b26c6be8 -size 14407 +oid sha256:a0581d601f1e536298cb52bfc8a167aa37aebdf065fc910973a752c9c159223d +size 14733 diff --git a/web_demo/favicon.ico b/web_demo/favicon.ico old mode 100755 new mode 100644 index 61ad031aa5b8aadf968a2b864649c3bb84b91735..59a822c613e6befe20332395bd91fd4eed421d0f GIT binary patch literal 2932 zcmaKuS5(u>7RLXSU<6S_+5vCqN)4ffc0>XJfdipPCm^DsNDTy}giwM+no1K8ETADE zq99;Hg-ACja6%{vO(IgIcNK2#@#Q|;bsuK-+B19Yhxz9BtqlNN01qG}1aP7Zu*3xb zZ~y>gWPax#c>q9x8vtOi-+3Ac0G2o_-5zyLkYAFYQz~d>iFD*dP6t2;%*WZK*AFgo z${Vea2&b4*^7-bV4<;f6f7J29a{E!nb*Q1g5HqQ%dN%)-Z69^(z7p~fiAgnYqawmZ zAC)Ag6iKxs^s2>jCRGh!-x|yk{{)ZzSXz6x{7a6#y7P2mxf#|031%Io9?NpvQ>LxO z*SX=jZRz6xaIhA5XRzWnrpkLOK!<-iVkBJ-c?NQ`Zw}{EKk`cAk`2h*VbFWfGHYzm#iQx>g&A?n~`UX1PQ!k$^c<~UB;E{!uR$JMti^Vplj<+-& z&Hb_&w{2MwY;ZU+jCzyQJHwieHI~ZE%%t9|{kaM&`Q)o4ve%A$*#B&3XlN$@?d2sa znN7IFi|88k4Gjrtr&77}Z_ssp>m*cERdov{WB2nMRGI7R>zdly#GW3TvB8mGc+aO# zWwo_J;^N|u@d_pV%wRko?Xa`71a)zBC1Y{8jnsREg`}k=PQ&>4_+t!i^5e(WHmCB+ zh36L*Fvc%gHC^)Hl-Y27C!UFw>U+N+YLuC8jPP-P5_ zjL5NAtmfX{o`{~nHB%)Gjlz%WRQBqF7ca!VgiE9&O`j@8H+k**$Hcf9qZrmj1qF)t zN=uboUUU}R8vO&zi##*TWX4Sr7}j0blXzq4jmf2@qeLPR_wetp{as5-FfKXhK_F*5w?VT=i|czfTFL z@61(Mc*l}vcGYRmPk-H-pP#?KVVl})FeatZSG_mKA@%=}%yD)A_7BOf+`A+I0NMT~ znc=}w#m9?Wh%d>~{J5(LLCY!Uis$4@=C3{aT!uDWoirb?6DA$k?Kra?4*65AT=~~$ zX&LtI)UU}1O?m4M8)9HjTTy~X-h@UEW_kJEs+4h~_tyT?cSFpDJ+BE{suAqSi6@LJ z%?q1*%^PtEvuySyp`GK&$;mG1$OtJ2WMAIcn2=Jgl*wx6e4A;Nf_ViaBbJ4O2yqY3 zR~~q$ySqEqvC65FLZKu!iR{nLR+>QzCw4sDe9E@Awxk+8YQp!{E+ArJWUrZG*J;AD z>ogke^u8EB7(5n_!Az%KlsF!sbH{HZHcrD!vqHhZxpXwXEHr*S(8op^Y5O*WZbxs_U)VBilS0RN`Jq@ zgFs)biOI*lu`xm4x?%R6*49=%LUxL%xVTwBK&9ush6WuD5~R7gK&@mkaq&j9{8@Q< zVl08+H*A0YJiD#hNyU8^s-f{mom-E=iLynV^kPz4T1@e-|KJXI`_XHtnyM;WGypG* zM59Z?L|Yn0SS*2N9=RMahd6w-iJ2Kszw2Qz7znfX@ZfiMcYp67qoR^AtZ~WLck(LP z`A!JJ&hA)hYO2s-dpEb)Mo}#-t&@QaRnUs8sHkY0hMkQKj`!iq+3(*mH3ki*wQpo) zt)NPF);@nmowHvvcXATxx~z3GNJ)*)+}s@dlsk|>*#Fc>B=Qs$6~*My>r}I|v!h8dNVKOYh?|Q(IM^}#J-X1@wh1*LiE}jK36DM=Tz$e%fqlF;P-dih0<^V2COxDkgVdo`STU4fOL%YXof#px7N! z#33)ANCXC>co6-DOa?(9kR%P*SUJtdv*l1zQxi;AcN&|04#d%W%uiI9Glh&MomgiK z4b?BnyU`X~S^zW}ouv1wsfk!qqb>&_JI)p1Ym=S1aI!eXurm6b{xc3AeFFn+f$9_s z|IpAILo0K-mTCTGW@ZlQ^>d3zV$IOqi$OuEJHjs21>0{62nM4v+!YLt*`nxhIA;W_eEfK-6n=pR zBhKM(%I^?8Fe<9CRrns?3v_r?)O4+iG%pBr1}rBh7m8(no(Y;Fh<8gr2$bX;WHlh= zcI{LOrer4yO{1e?h9(qFr+s!opjgU5X=;2so9lJ<0nbZ-(y%a`Gl8?gSW_2WO z=cMu~8S~LjQ`(*!=c%Z#KTmO~o`(M7S5H1Y8FM|0g-8@QqLKRi>$h*lWvsUcYbc`R vhHAFO`j`*wXVuU`b3)og2#n>^($U#@@MHgK$pII}X3_taL8kA2yYRmO@-Ak| literal 15406 zcmeHOTS%2z6#ku=ri~V2rUhCUy zul?_}*Iq}Ge57xsMT;aZO;YxElJv7ANhZ^$``{lX=}&$Z8anqrNRqmKlB8g+!%euw zr{`RJ@nCr{}7`SYslo9iGvJe)p!_&^sfT%aRIj?AMq zYu4E185R~M+PAj0((T)~1wjYkxnaWwyX}ExaBz^$oH?V^J$(2uO-xKsU|^u~%p9}j zb#`_(O-@cyd3iYhqMwiQ$ zE&J%GjJY|up!xg7kAjAW_|BM^Vwily6<>V$_YLFyjr%*v^q-@BR@)9T=42Hz93RM5 z+g9&-jN@0PDZTls`}gms?(S}}?qTh-B&=tfHf=Hl!_lKh>D{|`ip*E9UQv5{JGHd52uzUu z{{4Fj4Gq;@er#+kO;1k?+2iBml#`S5*}9FjG$|=bciFJ9YuBy`+324ZpGyz>39vPGDmw)&*y-Oimm75SF) z&Dy=5b{H8MsmO0=Xb`p!`?sX4SFbAdfLTi)?bUt${JG$L@7_JDe9xXeE9zj5z+bn= zQ_K6x%1Xg|c6OGwZQJ%4?-eUnP)|>fr~@0(!dp#UjKS;IuLXbXBerbWqR7Dhp{lA% zyn{WImX<2-)bLhY=g^@;V*dsi(8K-v_o=?VUhG3;JH*InNhwf%9l9=?Zr_Uxh4r%zK^Ss4`+6i`e|jNx(}#p`Y789tq}#|irn-v7-w zTdpjRnnS5-=NhE)N z|3yoeE=_0Mb+HcrVKRKYwAuqOF}}aK>}I?NeSLiqZkGJ~{C?o)mB6i)r>p8S){l(& zcl2S&k|hUl?g$^~YO1NWoTQ68>t$9?E#@)*0Z)EBTbx!qi~#A)r`ld)UtllQsT zC-mCD*KGqh_r%#HVw;F3AZCR!E-sFuqoc)M+8xga*RNkM_T1O6U#G6FE*c#jrJ0!- z5tG3A^2?VmskgV6nwpvj@xs*9R9d-mrCaRJ_wLeVj84t*eIhj|I?u!n(x0ouHIvr92)8K)aJZYXwx^QVf63b)Ohu_R*Hy-aQe6* z=g1sC>>p==ju5`Hdg@jUFRWSS_!n|rTwLsQ{P11OG2=Vh-`}r_%;!CLAV)xr&h)Ia zapT5$_(w)Y2+!87TW6Q<+_^Kat(IItz50MP)ZA}?&#+_14!e59cekyrO~H?KPtP-9 zE&W5jrEEj+lW}fk&!5286Mq;$bJ3c;M1RlnU8z={$+8unb4^I9u!Z~d=3Hs>9G@A!LdKpz{x`5mACYW^>myXc>>|HXaq z*z;?~-@*73nLO?c8<;uge%6kDFuC2CJTL(pW9&rE T$>PO}zeoOz_vh8XLNxF{TPS{( From bccd5f87bdabfc6c5061777e83c997c7e939342c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 12:33:41 +0100 Subject: [PATCH 09/89] Update wasm-bindgen to 0.2.108, and ehttp to 0.7.1 (#7996) --- .github/workflows/rust.yml | 2 +- Cargo.lock | 224 +++++++++++++++++++++---------------- Cargo.toml | 4 +- deny.toml | 1 + scripts/setup_web.sh | 4 +- 5 files changed, 133 insertions(+), 102 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9018d251b..2122e5b99 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -94,7 +94,7 @@ jobs: - name: wasm-bindgen uses: jetli/wasm-bindgen-action@v0.1.0 with: - version: "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml + version: "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml - run: ./scripts/wasm_bindgen_check.sh --skip-setup diff --git a/Cargo.lock b/Cargo.lock index 896a36665..ec1abe31c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,9 +124,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" @@ -496,9 +496,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" @@ -610,9 +610,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -686,10 +686,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -703,9 +704,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -961,9 +962,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1186,9 +1187,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -1434,9 +1435,9 @@ dependencies = [ [[package]] name = "ehttp" -version = "0.6.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04499d3c719edecfad5c9b46031726c8540905d73be6d7e4f9788c4a298da908" +checksum = "b2f1b93eb2e039aaff63ce07cca59bd1dca02f2ce30075a17b619d2c42f56efc" dependencies = [ "document-features", "js-sys", @@ -1697,10 +1698,16 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.1.4" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1874,13 +1881,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -2130,6 +2137,22 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -2360,9 +2383,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -2427,9 +2450,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -2498,9 +2521,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2571,9 +2594,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" -version = "0.4.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" @@ -2694,7 +2717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -3110,9 +3133,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3365,9 +3388,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" @@ -3507,9 +3530,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3594,9 +3617,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3648,7 +3671,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -3726,7 +3749,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 2.0.17", ] @@ -3824,7 +3847,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3895,9 +3918,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.18" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -3910,15 +3933,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -3927,9 +3953,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" @@ -4087,9 +4113,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" @@ -4138,12 +4164,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "slotmap" @@ -4266,9 +4289,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4672,9 +4695,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-properties" @@ -4734,20 +4757,33 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.10.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64", "flate2", "log", - "once_cell", + "percent-encoding", "rustls", "rustls-pki-types", - "url", + "ureq-proto", + "utf8-zero", "webpki-roots", ] +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.4" @@ -4794,6 +4830,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4861,9 +4903,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -4876,37 +4918,25 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -4915,9 +4945,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4925,22 +4955,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] @@ -5056,9 +5086,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -5092,9 +5122,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.6" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5995,9 +6025,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index d12b040fe..a617e84a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ criterion = { version = "0.7.0", default-features = false } dify = { version = "0.8", default-features = false } directories = "6.0.0" document-features = "0.2.11" -ehttp = { version = "0.6.0", default-features = false } +ehttp = { version = "0.7.1", default-features = false } enum-map = "2.7.3" env_logger = { version = "0.11.8", default-features = false } font-types = { version = "0.11.0", default-features = false, features = ["std"] } @@ -138,7 +138,7 @@ type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } -wasm-bindgen = "0.2.100" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml +wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml wasm-bindgen-futures = "0.4.0" wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" diff --git a/deny.toml b/deny.toml index 486af7745..595641501 100644 --- a/deny.toml +++ b/deny.toml @@ -80,6 +80,7 @@ allow = [ "BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised) "BSL-1.0", # https://tldrlegal.com/license/boost-software-license-1.0-explained "CC0-1.0", # https://creativecommons.org/publicdomain/zero/1.0/ + "CDLA-Permissive-2.0", # https://spdx.org/licenses/CDLA-Permissive-2.0.html "ISC", # https://www.tldrlegal.com/license/isc-license "MIT-0", # https://choosealicense.com/licenses/mit-0/ "MIT", # https://tldrlegal.com/license/mit-license diff --git a/scripts/setup_web.sh b/scripts/setup_web.sh index f51258855..5ba11b665 100755 --- a/scripts/setup_web.sh +++ b/scripts/setup_web.sh @@ -10,6 +10,6 @@ rustup target add wasm32-unknown-unknown # For generating JS bindings: # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml -if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.100'; then - cargo install --force --quiet wasm-bindgen-cli --version 0.2.100 +if ! cargo install --list | grep -q 'wasm-bindgen-cli v0.2.108'; then + cargo install --force --quiet wasm-bindgen-cli --version 0.2.108 --locked fi From f2a47411553c53e8769c8bb3b71c172f70f23b3e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 15:37:45 +0100 Subject: [PATCH 10/89] Remove easymark from default demo app (#7998) I don't want to give the impression `easymark` is either official, useful, or indeed "good" --- crates/egui_demo_app/Cargo.toml | 2 +- crates/egui_demo_app/src/wrap_app.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 7cde46383..b23ea9cbb 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -25,10 +25,10 @@ crate-type = ["cdylib", "rlib"] [features] default = ["wgpu", "persistence"] -# image_viewer adds about 0.9 MB of WASM web_app = ["http", "persistence"] accessibility_inspector = ["dep:accesskit", "dep:accesskit_consumer", "eframe/accesskit"] +easymark = [] # easymark is off by default, because it a pretty shitty markup language http = ["ehttp", "image/jpeg", "poll-promise", "egui_extras/image"] image_viewer = ["image/jpeg", "egui_extras/all_loaders", "rfd"] persistence = ["eframe/persistence", "egui_extras/serde", "egui/persistence", "serde"] diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index c486783cf..313a1a685 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -8,12 +8,14 @@ use core::any::Any; use crate::DemoApp; +#[cfg(feature = "easymark")] #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct EasyMarkApp { editor: egui_demo_lib::easy_mark::EasyMarkEditor, } +#[cfg(feature = "easymark")] impl DemoApp for EasyMarkApp { fn demo_ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { self.editor.panels(ui); @@ -152,12 +154,18 @@ enum Command { #[cfg_attr(feature = "serde", serde(default))] pub struct State { demo: DemoWindows, + + #[cfg(feature = "easymark")] easy_mark_editor: EasyMarkApp, + #[cfg(feature = "http")] http: crate::apps::HttpApp, + #[cfg(feature = "image_viewer")] image_viewer: crate::apps::ImageViewer, + pub clock: FractalClockApp, + rendering_test: ColorTestApp, selected_anchor: Anchor, @@ -212,6 +220,7 @@ impl WrapApp { Anchor::Demo, &mut self.state.demo as &mut dyn DemoApp, ), + #[cfg(feature = "easymark")] ( "šŸ–¹ EasyMark editor", Anchor::EasyMarkEditor, From b077cf910297884a4f1b431e8da99806ae925168 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 15:50:57 +0100 Subject: [PATCH 11/89] Add some example docs to atoms (#7997) --- crates/egui/src/atomics/atom.rs | 16 +++++++++++++++- crates/egui/src/atomics/atoms.rs | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/atomics/atom.rs b/crates/egui/src/atomics/atom.rs index 4db7f12a9..6f289fcfb 100644 --- a/crates/egui/src/atomics/atom.rs +++ b/crates/egui/src/atomics/atom.rs @@ -4,7 +4,21 @@ use epaint::text::TextWrapMode; /// A low-level ui building block. /// -/// Implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. +/// This can be a piece of text, an image, or even a custom widget. +/// It can be decorated with various layout hints, such as `grow`, `shrink`, `align`, and more. +/// +/// `Atom` implements [`From`] for [`String`], [`str`], [`crate::Image`] and much more for convenience. +/// +/// Many widgets take an `impl` [`crate::IntoAtoms`] parameter, +/// which allows you to easily create atoms from tuples of text, images, and other atoms: +/// ``` +/// # use egui::{Vec2, AtomExt, AtomKind, Atom, Image, Id}; +/// # egui::__run_test_ui(|ui| { +/// let image = egui::include_image!("../../../eframe/data/icon.png"); +/// ui.button((image, "Click me!")); +/// # }); +/// ``` +/// /// You can directly call the `atom_*` methods on anything that implements `Into`. /// ``` /// # use egui::{Image, emath::Vec2}; diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 5051a7676..761db8eb6 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -8,6 +8,15 @@ use std::ops::{Deref, DerefMut}; pub(crate) const ATOMS_SMALL_VEC_SIZE: usize = 2; /// A list of [`Atom`]s. +/// +/// Many widgets take an `impl` [`IntoAtoms`] parameter, +/// which allows you to easily create atoms from tuples of text, images, and other atoms: +/// ``` +/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2}; +/// # egui::__run_test_ui(|ui| { +/// let image = egui::include_image!("../../../eframe/data/icon.png"); +/// ui.button((image, "Click me!")); +/// # }); #[derive(Clone, Debug, Default)] pub struct Atoms<'a>(SmallVec<[Atom<'a>; ATOMS_SMALL_VEC_SIZE]>); @@ -192,6 +201,16 @@ where } /// Trait for turning a tuple of [`Atom`]s into [`Atoms`]. +/// +/// Many widgets take an `impl` [`IntoAtoms`] parameter, +/// which allows you to easily create atoms from tuples of text, images, and other atoms: +/// ``` +/// # use egui::{AtomExt, AtomKind, Atom, Image, Id, Vec2}; +/// # egui::__run_test_ui(|ui| { +/// let image = egui::include_image!("../../../eframe/data/icon.png"); +/// ui.button((image, "Click me!")); +/// # }); +/// ``` pub trait IntoAtoms<'a> { fn collect(self, atoms: &mut Atoms<'a>); From a59e803f2567ad12940280e3c50ad22187c10ae6 Mon Sep 17 00:00:00 2001 From: Connor Fitzgerald Date: Mon, 23 Mar 2026 13:21:25 -0400 Subject: [PATCH 12/89] Update to wgpu 29 (#7990) * [x] I have followed the instructions in the PR template This updates wgpu to v29 across the egui crate stack. There a a few API changes due to the requirement to provide a display handle up front to properly support GLES on linux. I have done my best to make the api changes as reasonable as possible, but I don't have all the greater project context, so lmk if things should be done a bit differently. I've also updated glow to 0.17 to make cargo deny happy, there are no source changes. I'm not sure how you want to land these. --------- Co-authored-by: lucasmerlin --- Cargo.lock | 205 ++++++++++-------- Cargo.toml | 4 +- crates/eframe/src/native/wgpu_integration.rs | 10 +- crates/eframe/src/web/mod.rs | 3 +- crates/eframe/src/web/web_painter_wgpu.rs | 36 ++- crates/egui-wgpu/src/lib.rs | 31 ++- crates/egui-wgpu/src/renderer.rs | 15 +- crates/egui-wgpu/src/setup.rs | 197 ++++++++++++++--- crates/egui-wgpu/src/winit.rs | 33 ++- .../egui_demo_app/src/apps/custom3d_wgpu.rs | 2 +- crates/egui_kittest/src/wgpu.rs | 4 +- 11 files changed, 374 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec1abe31c..e9694b07f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -536,7 +536,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", ] [[package]] @@ -545,6 +554,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + [[package]] name = "bitflags" version = "1.3.2" @@ -560,12 +575,6 @@ dependencies = [ "serde", ] -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block2" version = "0.5.1" @@ -805,9 +814,9 @@ dependencies = [ [[package]] name = "codespan-reporting" -version = "0.12.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", @@ -924,7 +933,7 @@ checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", - "core-graphics-types 0.1.3", + "core-graphics-types", "foreign-types", "libc", ] @@ -940,17 +949,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.1", - "libc", -] - [[package]] name = "core_maths" version = "0.1.1" @@ -1659,7 +1657,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" dependencies = [ - "bit-set", + "bit-set 0.8.0", "regex-automata", "regex-syntax", ] @@ -1931,9 +1929,9 @@ dependencies = [ [[package]] name = "glow" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" dependencies = [ "js-sys", "slotmap", @@ -2619,15 +2617,6 @@ version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" version = "2.7.4" @@ -2652,21 +2641,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "metal" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" -dependencies = [ - "bitflags 2.9.4", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] - [[package]] name = "mimalloc" version = "0.1.48" @@ -2731,12 +2705,12 @@ dependencies = [ [[package]] name = "naga" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618f667225063219ddfc61251087db8a9aec3c3f0950c916b614e403486f1135" +checksum = "85b4372fed0bd362d646d01b6926df0e837859ccc522fed720c395e0460f29c8" dependencies = [ "arrayvec", - "bit-set", + "bit-set 0.9.1", "bitflags 2.9.4", "cfg-if", "cfg_aliases", @@ -2841,15 +2815,6 @@ dependencies = [ "syn", ] -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -2888,7 +2853,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-foundation 0.2.2", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", ] [[package]] @@ -2974,7 +2939,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -3054,6 +3019,18 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.9.4", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -3064,7 +3041,20 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", ] [[package]] @@ -3092,7 +3082,7 @@ dependencies = [ "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", - "objc2-quartz-core", + "objc2-quartz-core 0.2.2", "objc2-symbols", "objc2-uniform-type-identifiers", "objc2-user-notifications", @@ -3232,12 +3222,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - [[package]] name = "pathdiff" version = "0.2.3" @@ -3695,6 +3679,18 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + [[package]] name = "rayon" version = "1.11.0" @@ -4243,9 +4239,9 @@ dependencies = [ [[package]] name = "spirv" -version = "0.3.0+sdk-1.3.268.0" +version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ "bitflags 2.9.4", ] @@ -5137,9 +5133,9 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cb534d5ffd109c7d1135f34cdae29e60eab94855a625dcfe1705f8bc7ad79f" +checksum = "78f9f386699b1fb8b8a05bfe82169b24d151f05702d2905a0bf93bc454fcc825" dependencies = [ "arrayvec", "bitflags 2.9.4", @@ -5167,13 +5163,13 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb4c8b5db5f00e56f1f08869d870a0dff7c8bc7ebc01091fec140b0cf0211a9" +checksum = "c7c34181b0acb8f98168f78f8e57ec66f57df5522b39143dbe5f2f45d7ca927c" dependencies = [ "arrayvec", - "bit-set", - "bit-vec", + "bit-set 0.9.1", + "bit-vec 0.9.1", "bitflags 2.9.4", "bytemuck", "cfg_aliases", @@ -5195,61 +5191,61 @@ dependencies = [ "wgpu-core-deps-wasm", "wgpu-core-deps-windows-linux-android", "wgpu-hal", + "wgpu-naga-bridge", "wgpu-types", ] [[package]] name = "wgpu-core-deps-apple" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87b7b696b918f337c486bf93142454080a32a37832ba8a31e4f48221890047da" +checksum = "43acd053312501689cd92a01a9638d37f3e41a5fd9534875efa8917ee2d11ac0" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-emscripten" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b251c331f84feac147de3c4aa3aa45112622a95dd7ee1b74384fa0458dbd79" +checksum = "ef043bf135cc68b6f667c55ff4e345ce2b5924d75bad36a47921b0287ca4b24a" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-wasm" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a2cf578ce8d7d50d0e63ddc2345c7dcb599f6eb90b888813406ea78b9b7010" +checksum = "2f7b75e72f49035f000dd5262e4126242e92a090a4fd75931ecfe7e60784e6fa" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-core-deps-windows-linux-android" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ca976e72b2c9964eb243e281f6ce7f14a514e409920920dcda12ae40febaae" +checksum = "725d5c006a8c02967b6d93ef04f6537ec4593313e330cfe86d9d3f946eb90f28" dependencies = [ "wgpu-hal", ] [[package]] name = "wgpu-hal" -version = "28.0.0" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293080d77fdd14d6b08a67c5487dfddbf874534bb7921526db56a7b75d7e3bef" +checksum = "058b6047337cf323a4f092486443a9337f3d81325347e5d77deed7e563aeaedc" dependencies = [ "android_system_properties", "arrayvec", "ash", - "bit-set", + "bit-set 0.9.1", "bitflags 2.9.4", - "block", + "block2 0.6.2", "bytemuck", "cfg-if", "cfg_aliases", - "core-graphics-types 0.2.0", "glow", "glutin_wgl_sys", "gpu-allocator", @@ -5260,10 +5256,13 @@ dependencies = [ "libc", "libloading", "log", - "metal", "naga", "ndk-sys", - "objc", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", "once_cell", "ordered-float", "parking_lot", @@ -5272,26 +5271,40 @@ dependencies = [ "profiling", "range-alloc", "raw-window-handle", + "raw-window-metal", "renderdoc-sys", "smallvec", "thiserror 2.0.17", "wasm-bindgen", + "wayland-sys", "web-sys", + "wgpu-naga-bridge", "wgpu-types", "windows", "windows-core 0.62.2", ] [[package]] -name = "wgpu-types" -version = "28.0.0" +name = "wgpu-naga-bridge" +version = "29.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e18308757e594ed2cd27dddbb16a139c42a683819d32a2e0b1b0167552f5840c" +checksum = "d0b8e1e505095f24cb4a578f04b1421d456257dca7fac114d9d9dd3d978c34b8" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d15ece45db77dd5451f11c0ce898334317ce8502d304a20454b531fdc0652fae" dependencies = [ "bitflags 2.9.4", "bytemuck", "js-sys", "log", + "raw-window-handle", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index a617e84a5..b084399a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ ehttp = { version = "0.7.1", default-features = false } enum-map = "2.7.3" env_logger = { version = "0.11.8", default-features = false } font-types = { version = "0.11.0", default-features = false, features = ["std"] } -glow = "0.16.0" +glow = "0.17.0" glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" @@ -144,7 +144,7 @@ wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" web-time = "1.1.0" # Timekeeping for native and web webbrowser = "1.0.5" -wgpu = { version = "28.0.0", default-features = false, features = ["std"] } +wgpu = { version = "29.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 9d6283808..ea96a1845 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -184,9 +184,17 @@ impl<'app> WgpuWinitApp<'app> { builder: ViewportBuilder, ) -> crate::Result<&mut WgpuWinitRunning<'app>> { profiling::function_scope!(); + // Inject the display handle into the wgpu setup so that wgpu can create + // surfaces on platforms that require it (e.g. GLES on Wayland). + let mut wgpu_options = self.native_options.wgpu_options.clone(); + if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup + && create_new.display_handle.is_none() + { + create_new.display_handle = Some(Box::new(event_loop.owned_display_handle())); + } let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new( egui_ctx.clone(), - self.native_options.wgpu_options.clone(), + wgpu_options, self.native_options.viewport.transparent.unwrap_or(false), egui_wgpu::RendererOptions { msaa_samples: self.native_options.multisampling as _, diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 1e54d7a84..87771f722 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -38,6 +38,7 @@ mod web_painter_wgpu; pub use backend::*; use egui::Theme; +use js_sys::Object; use wasm_bindgen::prelude::*; use web_sys::{Document, MediaQueryList, Node}; @@ -370,5 +371,5 @@ pub fn percent_decode(s: &str) -> String { /// Are we running inside the Safari browser? pub fn is_safari_browser() -> bool { - web_sys::window().is_some_and(|window| window.has_own_property(&JsValue::from("safari"))) + web_sys::window().is_some_and(|window| Object::has_own(&window, &JsValue::from("safari"))) } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index f7adb8fbb..ebce9d981 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -15,13 +15,14 @@ pub(crate) struct WebPainterWgpu { surface: wgpu::Surface<'static>, surface_configuration: wgpu::SurfaceConfiguration, render_state: Option, - on_surface_error: Arc SurfaceErrorAction>, + on_surface_status: Arc SurfaceErrorAction>, depth_stencil_format: Option, depth_texture_view: Option, screen_capture_state: Option, capture_tx: CaptureSender, capture_rx: CaptureReceiver, ctx: egui::Context, + needs_reconfigure: bool, } impl WebPainterWgpu { @@ -105,11 +106,12 @@ impl WebPainterWgpu { surface_configuration, depth_stencil_format, depth_texture_view: None, - on_surface_error: Arc::clone(&options.wgpu_options.on_surface_error) as _, + on_surface_status: Arc::clone(&options.wgpu_options.on_surface_status) as _, screen_capture_state: None, capture_tx, capture_rx, ctx, + needs_reconfigure: false, }) } } @@ -195,18 +197,28 @@ impl WebPainter for WebPainterWgpu { ); } + if self.needs_reconfigure { + self.surface + .configure(&render_state.device, &self.surface_configuration); + self.needs_reconfigure = false; + } + let output_frame = match self.surface.get_current_texture() { - Ok(frame) => frame, - Err(err) => match (*self.on_surface_error)(err) { - SurfaceErrorAction::RecreateSurface => { - self.surface - .configure(&render_state.device, &self.surface_configuration); - return Ok(()); + wgpu::CurrentSurfaceTexture::Success(frame) => frame, + wgpu::CurrentSurfaceTexture::Suboptimal(frame) => { + self.needs_reconfigure = true; + frame + } + other => { + match (*self.on_surface_status)(&other) { + SurfaceErrorAction::RecreateSurface => { + self.surface + .configure(&render_state.device, &self.surface_configuration); + } + SurfaceErrorAction::SkipFrame => {} } - SurfaceErrorAction::SkipFrame => { - return Ok(()); - } - }, + return Ok(()); + } }; { diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 46becf8f7..05936d6cd 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -24,7 +24,10 @@ mod renderer; mod setup; pub use renderer::*; -pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting}; +pub use setup::{ + EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, + WgpuSetupExisting, +}; /// Helpers for capturing screenshots of the UI. #[cfg(feature = "capture")] @@ -191,6 +194,7 @@ impl RenderState { let (adapter, device, queue) = match config.wgpu_setup.clone() { WgpuSetup::CreateNew(WgpuSetupCreateNew { instance_descriptor: _, + display_handle: _, power_preference, native_adapter_selector: _native_adapter_selector, device_descriptor, @@ -272,7 +276,7 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String { } } -/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`] +/// Specifies which action should be taken as consequence of a surface error. pub enum SurfaceErrorAction { /// Do nothing and skip the current frame. SkipFrame, @@ -299,8 +303,15 @@ pub struct WgpuConfiguration { /// How to create the wgpu adapter & device pub wgpu_setup: WgpuSetup, - /// Callback for surface errors. - pub on_surface_error: Arc SurfaceErrorAction + Send + Sync>, + /// Callback for surface status changes. + /// + /// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame + /// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For + /// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and + /// defers surface reconfiguration to the next frame — the callback is not invoked + /// in that case either. + pub on_surface_status: + Arc SurfaceErrorAction + Send + Sync>, } #[test] @@ -315,7 +326,7 @@ impl std::fmt::Debug for WgpuConfiguration { present_mode, desired_maximum_frame_latency, wgpu_setup, - on_surface_error: _, + on_surface_status: _, } = self; f.debug_struct("WgpuConfiguration") .field("present_mode", &present_mode) @@ -333,14 +344,16 @@ impl Default for WgpuConfiguration { Self { present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: None, - wgpu_setup: Default::default(), - on_surface_error: Arc::new(|err| { - if err == wgpu::SurfaceError::Outdated { + // No display handle available at this point — callers should replace this with + // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available. + wgpu_setup: WgpuSetup::without_display_handle(), + on_surface_status: Arc::new(|status| { + if matches!(status, wgpu::CurrentSurfaceTexture::Outdated) { // This error occurs when the app is minimized on Windows. // Silently return here to prevent spamming the console with: // "The underlying surface has changed, and therefore the swap chain must be updated" } else { - log::warn!("Dropped frame with error: {err}"); + log::warn!("Dropped frame with error: {status:?}"); } SurfaceErrorAction::SkipFrame }), diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index c37802448..3222a5521 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -352,7 +352,10 @@ impl Renderer { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("egui_pipeline_layout"), - bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout], + bind_group_layouts: &[ + Some(&uniform_bind_group_layout), + Some(&texture_bind_group_layout), + ], immediate_size: 0, }); @@ -360,8 +363,8 @@ impl Renderer { .depth_stencil_format .map(|format| wgpu::DepthStencilState { format, - depth_write_enabled: false, - depth_compare: wgpu::CompareFunction::Always, + depth_write_enabled: Some(false), + depth_compare: Some(wgpu::CompareFunction::Always), stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }); @@ -968,7 +971,8 @@ impl Renderer { Primitive::Mesh(mesh) => { let size = mesh.indices.len() * std::mem::size_of::(); let slice = index_offset..(size + index_offset); - index_buffer_staging[slice.clone()] + index_buffer_staging + .slice(slice.clone()) .copy_from_slice(bytemuck::cast_slice(&mesh.indices)); self.index_buffer.slices.push(slice); index_offset += size; @@ -1011,7 +1015,8 @@ impl Renderer { Primitive::Mesh(mesh) => { let size = mesh.vertices.len() * std::mem::size_of::(); let slice = vertex_offset..(size + vertex_offset); - vertex_buffer_staging[slice.clone()] + vertex_buffer_staging + .slice(slice.clone()) .copy_from_slice(bytemuck::cast_slice(&mesh.vertices)); self.vertex_buffer.slices.push(slice); vertex_offset += size; diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 0c3cb8c39..9d83d4380 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -1,5 +1,48 @@ use std::sync::Arc; +/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`]. +/// +/// This trait exists so that a [`winit::event_loop::OwnedDisplayHandle`] (or similar platform +/// display handle) can be stored, cloned, and later passed to wgpu. +/// +/// wgpu requires an explicit display handle for GLES on some platforms (notably Wayland). +/// Because [`wgpu::InstanceDescriptor`] contains a `Box` which is +/// not cloneable, we wrap the handle in this trait so it can be cloned alongside the rest of +/// the egui wgpu configuration. +/// +/// This is automatically implemented for all types that satisfy the bounds (including +/// [`winit::event_loop::OwnedDisplayHandle`]). +pub trait EguiDisplayHandle: + wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static +{ + /// Clone this handle into a `Box` suitable for setting on + /// [`wgpu::InstanceDescriptor::display`]. + fn clone_for_wgpu(&self) -> Box; + + /// Clone this handle into a new `Box`. + fn clone_display_handle(&self) -> Box; +} + +impl Clone for Box { + fn clone(&self) -> Self { + // We need to deref here, otherwise this causes infinite recursion stack overflow. + (**self).clone_display_handle() + } +} + +impl EguiDisplayHandle for T +where + T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static, +{ + fn clone_for_wgpu(&self) -> Box { + Box::new(self.clone()) + } + + fn clone_display_handle(&self) -> Box { + Box::new(self.clone()) + } +} + #[derive(Clone)] pub enum WgpuSetup { /// Construct a wgpu setup using some predefined settings & heuristics. @@ -22,9 +65,32 @@ pub enum WgpuSetup { Existing(WgpuSetupExisting), } -impl Default for WgpuSetup { - fn default() -> Self { - Self::CreateNew(WgpuSetupCreateNew::default()) +impl WgpuSetup { + /// Creates a new [`WgpuSetup::CreateNew`] with the given display handle. + /// + /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web) + /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require + /// one. Providing it unconditionally ensures your app works everywhere. + /// + /// If you don't have a display handle available, use [`Self::without_display_handle`] + /// instead — it will still work on the majority of platforms. + /// + /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self { + Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle)) + } + + /// Creates a new [`WgpuSetup::CreateNew`] without a display handle. + /// + /// A display handle is not required for headless operation (offscreen rendering, tests, + /// compute-only workloads). It also isn't needed on most platforms even when presenting + /// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one. + /// + /// If you do have a display handle available, prefer [`Self::from_display_handle`] for + /// maximum compatibility. With winit you can obtain one via + /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn without_display_handle() -> Self { + Self::CreateNew(WgpuSetupCreateNew::without_display_handle()) } } @@ -65,8 +131,18 @@ impl WgpuSetup { } log::debug!("Creating wgpu instance with backends {backends:?}"); - wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor) - .await + let desc = &create_new.instance_descriptor; + let descriptor = wgpu::InstanceDescriptor { + backends: desc.backends, + flags: desc.flags, + backend_options: desc.backend_options.clone(), + memory_budget_thresholds: desc.memory_budget_thresholds, + display: create_new + .display_handle + .as_ref() + .map(|handle| handle.clone_for_wgpu()), + }; + wgpu::util::new_instance_with_webgpu_detection(descriptor).await } Self::Existing(existing) => existing.instance.clone(), } @@ -98,9 +174,28 @@ pub type NativeAdapterSelectorMethod = Arc< /// Configuration for creating a new wgpu setup. /// /// Used for [`WgpuSetup::CreateNew`]. +/// +/// Use [`Self::from_display_handle`] when you have a display handle available — this is the +/// recommended constructor. With winit you can obtain one via +/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). +/// Most platforms (Windows, macOS/iOS, Android, web) work fine without one, but some +/// (e.g. Wayland on Linux with GLES) require it. Providing it unconditionally ensures your +/// app works everywhere. +/// +/// If you don't have a display handle, use [`Self::without_display_handle`] — it will still +/// work on the majority of platforms, and is appropriate for headless rendering, tests, or +/// web targets. +/// +/// Note: The [`wgpu::InstanceDescriptor::display`] field is always stored as `None` in +/// [`Self::instance_descriptor`]. The display handle is stored separately so it can be cloned +/// (since [`wgpu::InstanceDescriptor`] itself does not implement `Clone`), and is injected +/// into the descriptor at instance creation time. pub struct WgpuSetupCreateNew { /// Instance descriptor for creating a wgpu instance. /// + /// The [`wgpu::InstanceDescriptor::display`] field should be left as `None`; use the + /// [`Self::display_handle`] field instead (it will be injected when the instance is created). + /// /// The most important field is [`wgpu::InstanceDescriptor::backends`], which /// controls which backends are supported (wgpu will pick one of these). /// If you only want to support WebGL (and not WebGPU), @@ -110,6 +205,16 @@ pub struct WgpuSetupCreateNew { /// and only if you have enabled the `webgl` feature of crate `wgpu`. pub instance_descriptor: wgpu::InstanceDescriptor, + /// The display handle to pass to wgpu when creating the instance. + /// + /// Most platforms (Windows, macOS/iOS, Android, web) work without this, but some + /// (e.g. Wayland on Linux with GLES) require it. If you have a display handle + /// available, providing it ensures maximum compatibility. + /// + /// When using winit, this is typically the + /// [`winit::event_loop::OwnedDisplayHandle`] obtained from the event loop. + pub display_handle: Option>, + /// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web. pub power_preference: wgpu::PowerPreference, @@ -128,32 +233,34 @@ pub struct WgpuSetupCreateNew { Arc wgpu::DeviceDescriptor<'static> + Send + Sync>, } -impl Clone for WgpuSetupCreateNew { - fn clone(&self) -> Self { +impl WgpuSetupCreateNew { + /// Creates a new configuration with the given display handle. + /// + /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web) + /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require + /// one. Providing it unconditionally ensures your app works everywhere. + /// + /// If you don't have a display handle available, use [`Self::without_display_handle`] + /// instead — it will still work on the majority of platforms. + /// + /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self { Self { - instance_descriptor: self.instance_descriptor.clone(), - power_preference: self.power_preference, - native_adapter_selector: self.native_adapter_selector.clone(), - device_descriptor: Arc::clone(&self.device_descriptor), + display_handle: Some(Box::new(display_handle)), + ..Self::without_display_handle() } } -} -impl std::fmt::Debug for WgpuSetupCreateNew { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WgpuSetupCreateNew") - .field("instance_descriptor", &self.instance_descriptor) - .field("power_preference", &self.power_preference) - .field( - "native_adapter_selector", - &self.native_adapter_selector.is_some(), - ) - .finish() - } -} - -impl Default for WgpuSetupCreateNew { - fn default() -> Self { + /// Creates a new configuration without a display handle. + /// + /// A display handle is not required for headless operation (offscreen rendering, tests, + /// compute-only workloads). It also isn't needed on most platforms even when presenting + /// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one. + /// + /// If you do have a display handle available, prefer [`Self::from_display_handle`] for + /// maximum compatibility. With winit you can obtain one via + /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + pub fn without_display_handle() -> Self { Self { instance_descriptor: wgpu::InstanceDescriptor { // Add GL backend, primarily because WebGPU is not stable enough yet. @@ -163,8 +270,11 @@ impl Default for WgpuSetupCreateNew { flags: wgpu::InstanceFlags::from_build_config().with_env(), backend_options: wgpu::BackendOptions::from_env_or_default(), memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(), + display: None, }, + display_handle: None, + power_preference: wgpu::PowerPreference::from_env() .unwrap_or(wgpu::PowerPreference::HighPerformance), @@ -192,6 +302,39 @@ impl Default for WgpuSetupCreateNew { } } +impl Clone for WgpuSetupCreateNew { + fn clone(&self) -> Self { + let desc = &self.instance_descriptor; + Self { + instance_descriptor: wgpu::InstanceDescriptor { + backends: desc.backends, + flags: desc.flags, + backend_options: desc.backend_options.clone(), + memory_budget_thresholds: desc.memory_budget_thresholds, + display: None, + }, + display_handle: self.display_handle.clone(), + power_preference: self.power_preference, + native_adapter_selector: self.native_adapter_selector.clone(), + device_descriptor: Arc::clone(&self.device_descriptor), + } + } +} + +impl std::fmt::Debug for WgpuSetupCreateNew { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WgpuSetupCreateNew") + .field("instance_descriptor", &self.instance_descriptor) + .field("display_handle", &self.display_handle) + .field("power_preference", &self.power_preference) + .field( + "native_adapter_selector", + &self.native_adapter_selector.is_some(), + ) + .finish() + } +} + /// Configuration for using an existing wgpu setup. /// /// Used for [`WgpuSetup::Existing`]. diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 5fb8d123a..3f6adfc27 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -17,6 +17,7 @@ struct SurfaceState { width: u32, height: u32, resizing: bool, + needs_reconfigure: bool, } /// Everything you need to paint egui with [`wgpu`] on [`winit`]. @@ -234,6 +235,7 @@ impl Painter { height: size.height, alpha_mode, resizing: false, + needs_reconfigure: false, }, ); let Some(width) = NonZeroU32::new(size.width) else { @@ -368,7 +370,7 @@ impl Painter { hal_surface .render_layer() .lock() - .set_presents_with_transaction(resizing); + .setPresentsWithTransaction(resizing); Self::configure_surface( state, @@ -454,7 +456,7 @@ impl Painter { commands_submitted: false, }; - let Some(surface_state) = self.surfaces.get(&viewport_id) else { + let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else { return vsync_sec; }; @@ -491,6 +493,11 @@ impl Painter { ) }; + if surface_state.needs_reconfigure { + Self::configure_surface(surface_state, render_state, &self.configuration); + surface_state.needs_reconfigure = false; + } + let output_frame = { profiling::scope!("get_current_texture"); // This is what vsync-waiting happens on my Mac. @@ -501,16 +508,20 @@ impl Painter { }; let output_frame = match output_frame { - Ok(frame) => frame, - Err(err) => match (*self.configuration.on_surface_error)(err) { - SurfaceErrorAction::RecreateSurface => { - Self::configure_surface(surface_state, render_state, &self.configuration); - return vsync_sec; + wgpu::CurrentSurfaceTexture::Success(frame) => frame, + wgpu::CurrentSurfaceTexture::Suboptimal(frame) => { + surface_state.needs_reconfigure = true; + frame + } + other => { + match (*self.configuration.on_surface_status)(&other) { + SurfaceErrorAction::RecreateSurface => { + Self::configure_surface(surface_state, render_state, &self.configuration); + } + SurfaceErrorAction::SkipFrame => {} } - SurfaceErrorAction::SkipFrame => { - return vsync_sec; - } - }, + return vsync_sec; + } }; let mut capture_buffer = None; diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index fd1d9ae73..d83f000a4 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -40,7 +40,7 @@ impl Custom3d { let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("custom3d"), - bind_group_layouts: &[&bind_group_layout], + bind_group_layouts: &[Some(&bind_group_layout)], immediate_size: 0, }); diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 3f97e0036..a9f0de9ad 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -17,7 +17,8 @@ pub(crate) const WAIT_TIMEOUT: Duration = Duration::from_secs(10); /// Default wgpu setup used for the wgpu renderer. pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { - let mut setup = egui_wgpu::WgpuSetupCreateNew::default(); + // No display handle needed for headless testing — we don't present to a window. + let mut setup = egui_wgpu::WgpuSetupCreateNew::without_display_handle(); // WebGPU not supported yet since we rely on blocking screenshots. setup @@ -58,6 +59,7 @@ pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { } pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { + // No display handle needed for headless testing — we don't present to a window. let instance = pollster::block_on(setup.new_instance()); pollster::block_on(egui_wgpu::RenderState::create( From 0d2f6cf4e686462a307eddd2043948039d8437b3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 18:53:32 +0100 Subject: [PATCH 13/89] Reduce warning level on occluded error --- crates/egui-wgpu/src/lib.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 05936d6cd..eb04173b5 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -348,13 +348,21 @@ impl Default for WgpuConfiguration { // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available. wgpu_setup: WgpuSetup::without_display_handle(), on_surface_status: Arc::new(|status| { - if matches!(status, wgpu::CurrentSurfaceTexture::Outdated) { - // This error occurs when the app is minimized on Windows. - // Silently return here to prevent spamming the console with: - // "The underlying surface has changed, and therefore the swap chain must be updated" - } else { - log::warn!("Dropped frame with error: {status:?}"); + match status { + wgpu::CurrentSurfaceTexture::Outdated => { + // This error occurs when the app is minimized on Windows. + // Silently return here to prevent spamming the console with: + // "The underlying surface has changed, and therefore the swap chain must be updated" + } + wgpu::CurrentSurfaceTexture::Occluded => { + // This error occurs when the application is occluded (e.g. minimized or behind another window). + log::debug!("Dropped frame with error: {status:?}"); + } + _ => { + log::warn!("Dropped frame with error: {status:?}"); + } } + SurfaceErrorAction::SkipFrame }), } From fbe5763a91415a6e4277330718bc67c4c1f853b0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 23 Mar 2026 18:53:45 +0100 Subject: [PATCH 14/89] Remove fixed RUSTSEC advisory from deny.toml --- deny.toml | 1 - scripts/check.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/deny.toml b/deny.toml index 595641501..2d3231535 100644 --- a/deny.toml +++ b/deny.toml @@ -34,7 +34,6 @@ ignore = [ "RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect "RUSTSEC-2024-0436", # unmaintained paste pulled via metal/wgpu, see https://github.com/gfx-rs/metal-rs/issues/349 "RUSTSEC-2025-0141", # https://rustsec.org/advisories/RUSTSEC-2025-0141 - bincode is unmaintained - https://git.sr.ht/~stygianentity/bincode/tree/v3.0/item/README.md - "RUSTSEC-2026-0049", # https://rustsec.org/advisories/RUSTSEC-2026-0049 ] [bans] diff --git a/scripts/check.sh b/scripts/check.sh index 71dc38386..ce0b3b8d5 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -29,7 +29,6 @@ cargo check --quiet --all-targets cargo check --quiet --all-targets --all-features cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown cargo check --quiet -p egui_demo_app --lib --target wasm32-unknown-unknown --all-features -# TODO(#5297) re-enable --all-features once the tests work with the unity feature cargo test --quiet --all-targets --all-features cargo test --quiet --doc # slow - checks all doc-tests From 91effb9e57a911befd2552cc94f505902053c895 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 11:02:44 +0100 Subject: [PATCH 15/89] Update kittest to 0.4.0 (#8002) --- Cargo.lock | 5 +++-- Cargo.toml | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e9694b07f..152d178b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2483,8 +2483,9 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" -version = "0.3.0" -source = "git+https://github.com/rerun-io/kittest?branch=main#ce7a2f3b12c36021889b50bdff671cec8016b0fb" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ceaa75eb0036a32b6b9833962eb18137449e9817e2e586006471925b727fd5" dependencies = [ "accesskit", "accesskit_consumer", diff --git a/Cargo.toml b/Cargo.toml index b084399a6..d042c2349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25.6", default-features = false } js-sys = "0.3.77" -kittest = { version = "0.3.0" } +kittest = { version = "0.4.0" } log = { version = "0.4.28", features = ["std"] } memoffset = "0.9.1" mimalloc = "0.1.48" @@ -148,9 +148,6 @@ wgpu = { version = "29.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.12", default-features = false } -[patch.crates-io] -kittest = { git = "https://github.com/rerun-io/kittest", branch = "main" } - [workspace.lints.rust] unsafe_code = "deny" From c0ea6117e0526c86c8d113f316cf217d6bc04f86 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 11:04:36 +0100 Subject: [PATCH 16/89] Use `AtomLayoutResponse` in `TextEditOutput` (#8003) This is necessary to add e.g. buttons as prefix/suffix within the textedit (so you can read the rect and place the button). I'm not sure if deref on the AtomLayoutResponse is the right call but it should make the migration easier and is generally convenient (but might lead to some confusion). --- crates/egui/src/atomics/atom_layout.rs | 14 ++++++++++++++ crates/egui/src/widgets/text_edit/builder.rs | 7 +++---- crates/egui/src/widgets/text_edit/output.rs | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index da7f672a1..c408146d6 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -520,6 +520,20 @@ impl AtomLayoutResponse { } } +impl Deref for AtomLayoutResponse { + type Target = Response; + + fn deref(&self) -> &Self::Target { + &self.response + } +} + +impl DerefMut for AtomLayoutResponse { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.response + } +} + impl Widget for AtomLayout<'_> { fn ui(self, ui: &mut Ui) -> Response { self.show(ui).response diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 50154102a..086cf3091 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -409,7 +409,7 @@ impl<'t> TextEdit<'t> { impl Widget for TextEdit<'_> { fn ui(self, ui: &mut Ui) -> Response { - self.show(ui).response + self.show(ui).response.response } } @@ -563,7 +563,7 @@ impl TextEdit<'_> { // so we can clip it to the available size. Thus, extract it from the atom closure here. let mut get_galley = None; let inner_rect_id = Id::new("text_edit_rect"); - let atom_response = { + let mut response = { let any_shrink = hint_text.any_shrink(); // Ideally we could just do `let mut atoms = prefix` here, but that won't compile // but due to servo/rust-smallvec#146 (also see the comment below). @@ -716,8 +716,7 @@ impl TextEdit<'_> { allocated.paint(ui) }; - let inner_rect = atom_response.rect(inner_rect_id).unwrap_or(Rect::ZERO); - let mut response = atom_response.response; + let inner_rect = response.rect(inner_rect_id).unwrap_or(Rect::ZERO); // Our atom closure was now called, so the galley should always be available here let mut galley = get_galley.expect("Galley should be available here"); diff --git a/crates/egui/src/widgets/text_edit/output.rs b/crates/egui/src/widgets/text_edit/output.rs index 8149bbe58..3339f325e 100644 --- a/crates/egui/src/widgets/text_edit/output.rs +++ b/crates/egui/src/widgets/text_edit/output.rs @@ -5,7 +5,7 @@ use crate::text::CCursorRange; /// The output from a [`TextEdit`](crate::TextEdit). pub struct TextEditOutput { /// The interaction response. - pub response: crate::Response, + pub response: crate::AtomLayoutResponse, /// How the text was displayed. pub galley: Arc, From 7fbd1315ecd1782813c1e3f44bf25fd31c464c1f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 11:28:49 +0100 Subject: [PATCH 17/89] Include LICENSE files in published crates (#8004) * Closes https://github.com/emilk/egui/issues/7977 --- crates/ecolor/Cargo.toml | 2 +- crates/eframe/Cargo.toml | 2 +- crates/egui-wgpu/Cargo.toml | 2 +- crates/egui-winit/Cargo.toml | 2 +- crates/egui/Cargo.toml | 2 +- crates/egui_demo_lib/Cargo.toml | 2 +- crates/egui_extras/Cargo.toml | 2 +- crates/egui_glow/Cargo.toml | 2 +- crates/egui_kittest/Cargo.toml | 2 +- crates/emath/Cargo.toml | 2 +- crates/epaint/Cargo.toml | 2 +- crates/epaint_default_fonts/Cargo.toml | 4 ++-- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index f37aff109..43543113a 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui" categories = ["mathematics", "encoding"] keywords = ["gui", "color", "conversion", "gamedev", "images"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 86f63c50e..376f2ecbb 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/eframe" categories = ["gui", "game-development"] keywords = ["egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/icon.png"] [package.metadata.docs.rs] all-features = true diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index c514e0a49..86cd3192b 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui-wgpu" categories = ["gui", "game-development"] keywords = ["wgpu", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "**/*.wgsl", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index d1b2ab220..bb3576a2d 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui-winit" categories = ["gui", "game-development"] keywords = ["winit", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 764d2401e..3a22bf529 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -11,7 +11,7 @@ readme = "../../README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "game-development"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 3e61187d5..4f3b853e0 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui_demo_lib" categories = ["gui", "graphics"] keywords = ["glow", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "data/*"] [lints] workspace = true diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index 6639126ef..b124148bc 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -15,7 +15,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "game-development"] keywords = ["gui", "imgui", "immediate", "portable", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index 4a083611b..f714dd8e0 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui_glow" categories = ["gui", "game-development"] keywords = ["glow", "egui", "gui", "gamedev"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"] [lints] workspace = true diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index f922b807e..10938cd37 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -11,7 +11,7 @@ readme = "./README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "development-tools::testing", "accessibility"] keywords = ["gui", "immediate", "egui", "testing", "accesskit"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/emath/Cargo.toml b/crates/emath/Cargo.toml index 416d32750..9895fffc6 100644 --- a/crates/emath/Cargo.toml +++ b/crates/emath/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/emath" categories = ["mathematics", "gui"] keywords = ["math", "gui"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index c8a05e4d7..9d6e3eade 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -11,7 +11,7 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/epaint" categories = ["graphics", "gui"] keywords = ["graphics", "gui", "egui"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] [lints] workspace = true diff --git a/crates/epaint_default_fonts/Cargo.toml b/crates/epaint_default_fonts/Cargo.toml index 588719871..43f6fedfe 100644 --- a/crates/epaint_default_fonts/Cargo.toml +++ b/crates/epaint_default_fonts/Cargo.toml @@ -12,8 +12,8 @@ repository = "https://github.com/emilk/egui/tree/main/crates/epaint_default_font categories = ["graphics", "gui"] keywords = ["graphics", "gui", "egui"] include = [ - "../LICENSE-APACHE", - "../LICENSE-MIT", + "../../LICENSE-APACHE", + "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "fonts/*.ttf", From 2a03ae1348ad7473452672d37f1a74bcf3bda3c9 Mon Sep 17 00:00:00 2001 From: RndUsr123 <150948884+RndUsr123@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:35:37 +0000 Subject: [PATCH 18/89] Enables every combination of `TextEdit` and `LayoutJob` alignments (#7831) This is a fix/improvement that makes all kinds of alignments work in `TextEdit` when a custom `LayoutJob` with halign is used. I used the simplest approach possible to avoid unwanted bugs as I wasn't sure what's safe to change, but there's potentially better ways to achieve this. In particular, I'm not sure I fully understand the rationale behind aligning rows in a `Galley` based on the latter's leftmost border, considering the size is what's ultimately used in widgets (in `TextEdit` at least). Regardless, here's a demo of this PR: https://github.com/user-attachments/assets/5d9801d7-73af-4576-80c5-47f169700462 * [x] I have followed the instructions in the PR template --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/text_edit/builder.rs | 6 ++- crates/epaint/src/text/text_layout_types.rs | 4 +- tests/egui_tests/tests/regression_tests.rs | 54 ++++++++++++++++++- .../tests/snapshots/text_edit_halign.png | 3 ++ 4 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/text_edit_halign.png diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 086cf3091..a6c41e71c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -820,7 +820,11 @@ impl TextEdit<'_> { paint_text_selection(&mut galley, ui.visuals(), &cursor_range, None); } - painter.galley(galley_pos, Arc::clone(&galley), text_color); + painter.galley( + galley_pos - vec2(galley.rect.left(), 0.0), + Arc::clone(&galley), + text_color, + ); if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index d887fb13a..22fb03c57 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1047,7 +1047,7 @@ impl Galley { return self.end_pos(); }; - let x = row.x_offset(layout_cursor.column); + let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left(); Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } @@ -1092,7 +1092,7 @@ impl Galley { if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. - let column = row.char_at(pos.x - row.pos.x); + let column = row.char_at(pos.x - row.pos.x + self.rect.left()); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = CCursor { index: ccursor_index + column, diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index d61b76af3..d92a77103 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,8 +1,13 @@ +use std::sync::Arc; + +use egui::ScrollArea; use egui::accesskit::Role; use egui::epaint::Shape; use egui::style::ScrollAnimation; +use egui::text::{LayoutJob, TextWrapping}; use egui::{ - Align, Color32, Image, Label, Layout, RichText, ScrollArea, Sense, TextWrapMode, include_image, + Align, Color32, FontFamily, FontId, Image, Label, Layout, RichText, Sense, TextBuffer, + TextFormat, TextWrapMode, Ui, include_image, vec2, }; use egui_kittest::Harness; use egui_kittest::kittest::Queryable as _; @@ -66,6 +71,53 @@ fn text_edit_rtl() { } } +#[test] +fn text_edit_halign() { + let mut harness = Harness::builder().with_size((212.0, 212.0)).build_ui(|ui| { + ui.spacing_mut().item_spacing = vec2(2.0, 2.0); + + fn layouter(halign: Align) -> impl FnMut(&Ui, &dyn TextBuffer, f32) -> Arc { + move |ui: &egui::Ui, buf: &dyn egui::TextBuffer, wrap_width: f32| { + let mut job = LayoutJob { + wrap: TextWrapping { + max_rows: 4, + max_width: wrap_width, + ..Default::default() + }, + halign, + ..Default::default() + }; + job.append( + buf.as_str(), + 0.0, + TextFormat::simple(FontId::new(13.0, FontFamily::Proportional), Color32::GRAY), + ); + ui.fonts_mut(|f| f.layout_job(job)) + } + } + + for widget_alignment in [Align::Min, Align::Center, Align::Max] { + ui.horizontal(|ui| { + for text_alignment in [Align::LEFT, Align::Center, Align::RIGHT] { + ui.add_sized( + vec2(64.0, 64.0), + egui::TextEdit::multiline(&mut format!( + "{widget_alignment:?}\n+\n{text_alignment:?}", + )) + .layouter(&mut layouter(text_alignment)) + .vertical_align(widget_alignment) + .horizontal_align(widget_alignment), + ); + } + }); + } + }); + + harness.get_by_value("Center\n+\nCenter").focus(); + harness.step(); + harness.snapshot("text_edit_halign"); +} + #[test] fn text_edit_delay() { let mut text = String::new(); 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..29546a036 --- /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:502607c803b884e4e1640d39c97b03b0a40df93c2da328f889168e386f837f36 +size 13261 From cc7cfd27cabc62233a7b7dbb452f39f544093abe Mon Sep 17 00:00:00 2001 From: ilya sheprut Date: Tue, 24 Mar 2026 13:37:37 +0300 Subject: [PATCH 19/89] Fix `horizontal_wrapping` row height after using `text_edit_multiline` (#8000) * [x] I have followed the instructions in the PR template This PR have two commits: * **First commit** - introduction of tests and their canonization image. Expected behaviour is that `horizontal_wrapped_multiline_row_height` would match `horizontal_wrapped_multiline_row_height_reference`, but it doesn't. There is a bug in `horizontal_wrapped` that breaks line height after using `text_edit_multiline`. * **Second commit** - fix. You can see that `horizontal_wrapped_multiline_row_height` now looks like `horizontal_wrapped_multiline_row_height_reference` (although it's not a perfect match, upd: found, this is because of this issue: https://github.com/emilk/egui/issues/4921). I have used LLM to help me with this PR (codex + claude code). BTW, I'm using horizontal_wrapped with end_row instead of vertical + horizontal alternation, because I automatically generate my UI through some complex interactions between elements in my code, and it's can be that my `horizontal` starts in one function, and ends in another. Something like `begin_horizontal`/`end_horizontal`/`get_current_layout` would be very handy, related to https://github.com/emilk/egui/issues/1004. Also, I would like indent to be supported in `horizontal_wrapped`, or also, to have `indent_start`/`indent_end`. This is why I used `monospace("| ")` in my example, it simulates my use-case. --- crates/egui/src/layout.rs | 20 ++++- tests/egui_tests/tests/regression_tests.rs | 85 +++++++++++++++++++ ...orizontal_wrapped_multiline_row_height.png | 3 + ...wrapped_multiline_row_height_reference.png | 3 + 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png create mode 100644 tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index c44c928bb..c35fd254b 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -623,12 +623,24 @@ impl Layout { if (self.is_vertical() && self.horizontal_align() == Align::Center) || self.horizontal_justify() { - frame_size.x = frame_size.x.max(available_rect.width()); // fill full width + // For wrapping layouts, fill the current column width, not the entire layout width. + let width = if self.main_wrap { + region.cursor.width() + } else { + available_rect.width() + }; + frame_size.x = frame_size.x.max(width); // fill full width } if (self.is_horizontal() && self.vertical_align() == Align::Center) || self.vertical_justify() { - frame_size.y = frame_size.y.max(available_rect.height()); // fill full height + // For wrapping layouts, fill the current row height, not the entire layout height. + let height = if self.main_wrap { + region.cursor.height() + } else { + available_rect.height() + }; + frame_size.y = frame_size.y.max(height); // fill full height } let align2 = match self.main_dir { @@ -791,14 +803,14 @@ impl Layout { let new_top = region.cursor.bottom() + spacing.y; region.cursor = Rect::from_min_max( pos2(region.max_rect.left(), new_top), - pos2(INFINITY, new_top + region.cursor.height()), + pos2(INFINITY, new_top), ); } Direction::RightToLeft => { let new_top = region.cursor.bottom() + spacing.y; region.cursor = Rect::from_min_max( pos2(-INFINITY, new_top), - pos2(region.max_rect.right(), new_top + region.cursor.height()), + pos2(region.max_rect.right(), new_top), ); } Direction::TopDown | Direction::BottomUp => {} diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index d92a77103..2d6ab5c67 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -279,3 +279,88 @@ fn warn_if_rect_changes_id() { "Should warn when a widget rect changes Id between passes" ); } + +#[test] +fn horizontal_wrapped_multiline_row_height() { + let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| { + ui.style_mut().interaction.tooltip_delay = 0.0; + ui.style_mut().interaction.show_tooltips_only_when_still = false; + + let mut string = String::new(); + + ui.horizontal_wrapped(|ui| { + ui.monospace("| "); + let _ = ui.button("A"); + let _ = ui.button("B"); + ui.end_row(); + + ui.monospace("| "); + let _ = ui.button("C"); + let _ = ui.button("D"); + let _ = ui.button("E"); + ui.end_row(); + + ui.monospace("| "); + ui.text_edit_multiline(&mut string); + ui.end_row(); + + ui.monospace("| "); + let _ = ui.button("F"); + let _ = ui.button("G"); + ui.end_row(); + + ui.monospace("| "); + let _ = ui.button("H"); + let _ = ui.button("I"); + let _ = ui.button("K"); + ui.end_row(); + }); + }); + + harness.snapshot("horizontal_wrapped_multiline_row_height"); +} + +#[test] +fn horizontal_wrapped_multiline_row_height_reference() { + let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| { + ui.style_mut().interaction.tooltip_delay = 0.0; + ui.style_mut().interaction.show_tooltips_only_when_still = false; + + let mut string = String::new(); + + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("A"); + let _ = ui.button("B"); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("C"); + let _ = ui.button("D"); + let _ = ui.button("E"); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + ui.text_edit_multiline(&mut string); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("F"); + let _ = ui.button("G"); + }); + + ui.horizontal(|ui| { + ui.monospace("| "); + let _ = ui.button("H"); + let _ = ui.button("I"); + let _ = ui.button("K"); + }); + }); + }); + + harness.snapshot("horizontal_wrapped_multiline_row_height_reference"); +} diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png new file mode 100644 index 000000000..e6ac8e446 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef21b42f90401f6b85685e1cc37d07970b38d2b40394f53bbde5bd4f0d54fb95 +size 5340 diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png new file mode 100644 index 000000000..a533a8401 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5025f4cb528ae5edc387149f1d14523ab4b93058f0862e775a1c2276a3e77af6 +size 5377 From 90217f2ad1e21757e8fac8faff2c1a59c0010a24 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 11:54:09 +0100 Subject: [PATCH 20/89] Add error message when calling `.render()` without `.update_buffers()` (#8005) * Closes https://github.com/emilk/egui/issues/7968 --- crates/egui-wgpu/src/renderer.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 3222a5521..640f33f51 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -1,5 +1,3 @@ -#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps - use std::{borrow::Cow, num::NonZeroU64, ops::Range}; use ahash::HashMap; @@ -516,8 +514,12 @@ impl Renderer { // Skip rendering zero-sized clip areas. if let Primitive::Mesh(_) = primitive { // If this is a mesh, we need to advance the index and vertex buffer iterators: - index_buffer_slices.next().unwrap(); - vertex_buffer_slices.next().unwrap(); + index_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); + vertex_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); } continue; } @@ -527,8 +529,12 @@ impl Renderer { match primitive { Primitive::Mesh(mesh) => { - let index_buffer_slice = index_buffer_slices.next().unwrap(); - let vertex_buffer_slice = vertex_buffer_slices.next().unwrap(); + let index_buffer_slice = index_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); + let vertex_buffer_slice = vertex_buffer_slices + .next() + .expect("You must call .update_buffers() before .render()"); if let Some(Texture { bind_group, .. }) = self.textures.get(&mesh.texture_id) { render_pass.set_bind_group(1, bind_group, &[]); @@ -954,6 +960,7 @@ impl Renderer { let index_buffer_staging = queue.write_buffer_with( &self.index_buffer.buffer, 0, + #[expect(clippy::unwrap_used)] // Checked above NonZeroU64::new(required_index_buffer_size).unwrap(), ); @@ -998,6 +1005,7 @@ impl Renderer { let vertex_buffer_staging = queue.write_buffer_with( &self.vertex_buffer.buffer, 0, + #[expect(clippy::unwrap_used)] // Checked above NonZeroU64::new(required_vertex_buffer_size).unwrap(), ); From b4f9cd71407303087b607e0ae9c1b28b71397cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:58:58 +0800 Subject: [PATCH 21/89] Much improved IME (#7967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes #7809 * Closes #7876 * Closes #7908 * Supersedes #7877 * Supersedes #7898 * The author of the PR above replaced it with #7914, which additionally fixes another IME issue. I believe that fix deserves a separate PR. * Reverts #4794 * [x] I have followed the instructions in the PR template This approach is better than #7898 (#7914) because it correctly handles all three major IME types (Chinese, Japanese, and Korean) without requiring a predefined ā€œIME modeā€. ## Environments I haved tested this PR in

macOS 15.7.3 (AArch64, Host of other virtual machines) Run command: `cargo run -p egui_demo_app --release` Tested IMEs: - builtin Chinese IME (Shuangpin - Simplified) - builtin Japanese IME (Romaji) - builtin Korean IME (2-Set)
Windows 11 25H2 (AArch64, Virtual Machine) Build command: `cargo build --release -p egui_demo_app --target=x86_64-pc-windows-gnu --features=glow --no-default-features` (I cannot use `wgpu` due to [this bug](https://github.com/emilk/egui/issues/4381), which prevents debugging inside the VM. Anyways, the rendering backend should be irrelevant here.) Tested IMEs: - builtin Chinese IME (Shuangpin) - Sogou IME (Chinese Shuangpin) - WeType IME (Chinese Shuangpin) - builtin Japanese IME (Hiragana) - builtin Korean IME (2 Beolsik)
Linux [Wayland + IBus] (AArch64, Virtual Machine) Fedora KDE Plasma Desktop 43 [Wayland + IBus 1.5.33-rc2] (Not working at the moment because of [another issue](https://github.com/emilk/egui/issues/7485) that will be fixed by #7983. It is [a complicated story](https://github.com/emilk/egui/pull/7973#issuecomment-4074627603). ) > [!NOTE] > > IBus is partially broken in this system. The Input Method Selector refuses to select IBus. As a workaround, I have to open System Settings -> Virtual Keyboard and select ā€œIBus Waylandā€ to start an IBus instance that works in egui. > > The funny thing is: the Chinese Intelligent Pinyin IME is broken in native Apps like System Settings and KWrite, but works correctly in egui! > >
Screencast: What > > ![2026-03-13 3 10 11 AM](https://github.com/user-attachments/assets/4001cf12-8089-46f5-9cf4-e41d8f77ee24) >
Build command: `cross build --release -p egui_demo_app --target=aarch64-unknown-linux-gnu --features=wayland,wgpu --no-default-features` (The Linux toolchain on my mac is somehow broken, so I used `cross` instead.) Tested IMEs: - Chinese Intelligent Pinyin IME (Shuangpin) - Japanese Anthy IME (Hiragana) - Korean Hangul IME
Linux [X11 + Fcitx5] (AArch64, Virtual Machine) Debian 13 [Cinnamon 6.4.10 + X11 + Fcitx5 5.1.2] Build command: `cross build --release -p egui_demo_app --target=aarch64-unknown-linux-gnu --features=x11,wgpu --no-default-features` Tested IMEs: - Chinese Shuangpin IME - Chinese Rime IME with `luna-pinyin` - Japanese Mozc IME (Hiragana) - Korean Hangul IME Unlike macOS and Linux + Wayland, key-release events for keys processed by the IME are still forwarded to `egui`. These appear to be harmless in practice. Unlike on Windows, however, they cannot be filtered reliably because there are no corresponding key-press events marked as ā€œprocessed by IMEā€.
--- There are too many possible combinations to test (Operating Systems Ɨ [Desktop Environment](https://en.wikipedia.org/wiki/Desktop_environment)s Ɨ [Windowing System](https://en.wikipedia.org/wiki/Windowing_system)s Ɨ [IMF](https://wiki.archlinux.org/title/Input_method#Input_method_framework)s Ɨ [IME](https://en.wikipedia.org/wiki/Input_method)s Ɨ …), and I only have access to a limited subset. For example, Google Japanese Input refused to install on my Windows VM, and some paid Japanese IMEs are not accessible to me. Therefore, I would appreciate feedback from people other than me using all kinds of environments. ## Details There are two possible approaches to removing keyboard events that have already been processed by an IME: * Approach 1: Filter out events inside `egui` that appear to have been received during IME composition. * Approach 2: Filter out such events in the platform backend (terminology [borrowed from imgui](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md#using-standard-backends), e.g. the `egui-winit` crate or the code under `web/` in the `eframe` crate.). Both approaches already exist in `egui`: * #4794 uses the first approach, filtering these events in the `TextEdit`-related code. * `eframe` uses the second approach in its web integration. See: Compared to the first approach, the second has a clear advantage: when events are passed from the platform backends into `egui`, they are simplified and lose information. In contrast, events in the platform backends are the original events, which allows them to be handled more flexibly. This is also why #7898 (#7914), which attempts to address the issue from within the `egui` crate, struggles to make all IMEs work correctly at the same time and requires manually selecting an ā€œIME modeā€: the events received by `egui` have already been reduced and therefore lack necessary information. A more appropriate solution is to consistently follow the second approach, explicitly requiring platform backends not to forward events that have already been processed by the IME to `egui`. This is the method used in this PR. Specifically, this PR works within the `egui-winit` crate, where the original `KeyboardInput` events can be accessed. At least for key press events, these can be used directly to determine whether the event has already been processed by the IME on Windows (by checking whether `logical_key` equals `winit::keyboard::NamedKey::Process`). This makes it straightforward to ensure that all IMEs work correctly at the same time. This PR also reverts #4794, which took the first approach. It filters out some events that merely look like they were received during IME composition but actually are not. It also messes up the order of those events along the way. As a result, it caused several IME-related issues. One of the sections in the Demonstrations below will illustrate these problems. ## Demonstrations
Changes not included in this PR for displaying Unicode characters in demonstrations Download `unifont-17.0.03.otf` from , and place it at `crates/egui_demo_app/src/unifont-17.0.03.otf`. In `crates/egui_demo_app/src/wrap_app.rs`, add these lines at the beginning of `impl WrapApp`'s `pub fn new`: ```rust { const MAIN_FONT: &'static [u8] = include_bytes!("./unifont-17.0.03.otf"); let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "main-font".to_owned(), std::sync::Arc::new(egui::FontData::from_static(MAIN_FONT)), ); let proportional = fonts .families .entry(egui::FontFamily::Proportional) .or_default(); proportional.insert(0, "main-font".to_owned()); cc.egui_ctx.set_fonts(fonts); } ``` (I took this from somewhere, but I forgot where it is. Sorry…)
[GNU Unifont](https://unifoundry.com/unifont/index.html) is licensed under [OFL-1.1](https://unifoundry.com/OFL-1.1.txt). ### This PR Fixes: Focus on a single-line `TextEdit` is lost after completing candidate selection with Japanese IME on Windows (#7809)
Screencast: āœ… Japanese IME now behaves correctly while Korean IME behaves as before ![7809](https://github.com/user-attachments/assets/6e92f6e6-fed4-46f4-96e1-e0e12dadc360)
### This PR Fixes: Committing Japanese IME text with Enter inserts an unintended newline in multiline `TextEdit` on Windows (#7876)
Screencast: āœ… Japanese IME now behaves correctly while Korean IME behaves as before ![7876](https://github.com/user-attachments/assets/03d2cb22-fd0c-45fe-9132-e59fa39bfcf3)
### This PR Fixes: Backspacing deletes characters during composition in certain Chinese IMEs (e.g., Sogou) on Windows (#7908)
Screencast: āœ… Sogou IME now behaves correctly ![7908](https://github.com/user-attachments/assets/2c63de28-26f0-4387-9c50-dceabfdbe99d)
### This PR Obsoletes #4794, because `egui` receives only IME events during composition from now on On Windows, ā€œincompatibleā€ events are filtered in `egui-winit`, aligning the behavior with other systems.
Screencasts Some Chinese IMEs on Windows: ![2026-03-13 12 25 37 AM](https://github.com/user-attachments/assets/064fd1c7-244b-4053-bd24-c65d768cd943) The default Japanese IMEs on Windows: ![2026-03-13 12 28 33 AM](https://github.com/user-attachments/assets/f799b0b5-350b-4b05-a769-bcef16255bdb)
The 2-set Korean IMEs handle arrow keys differently. It will be discussed in the next section. ### This PR Reverts #4794, because it introduced several bugs Some of its bugs have already been worked around in the past, but those workarounds might also be problematic. For example, #4912 is a workaround for a bug (#4908) introduced by #4794, and that workaround is in fact the root cause of the macOS backspacing bug I have worked around with #7810. (The reversion of #4912 is out of the scope of this PR, I will do that in #7983.) #### It Caused: Arrow keys are incorrectly blocked during typical Korean IME composition When composing Korean text using 2-Set IMEs, users should still be able to move the cursor with arrow keys regardless if the composition is committed. ##### Correct behavior
Screencasts macOS TextEdit: ![2026-03-12 8 04 15 PM](https://github.com/user-attachments/assets/24383568-f51c-4a74-9251-adfd942cad8f) Windows Notepad: ![2026-03-12 8 05 08 PM](https://github.com/user-attachments/assets/5a29a5b5-69b8-407b-b1a4-84fdb8f8847d) With #4794 reverted, `egui` also behaves correctly (tested on Linux + Wayland, macOS, and Windows): ![2026-03-12 8 03 51 PM](https://github.com/user-attachments/assets/fcb7f25c-1329-4eb1-82f2-1cea33dcca73)
##### Incorrect behavior caused by #4794 `remove_ime_incompatible_events` removed arrow-key events in such cases. As a result, the first arrow key press only commits the composition, and users need to press the arrow key again to move the cursor:
Screencast ![2026-03-12 8 06 40 PM](https://github.com/user-attachments/assets/6760c6bd-b6ce-44ea-b192-6bd165191c01)
This is essentially the same issue described here: https://github.com/emilk/egui/pull/7877#issuecomment-3852719948 #### It Caused: Backspacing leaves the last character in Korean IME pre-edit text not removed on macOS
Screencasts Before this PR: ![2026-03-17 10 48 12 PM](https://github.com/user-attachments/assets/88021e7e-caf6-4aa9-8f73-ecffc63cda06) After this PR: ![2026-03-17 10 47 23 PM](https://github.com/user-attachments/assets/379cd0db-24e0-4c0e-a5b4-edb37d3e1df7)
### Korean IMEs also use Enter to confirm Hanja selections, and will not work properly in the Korean ā€œIME modeā€ proposed by #7898 (#7914)
Screencast: Korean IME using Enter and Space for confirmation (IBus Korean Hangul IME) The screencast below demonstrates that some Korean IMEs handle Hanja selection in a way similar to Japanese IMEs: the Up/Down arrow keys are used to navigate candidates, and Enter confirms the selected candidate. ![2026-03-13 6 39 17 AM](https://github.com/user-attachments/assets/0b054cc6-2251-4689-95a4-d69a9be36371)
Screencasts: Another example Using the built-in Korean IME on Windows, I type two lines: the first line in Hangul, and the second line as the same word converted to Hanja. Correct behavior in Notepad (reference): ![7914-ref](https://github.com/user-attachments/assets/1e9f9315-eb71-497a-b6e2-2b11eb6bbf7f) Behavior after applying this PR, which matches the Notepad behavior: ![7914-7967](https://github.com/user-attachments/assets/26f12b9f-9354-45b8-b2a8-ede28c34c5b1) Behavior after applying #7914 with the ā€œIME modeā€ set to Korean (which is also the behavior before this PR being applied): ![7914-7914](https://github.com/user-attachments/assets/0f82a019-c491-4b64-a92a-d88d62dfbd84) On the second line, each time a Hanja character is confirmed, an unintended newline is inserted. This mirrors the Japanese IME issues that are supposed to be fixed by setting the ā€œIME modeā€ to Japanese. (These Japanese IME issues are fixed in this PR as mentioned before.)
--- crates/egui-winit/src/lib.rs | 138 +++++++++++++++++-- crates/egui/src/data/input.rs | 4 + crates/egui/src/widgets/text_edit/builder.rs | 29 +--- 3 files changed, 131 insertions(+), 40 deletions(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 234a9989b..90f0311d5 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -9,6 +9,9 @@ #![expect(clippy::manual_range_contains)] +#[cfg(target_os = "windows")] +use std::collections::HashSet; + #[cfg(feature = "accesskit")] pub use accesskit_winit; pub use egui; @@ -106,6 +109,12 @@ pub struct State { allow_ime: bool, ime_rect_px: Option, + + /// Used by [`State::try_on_ime_processed_keyboard_input`] to track key + /// release events that should be filtered out. See comments in that method + /// for details. + #[cfg(target_os = "windows")] + pressed_processed_physical_keys: HashSet, } impl State { @@ -148,6 +157,8 @@ impl State { allow_ime: false, ime_rect_px: None, + #[cfg(target_os = "windows")] + pressed_processed_physical_keys: HashSet::new(), }; slf.egui_input @@ -364,25 +375,33 @@ impl State { is_synthetic, .. } => { - // Winit generates fake "synthetic" KeyboardInput events when the focus - // is changed to the window, or away from it. Synthetic key presses - // represent no real key presses and should be ignored. - // See https://github.com/rust-windowing/winit/issues/3543 if *is_synthetic && event.state == ElementState::Pressed { + // Winit generates fake "synthetic" KeyboardInput events when the focus + // is changed to the window, or away from it. Synthetic key presses + // represent no real key presses and should be ignored. + // See https://github.com/rust-windowing/winit/issues/3543 EventResponse { repaint: true, consumed: false, } } else { - self.on_keyboard_input(event); + let egui_wants_keyboard_input = self.egui_ctx.egui_wants_keyboard_input(); - // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. - let consumed = self.egui_ctx.egui_wants_keyboard_input() - || event.logical_key - == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab); - EventResponse { - repaint: true, - consumed, + if let Some(response) = + self.try_on_ime_processed_keyboard_input(event, egui_wants_keyboard_input) + { + response + } else { + self.on_keyboard_input(event); + + // When pressing the Tab key, egui focuses the first focusable element, hence Tab always consumes. + let consumed = egui_wants_keyboard_input + || event.logical_key + == winit::keyboard::Key::Named(winit::keyboard::NamedKey::Tab); + EventResponse { + repaint: true, + consumed, + } } } } @@ -526,6 +545,91 @@ impl State { } } + #[cfg(not(target_os = "windows"))] + #[expect(clippy::unused_self, clippy::needless_pass_by_ref_mut)] + #[inline(always)] + fn try_on_ime_processed_keyboard_input( + &mut self, + _event: &winit::event::KeyEvent, + _egui_wants_keyboard_input: bool, + ) -> Option { + // `KeyboardInput` events processed by the IME are not emitted by + // `winit` on non-Windows platforms, so we don't need to do anything + // here. + + None + } + + #[cfg(target_os = "windows")] + #[inline(always)] + fn try_on_ime_processed_keyboard_input( + &mut self, + event: &winit::event::KeyEvent, + egui_wants_keyboard_input: bool, + ) -> Option { + if !self.allow_ime { + None + } else if event.logical_key == winit::keyboard::NamedKey::Process { + // On Windows, the current version of `winit` (0.30.12) has a bug + // where `KeyboardInput` events processed by the IME are still + // emitted. [^1] + // + // As a workaround, we detect these events by checking whether their + // `logical_key` is `winit::keyboard::NamedKey::Process`, and filter + // them out to keep behavior consistent with other platforms. + // + // `winit::keyboard::NamedKey::Process` is not documented in + // `winit`. Reading through its source code, we find that it is + // mapped from `VK_PROCESSKEY` on Windows [^2]. (On an unrelated + // note, Web is the only other platform that also uses it [^3].) + // According to Microsoft, ā€œthe IME sets the virtual key value + // to `VK_PROCESSKEY` after processing a key input messageā€ [^4]. + // See also [^5]. + // (I can't find a documentation page dedicated to this value.) + // + // TODO(umajho): Remove this workaround once the `winit` bug is fixed + // and we've updated to a version that includes the fix. NOTE: Don't + // forget to also remove the `pressed_processed_physical_keys` field + // and its related code. + // + // [^1]: https://github.com/rust-windowing/winit/issues/4508 + // [^2]: https://github.com/rust-windowing/winit/blob/e9809ef54b18499bb4f2cac945719ecc2a61061b/src/platform_impl/windows/keyboard_layout.rs#L946 + // [^3]: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values + // [^4]: https://learn.microsoft.com/en-us/windows/win32/api/imm/nf-imm-immgetvirtualkey#remarks + // [^5]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/keyboard-input#character-messages + + self.pressed_processed_physical_keys + .insert(event.physical_key); + + Some(EventResponse { + repaint: false, + consumed: egui_wants_keyboard_input, + }) + } else if event.state == ElementState::Released + && self + .pressed_processed_physical_keys + .remove(&event.physical_key) + { + // Unlike key-presses, we can not tell whether a key-release event + // is processed by the IME or not by looking at its `logical_key`, + // because their `logical_key` is the original value (e.g. + // `winit::keyboard::Key::Character(…)`) rather than + // `winit::keyboard::Key::Named(winit::keyboard::NamedKey::Process)`. + // (See the screencast for Windows in [^1].) + // So we track the physical keys of processed key-presses and + // filter out the corresponding key-releases. + // + // [^1]: https://github.com/rust-windowing/winit/issues/4508 + + Some(EventResponse { + repaint: false, + consumed: egui_wants_keyboard_input, + }) + } else { + None + } + } + /// ## NOTE /// /// on Mac even Cmd-C is pressed during ime, a `c` is pushed to Preedit. @@ -1001,6 +1105,16 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; + #[cfg(target_os = "windows")] + if !self.allow_ime { + // Defensively clear the set to avoid unexpected behavior. + // + // We don't do the same in `ime_event_disable` because the key + // release events for IME confirmation keys arrive after + // `winit::event::Ime::Disabled`. + self.pressed_processed_physical_keys.clear(); + } + profiling::scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index a52d40233..5e1680334 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -441,6 +441,10 @@ pub enum Event { Text(String), /// A key was pressed or released. + /// + /// ## Note for integration authors + /// + /// Key events that has been processed by IMEs should not be sent to `egui`. Key { /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak /// keyboard layout, it will be taken into account. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index a6c41e71c..1f103d2f8 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -993,13 +993,7 @@ fn events( let mut any_change = false; - let mut events = ui.input(|i| i.filtered_events(&event_filter)); - - if state.ime_enabled { - remove_ime_incompatible_events(&mut events); - // Process IME events first: - events.sort_by_key(|e| !matches!(e, Event::Ime(_))); - } + let events = ui.input(|i| i.filtered_events(&event_filter)); for event in &events { let did_mutate_text = match event { @@ -1232,27 +1226,6 @@ fn events( // ---------------------------------------------------------------------------- -fn remove_ime_incompatible_events(events: &mut Vec) { - // Remove key events which cause problems while 'IME' is being used. - // See https://github.com/emilk/egui/pull/4509 - events.retain(|event| { - !matches!( - event, - Event::Key { repeat: true, .. } - | Event::Key { - key: Key::Backspace - | Key::ArrowUp - | Key::ArrowDown - | Key::ArrowLeft - | Key::ArrowRight, - .. - } - ) - }); -} - -// ---------------------------------------------------------------------------- - /// Returns `Some(new_cursor)` if we did mutate `text`. fn check_for_mutating_key_press( os: OperatingSystem, From d985bf9b83fb69b7028152ddaa6ee9ec993eb4d9 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Tue, 24 Mar 2026 12:37:33 +0100 Subject: [PATCH 22/89] Fill in DisplayHandle automatically on web painter just like it's done on winit (#8006) * Closes https://github.com/emilk/egui/issues/8001 * Follow-up to https://github.com/emilk/egui/pull/7990 Also simplified/shortened the comments around display handle a bit. Lots a repetition there made it hard to upgrade otherwise. --- crates/eframe/src/web/web_painter_wgpu.rs | 36 ++++++++- crates/egui-wgpu/src/setup.rs | 91 ++++++++--------------- 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index ebce9d981..63702592d 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -25,6 +25,24 @@ pub(crate) struct WebPainterWgpu { needs_reconfigure: bool, } +/// Owned web display handle that is `Send + Sync`. +/// +/// `DisplayHandle` from `raw-window-handle` is `!Send`/`!Sync` because the enum +/// contains platform variants with raw pointers. On web the handle is always empty, +/// so this wrapper is safe. +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug)] +struct WebDisplay; + +#[cfg(target_arch = "wasm32")] +impl egui_wgpu::wgpu::rwh::HasDisplayHandle for WebDisplay { + fn display_handle( + &self, + ) -> Result, egui_wgpu::wgpu::rwh::HandleError> { + Ok(egui_wgpu::wgpu::rwh::DisplayHandle::web()) + } +} + impl WebPainterWgpu { pub fn render_state(&self) -> Option { self.render_state.clone() @@ -64,7 +82,17 @@ impl WebPainterWgpu { ) -> Result { log::debug!("Creating wgpu painter"); - let instance = options.wgpu_options.wgpu_setup.new_instance().await; + // Inject the display handle into the wgpu setup so that wgpu can create surfaces on WebGL. + let mut wgpu_options = options.wgpu_options.clone(); + if let egui_wgpu::WgpuSetup::CreateNew(ref mut create_new) = wgpu_options.wgpu_setup + && create_new.display_handle.is_none() + { + // Force WebGL, useful for quick & dirty testing: + //create_new.instance_descriptor.backends = wgpu::Backends::GL; + create_new.display_handle = Some(Box::new(WebDisplay)); + } + + let instance = wgpu_options.wgpu_setup.new_instance().await; let surface = instance .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|err| format!("failed to create wgpu surface: {err}"))?; @@ -72,7 +100,7 @@ impl WebPainterWgpu { let depth_stencil_format = egui_wgpu::depth_format_from_bits(options.depth_buffer, 0); let render_state = RenderState::create( - &options.wgpu_options, + &wgpu_options, &instance, Some(&surface), egui_wgpu::RendererOptions { @@ -90,7 +118,7 @@ impl WebPainterWgpu { let surface_configuration = wgpu::SurfaceConfiguration { format: render_state.target_format, - present_mode: options.wgpu_options.present_mode, + present_mode: wgpu_options.present_mode, view_formats: vec![render_state.target_format], ..default_configuration }; @@ -106,7 +134,7 @@ impl WebPainterWgpu { surface_configuration, depth_stencil_format, depth_texture_view: None, - on_surface_status: Arc::clone(&options.wgpu_options.on_surface_status) as _, + on_surface_status: Arc::clone(&wgpu_options.on_surface_status) as _, screen_capture_state: None, capture_tx, capture_rx, diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index 9d83d4380..c5b3f0421 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -2,24 +2,19 @@ use std::sync::Arc; /// A cloneable display handle for use with [`wgpu::InstanceDescriptor`]. /// -/// This trait exists so that a [`winit::event_loop::OwnedDisplayHandle`] (or similar platform -/// display handle) can be stored, cloned, and later passed to wgpu. +/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable +/// `Box`. This trait wraps it so it can be cloned +/// alongside the rest of the egui wgpu configuration. /// -/// wgpu requires an explicit display handle for GLES on some platforms (notably Wayland). -/// Because [`wgpu::InstanceDescriptor`] contains a `Box` which is -/// not cloneable, we wrap the handle in this trait so it can be cloned alongside the rest of -/// the egui wgpu configuration. -/// -/// This is automatically implemented for all types that satisfy the bounds (including -/// [`winit::event_loop::OwnedDisplayHandle`]). +/// Automatically implemented for all types that satisfy the bounds +/// (including [`winit::event_loop::OwnedDisplayHandle`]). pub trait EguiDisplayHandle: wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static { - /// Clone this handle into a `Box` suitable for setting on - /// [`wgpu::InstanceDescriptor::display`]. + /// Clone into a `Box` for [`wgpu::InstanceDescriptor::display`]. fn clone_for_wgpu(&self) -> Box; - /// Clone this handle into a new `Box`. + /// Clone into a new `Box`. fn clone_display_handle(&self) -> Box; } @@ -68,27 +63,14 @@ pub enum WgpuSetup { impl WgpuSetup { /// Creates a new [`WgpuSetup::CreateNew`] with the given display handle. /// - /// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web) - /// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require - /// one. Providing it unconditionally ensures your app works everywhere. - /// - /// If you don't have a display handle available, use [`Self::without_display_handle`] - /// instead — it will still work on the majority of platforms. - /// - /// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// See [`WgpuSetupCreateNew::from_display_handle`] for details. pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self { Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle)) } /// Creates a new [`WgpuSetup::CreateNew`] without a display handle. /// - /// A display handle is not required for headless operation (offscreen rendering, tests, - /// compute-only workloads). It also isn't needed on most platforms even when presenting - /// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one. - /// - /// If you do have a display handle available, prefer [`Self::from_display_handle`] for - /// maximum compatibility. With winit you can obtain one via - /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// See [`WgpuSetupCreateNew::without_display_handle`] for details. pub fn without_display_handle() -> Self { Self::CreateNew(WgpuSetupCreateNew::without_display_handle()) } @@ -175,44 +157,32 @@ pub type NativeAdapterSelectorMethod = Arc< /// /// Used for [`WgpuSetup::CreateNew`]. /// -/// Use [`Self::from_display_handle`] when you have a display handle available — this is the -/// recommended constructor. With winit you can obtain one via -/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). -/// Most platforms (Windows, macOS/iOS, Android, web) work fine without one, but some -/// (e.g. Wayland on Linux with GLES) require it. Providing it unconditionally ensures your -/// app works everywhere. +/// Prefer [`Self::from_display_handle`] when you have a display handle available. +/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL) +/// require it, so providing one ensures maximum compatibility. +/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). /// -/// If you don't have a display handle, use [`Self::without_display_handle`] — it will still -/// work on the majority of platforms, and is appropriate for headless rendering, tests, or -/// web targets. -/// -/// Note: The [`wgpu::InstanceDescriptor::display`] field is always stored as `None` in -/// [`Self::instance_descriptor`]. The display handle is stored separately so it can be cloned -/// (since [`wgpu::InstanceDescriptor`] itself does not implement `Clone`), and is injected -/// into the descriptor at instance creation time. +/// Note: The display handle is stored in [`Self::display_handle`] rather than in +/// [`Self::instance_descriptor`] so the config can be cloned +/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time. pub struct WgpuSetupCreateNew { - /// Instance descriptor for creating a wgpu instance. + /// Descriptor for the wgpu instance. /// - /// The [`wgpu::InstanceDescriptor::display`] field should be left as `None`; use the - /// [`Self::display_handle`] field instead (it will be injected when the instance is created). + /// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`] + /// instead (injected at instance creation time). /// - /// The most important field is [`wgpu::InstanceDescriptor::backends`], which - /// controls which backends are supported (wgpu will pick one of these). - /// If you only want to support WebGL (and not WebGPU), - /// you can set this to [`wgpu::Backends::GL`]. - /// By default on web, WebGPU will be used if available. - /// WebGL will only be used as a fallback, - /// and only if you have enabled the `webgl` feature of crate `wgpu`. + /// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls + /// which backends are supported (wgpu will pick one of these). For example, set it to + /// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred + /// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`). pub instance_descriptor: wgpu::InstanceDescriptor, - /// The display handle to pass to wgpu when creating the instance. + /// Display handle passed to wgpu at instance creation time. /// - /// Most platforms (Windows, macOS/iOS, Android, web) work without this, but some - /// (e.g. Wayland on Linux with GLES) require it. If you have a display handle - /// available, providing it ensures maximum compatibility. + /// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere. + /// With winit, use [`winit::event_loop::OwnedDisplayHandle`]. /// - /// When using winit, this is typically the - /// [`winit::event_loop::OwnedDisplayHandle`] obtained from the event loop. + /// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty. pub display_handle: Option>, /// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web. @@ -258,8 +228,11 @@ impl WgpuSetupCreateNew { /// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one. /// /// If you do have a display handle available, prefer [`Self::from_display_handle`] for - /// maximum compatibility. With winit you can obtain one via - /// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// maximum compatibility. + /// + /// With winit you can obtain one via [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle). + /// + /// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty. pub fn without_display_handle() -> Self { Self { instance_descriptor: wgpu::InstanceDescriptor { From 3a2d437bd71641862ab49274de57ec59837474bd Mon Sep 17 00:00:00 2001 From: Deuracell Date: Tue, 24 Mar 2026 12:50:20 +0000 Subject: [PATCH 23/89] Add `DatePickerButton::reverse_years/year_scroll_to` (#7978) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Two new builder methods on `DatePickerButton`: - **`reverse_years(bool)`** — lists years in descending order (newest first). Useful when users are more likely to pick recent years. - **`year_scroll_to(i32)`** — scrolls the year dropdown to a specific year when it first opens, centred in the list. Defaults to the currently selected year, so the picker no longer opens at the top of a 110-item list. ## Why The year `ComboBox` currently always renders in ascending order and opens scrolled to the top. For a range spanning e.g. 1925–2035, the current year is buried near the bottom. Users have to scroll past ~100 entries every time they open the picker. ## Notes - Both options are purely additive builder methods — no breaking changes. - `year_scroll_needed` is a persisted state flag that is set on popup open and cleared after the first scroll, so the user can freely scroll the list after that. - Existing behaviour is unchanged when neither method is called. Co-authored-by: Simon Deurell --- crates/egui_extras/src/datepicker/button.rs | 22 +++++++++++- crates/egui_extras/src/datepicker/popup.rs | 38 +++++++++++++++------ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 98aceefe2..d6f69fe77 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -21,6 +21,8 @@ pub struct DatePickerButton<'a> { format: String, highlight_weekends: bool, start_end_years: Option>, + reverse_years: bool, + year_scroll_to: Option, } impl<'a> DatePickerButton<'a> { @@ -36,6 +38,8 @@ impl<'a> DatePickerButton<'a> { format: "%Y-%m-%d".to_owned(), highlight_weekends: true, start_end_years: None, + reverse_years: false, + year_scroll_to: None, } } @@ -115,6 +119,21 @@ impl<'a> DatePickerButton<'a> { self.start_end_years = Some(start_end_years); self } + + /// List years in descending order in the year dropdown. (Default: false) + #[inline] + pub fn reverse_years(mut self, reverse_years: bool) -> Self { + self.reverse_years = reverse_years; + self + } + + /// Scroll the year dropdown to this year when the picker first opens. + /// Defaults to the currently selected year. + #[inline] + pub fn year_scroll_to(mut self, year: i32) -> Self { + self.year_scroll_to = Some(year); + self + } } impl Widget for DatePickerButton<'_> { @@ -154,7 +173,6 @@ impl Widget for DatePickerButton<'_> { pos.x = button_response.rect.right() - width_with_padding; } - // Check to make sure the calendar never is displayed out of window pos.x = pos.x.max(ui.style().spacing.window_margin.leftf()); //TODO(elwerene): Better positioning @@ -182,6 +200,8 @@ impl Widget for DatePickerButton<'_> { calendar_week: self.calendar_week, highlight_weekends: self.highlight_weekends, start_end_years: self.start_end_years, + reverse_years: self.reverse_years, + year_scroll_to: self.year_scroll_to, } .draw(ui) }) diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index d353307b3..1c24ca81d 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -13,6 +13,7 @@ struct DatePickerPopupState { month: u32, day: u32, setup: bool, + year_scroll_needed: bool, } impl DatePickerPopupState { @@ -36,6 +37,8 @@ pub(crate) struct DatePickerPopup<'a> { pub calendar_week: bool, pub highlight_weekends: bool, pub start_end_years: Option>, + pub reverse_years: bool, + pub year_scroll_to: Option, } impl DatePickerPopup<'_> { @@ -51,6 +54,7 @@ impl DatePickerPopup<'_> { popup_state.month = self.selection.month(); popup_state.day = self.selection.day(); popup_state.setup = true; + popup_state.year_scroll_needed = true; ui.data_mut(|data| data.insert_persisted(id, popup_state.clone())); } @@ -60,7 +64,7 @@ impl DatePickerPopup<'_> { let spacing = 2.0; ui.spacing_mut().item_spacing = Vec2::splat(spacing); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Don't wrap any text + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); StripBuilder::new(ui) .clip(false) @@ -89,15 +93,30 @@ impl DatePickerPopup<'_> { Some(range) => (*range.start(), *range.end()), None => (today.year() - 100, today.year() + 10), }; - for year in start_year..=end_year { - if ui - .selectable_value( - &mut popup_state.year, - year, - year.to_string(), - ) - .changed() + let scroll_to_year = + self.year_scroll_to.unwrap_or(popup_state.year); + let years: Vec = if self.reverse_years { + (start_year..=end_year).rev().collect() + } else { + (start_year..=end_year).collect() + }; + for year in years { + let resp = ui.selectable_value( + &mut popup_state.year, + year, + year.to_string(), + ); + if popup_state.year_scroll_needed + && year == scroll_to_year { + resp.scroll_to_me(Some(Align::Center)); + popup_state.year_scroll_needed = false; + ui.memory_mut(|mem| { + mem.data + .insert_persisted(id, popup_state.clone()); + }); + } + if resp.changed() { popup_state.day = popup_state .day .min(popup_state.last_day_of_month()); @@ -349,7 +368,6 @@ impl DatePickerPopup<'_> { ); if day == today { - // Encircle today's date let stroke = ui .visuals() .widgets From 1e4619c5ef93dee9cf0dd9777a6d4ab20272ba1a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 13:55:28 +0100 Subject: [PATCH 24/89] Explain that we shouldn't update wasm-bindgen version willy-nilly --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d042c2349..4c8a52626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,8 +138,8 @@ type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } -wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml -wasm-bindgen-futures = "0.4.0" +wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766 +wasm-bindgen-futures = "0.4.58" wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" web-time = "1.1.0" # Timekeeping for native and web From 5d5f0dedccc7ec170c3cc51c5f7ba0990549c984 Mon Sep 17 00:00:00 2001 From: Ryan Bluth Date: Tue, 24 Mar 2026 08:58:02 -0400 Subject: [PATCH 25/89] Allow rotation of rectangles and ellipses (#7682) Added the ability to rotate rectangles and ellipses. Similar to the existing text implementation * [x ] I have followed the instructions in the PR template --- .../src/demo/tests/tessellation_test.rs | 1 + crates/epaint/src/shape_transform.rs | 2 + crates/epaint/src/shapes/ellipse_shape.rs | 30 ++++- crates/epaint/src/shapes/rect_shape.rs | 37 +++++- crates/epaint/src/tessellator.rs | 20 +++ .../tests/snapshots/rotated_ellipse.png | 3 + .../tests/snapshots/rotated_rect.png | 3 + tests/egui_tests/tests/test_rotation.rs | 117 ++++++++++++++++++ 8 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/rotated_ellipse.png create mode 100644 tests/egui_tests/tests/snapshots/rotated_rect.png create mode 100644 tests/egui_tests/tests/test_rotation.rs diff --git a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs index 3bb814889..abe3280eb 100644 --- a/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/tessellation_test.rs @@ -297,6 +297,7 @@ fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) { blur_width, round_to_pixels, brush: _, + angle: _, } = shape; let round_to_pixels = round_to_pixels.get_or_insert(true); diff --git a/crates/epaint/src/shape_transform.rs b/crates/epaint/src/shape_transform.rs index 8fce01f64..71cc1332e 100644 --- a/crates/epaint/src/shape_transform.rs +++ b/crates/epaint/src/shape_transform.rs @@ -57,6 +57,7 @@ pub fn adjust_colors( radius: _, fill, stroke, + angle: _, }) | Shape::Rect(RectShape { rect: _, @@ -67,6 +68,7 @@ pub fn adjust_colors( round_to_pixels: _, blur_width: _, brush: _, + angle: _, }) => { adjust_color(fill); adjust_color(&mut stroke.color); diff --git a/crates/epaint/src/shapes/ellipse_shape.rs b/crates/epaint/src/shapes/ellipse_shape.rs index 310638d0f..b436eb841 100644 --- a/crates/epaint/src/shapes/ellipse_shape.rs +++ b/crates/epaint/src/shapes/ellipse_shape.rs @@ -10,6 +10,9 @@ pub struct EllipseShape { pub radius: Vec2, pub fill: Color32, pub stroke: Stroke, + + /// Rotate ellipse by this many radians clockwise around its center. + pub angle: f32, } impl EllipseShape { @@ -20,6 +23,7 @@ impl EllipseShape { radius, fill: fill_color.into(), stroke: Default::default(), + angle: 0.0, } } @@ -30,18 +34,38 @@ impl EllipseShape { radius, fill: Default::default(), stroke: stroke.into(), + angle: 0.0, } } + /// Set the rotation of the ellipse (in radians, clockwise). + /// The ellipse rotates around its center. + #[inline] + pub fn with_angle(mut self, angle: f32) -> Self { + self.angle = angle; + self + } + + /// Set the rotation of the ellipse (in radians, clockwise) around a custom pivot point. + #[inline] + pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self { + self.angle = angle; + let rot = emath::Rot2::from_angle(angle); + self.center = pivot + rot * (self.center - pivot); + self + } + /// The visual bounding rectangle (includes stroke width) pub fn visual_bounding_rect(&self) -> Rect { if self.fill == Color32::TRANSPARENT && self.stroke.is_empty() { Rect::NOTHING } else { - Rect::from_center_size( - self.center, + let rect = Rect::from_center_size( + Pos2::ZERO, self.radius * 2.0 + Vec2::splat(self.stroke.width), - ) + ); + rect.rotate_bb(emath::Rot2::from_angle(self.angle)) + .translate(self.center.to_vec2()) } } } diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index 2e855d369..e0c528377 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -54,13 +54,16 @@ pub struct RectShape { /// Since most rectangles do not have a texture, this is optional and in an `Arc`, /// so that [`RectShape`] is kept small.. pub brush: Option>, + + /// Rotate rectangle by this many radians clockwise around its center. + pub angle: f32, } #[test] fn rect_shape_size() { assert_eq!( std::mem::size_of::(), - 48, + 56, "RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( @@ -88,6 +91,7 @@ impl RectShape { round_to_pixels: None, blur_width: 0.0, brush: Default::default(), + angle: 0.0, } } @@ -157,6 +161,25 @@ impl RectShape { self } + /// Set the rotation of the rectangle (in radians, clockwise). + /// The rectangle rotates around its center. + #[inline] + pub fn with_angle(mut self, angle: f32) -> Self { + self.angle = angle; + self + } + + /// Set the rotation of the rectangle (in radians, clockwise) around a custom pivot point. + #[inline] + pub fn with_angle_and_pivot(mut self, angle: f32, pivot: Pos2) -> Self { + self.angle = angle; + let rot = emath::Rot2::from_angle(angle); + let center = self.rect.center(); + let new_center = pivot + rot * (center - pivot); + self.rect = self.rect.translate(new_center - center); + self + } + /// The visual bounding rectangle (includes stroke width) #[inline] pub fn visual_bounding_rect(&self) -> Rect { @@ -168,7 +191,17 @@ impl RectShape { StrokeKind::Middle => self.stroke.width / 2.0, StrokeKind::Outside => self.stroke.width, }; - self.rect.expand(expand + self.blur_width / 2.0) + let expanded = self.rect.expand(expand + self.blur_width / 2.0); + if self.angle == 0.0 { + expanded + } else { + // Rotate around the rectangle's center and compute bounding box + let center = self.rect.center(); + let rect_relative = Rect::from_center_size(Pos2::ZERO, expanded.size()); + rect_relative + .rotate_bb(emath::Rot2::from_angle(self.angle)) + .translate(center.to_vec2()) + } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 9529765ac..9256ae16e 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1546,6 +1546,7 @@ impl Tessellator { radius, fill, stroke, + angle, } = shape; if radius.x <= 0.0 || radius.y <= 0.0 { @@ -1596,6 +1597,14 @@ impl Tessellator { points.push(center + Vec2::new(0.0, -radius.y)); points.extend(quarter.iter().rev().map(|p| center + Vec2::new(p.x, -p.y))); + // Apply rotation if angle is non-zero + if angle != 0.0 { + let rot = emath::Rot2::from_angle(angle); + for point in &mut points { + *point = center + rot * (*point - center); + } + } + let path_stroke = PathStroke::from(stroke).outside(); self.scratchpad_path.clear(); self.scratchpad_path.add_line_loop(&points); @@ -1773,6 +1782,7 @@ impl Tessellator { round_to_pixels, mut blur_width, brush: _, // brush is extracted on its own, because it is not Copy + angle, } = *rect_shape; let mut corner_radius = CornerRadiusF32::from(corner_radius); @@ -1940,6 +1950,16 @@ impl Tessellator { let path = &mut self.scratchpad_path; path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius); + + // Apply rotation if angle is non-zero + if angle != 0.0 { + let rot = emath::Rot2::from_angle(angle); + let center = rect.center(); + for point in &mut self.scratchpad_points { + *point = center + rot * (*point - center); + } + } + path.add_line_loop(&self.scratchpad_points); let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind); diff --git a/tests/egui_tests/tests/snapshots/rotated_ellipse.png b/tests/egui_tests/tests/snapshots/rotated_ellipse.png new file mode 100644 index 000000000..e32f7864c --- /dev/null +++ b/tests/egui_tests/tests/snapshots/rotated_ellipse.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8f222733524b21969834a9ccc15aa9b0a4deb1d41e1086c80750f7cdd9711c8 +size 17324 diff --git a/tests/egui_tests/tests/snapshots/rotated_rect.png b/tests/egui_tests/tests/snapshots/rotated_rect.png new file mode 100644 index 000000000..52255aa7f --- /dev/null +++ b/tests/egui_tests/tests/snapshots/rotated_rect.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb4f1d10aa664e04da4b2e38c52cb6516a4c43a98884c9223e15266ea28ccd3d +size 14191 diff --git a/tests/egui_tests/tests/test_rotation.rs b/tests/egui_tests/tests/test_rotation.rs new file mode 100644 index 000000000..03c9a8664 --- /dev/null +++ b/tests/egui_tests/tests/test_rotation.rs @@ -0,0 +1,117 @@ +use egui::epaint::{EllipseShape, RectShape, StrokeKind}; +use egui::{Color32, Grid, Pos2, Rect, Shape, Stroke, Vec2}; +use egui_kittest::Harness; + +const SHAPE_COLOR: Color32 = Color32::from_rgb(255, 165, 0); +const GHOST_COLOR: Color32 = Color32::from_rgb(0, 255, 255); +const PIVOT_COLOR: Color32 = Color32::from_rgb(255, 0, 255); +const CELL_SIZE: Vec2 = Vec2::new(180.0, 180.0); + +#[test] +fn rotated_rect() { + let shape_stroke = Stroke::new(2.0, Color32::BLACK); + let ghost_stroke = Stroke::new(1.0, GHOST_COLOR); + + let mut harness = Harness::new_ui(|ui| { + ui.ctx().set_pixels_per_point(1.0); + + let rect_size = Vec2::new(100.0, 60.0); + let cell_center = Pos2::new(90.0, 90.0); + let cell_rect = Rect::from_center_size(cell_center, rect_size); + + Grid::new("rotated_rect_grid") + .spacing(Vec2::new(30.0, 30.0)) + .show(ui, |ui| { + for (label, angle, pivot) in [ + ("0°", 0.0, None), + ("Center 45°", 45.0f32.to_radians(), None), + ( + "Top-Left 45°", + 45.0f32.to_radians(), + Some(cell_rect.left_top()), + ), + ] { + paint_case(ui, label, |offset| { + let rect = cell_rect.translate(offset); + let pivot = pivot.map(|p| p + offset); + let pivot_pos = pivot.unwrap_or_else(|| rect.center()); + + let ghost = RectShape::stroke(rect, 0.0, ghost_stroke, StrokeKind::Outside); + let shape = RectShape::new( + rect, + 0.0, + SHAPE_COLOR, + shape_stroke, + StrokeKind::Outside, + ) + .with_angle_and_pivot(angle, pivot_pos); + + (ghost.into(), shape.into(), pivot_pos) + }); + } + }); + }); + + harness.fit_contents(); + harness.try_snapshot("rotated_rect").unwrap(); +} + +#[test] +fn rotated_ellipse() { + let shape_stroke = Stroke::new(2.0, Color32::BLACK); + let ghost_stroke = Stroke::new(1.0, GHOST_COLOR); + + let mut harness = Harness::new_ui(|ui| { + ui.ctx().set_pixels_per_point(1.0); + + let rect_size = Vec2::new(100.0, 60.0); + let cell_center = Pos2::new(90.0, 90.0); + let radius = rect_size / 2.0; + + Grid::new("rotated_ellipse_grid") + .spacing(Vec2::new(30.0, 30.0)) + .show(ui, |ui| { + for (label, angle, pivot) in [ + ("0°", 0.0, None), + ("Center 45°", 45.0f32.to_radians(), None), + ( + "Top-Left 45°", + 45.0f32.to_radians(), + Some(cell_center - radius), + ), + ] { + paint_case(ui, label, |offset| { + let center = cell_center + offset; + let pivot = pivot.map(|p| p + offset); + let pivot_pos = pivot.unwrap_or(center); + + let ghost = EllipseShape::stroke(center, radius, ghost_stroke); + let mut shape = EllipseShape::filled(center, radius, SHAPE_COLOR); + shape.stroke = shape_stroke; + let shape = shape.with_angle_and_pivot(angle, pivot_pos); + + (ghost.into(), shape.into(), pivot_pos) + }); + } + }); + }); + + harness.fit_contents(); + harness.try_snapshot("rotated_ellipse").unwrap(); +} + +fn paint_case(ui: &mut egui::Ui, label: &str, make_shapes: F) +where + F: FnOnce(Vec2) -> (Shape, Shape, Pos2), +{ + ui.vertical(|ui| { + ui.label(label); + let (response, painter) = ui.allocate_painter(CELL_SIZE, egui::Sense::hover()); + let offset = response.rect.min.to_vec2(); + + let (ghost, shape, pivot) = make_shapes(offset); + painter.add(ghost); + painter.add(shape); + painter.circle_filled(pivot, 3.0, PIVOT_COLOR); + }); +} From a12d18d9bdf79afcb669908d1c6119b1816f440c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 13:58:21 +0100 Subject: [PATCH 26/89] Replace `chrono` with `jiff` (#8008) `jiff` is more modern, and seem to be where the ecosystem is heading. --- Cargo.lock | 103 ++++-------------- Cargo.toml | 2 +- crates/egui_demo_app/Cargo.toml | 4 +- crates/egui_demo_app/src/lib.rs | 7 +- crates/egui_demo_lib/Cargo.toml | 4 +- .../egui_demo_lib/src/demo/widget_gallery.rs | 26 ++--- crates/egui_extras/Cargo.toml | 4 +- crates/egui_extras/src/datepicker/button.rs | 20 ++-- crates/egui_extras/src/datepicker/mod.rs | 20 ++-- crates/egui_extras/src/datepicker/popup.rs | 47 ++++---- crates/egui_extras/src/lib.rs | 4 +- examples/hello_android/Cargo.toml | 2 +- 12 files changed, 88 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 152d178b9..a15a00a02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,7 +96,7 @@ dependencies = [ "hashbrown 0.16.1", "static_assertions", "windows", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -732,19 +732,6 @@ dependencies = [ "libc", ] -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link 0.2.1", -] - [[package]] name = "ciborium" version = "0.2.2" @@ -1323,7 +1310,6 @@ dependencies = [ "accesskit", "accesskit_consumer", "bytemuck", - "chrono", "eframe", "egui", "egui_demo_lib", @@ -1332,6 +1318,7 @@ dependencies = [ "ehttp", "env_logger", "image", + "jiff", "log", "mimalloc", "poll-promise", @@ -1350,13 +1337,13 @@ dependencies = [ name = "egui_demo_lib" version = "0.33.3" dependencies = [ - "chrono", "criterion", "document-features", "egui", "egui_extras", "egui_kittest", "image", + "jiff", "mimalloc", "rand 0.9.2", "serde", @@ -1368,12 +1355,12 @@ name = "egui_extras" version = "0.33.3" dependencies = [ "ahash", - "chrono", "document-features", "egui", "ehttp", "enum-map", "image", + "jiff", "log", "mime_guess2", "profiling", @@ -2151,30 +2138,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.61.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -2387,22 +2350,25 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", + "js-sys", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", + "wasm-bindgen", + "windows-sys 0.61.2", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", @@ -5282,7 +5248,7 @@ dependencies = [ "wgpu-naga-bridge", "wgpu-types", "windows", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -5347,7 +5313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core 0.62.2", + "windows-core", "windows-future", "windows-numerics", ] @@ -5358,20 +5324,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-core", ] [[package]] @@ -5383,8 +5336,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-result", + "windows-strings", ] [[package]] @@ -5393,7 +5346,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core 0.62.2", + "windows-core", "windows-link 0.2.1", "windows-threading", ] @@ -5438,19 +5391,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core 0.62.2", + "windows-core", "windows-link 0.2.1", ] -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -5460,15 +5404,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-strings" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 4c8a52626..6978e4df8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,6 @@ arboard = { version = "3.6.1", default-features = false } backtrace = "0.3.76" bitflags = "2.9.4" bytemuck = "1.24.0" -chrono = { version = "0.4.42", default-features = false } cint = "0.3.1" color-hex = "0.2.0" criterion = { version = "0.7.0", default-features = false } @@ -96,6 +95,7 @@ glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25.6", default-features = false } +jiff = { version = "0.2.23", default-features = false } js-sys = "0.3.77" kittest = { version = "0.4.0" } log = { version = "0.4.28", features = ["std"] } diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index b23ea9cbb..49609746f 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -42,15 +42,15 @@ wayland = ["eframe/wayland"] x11 = ["eframe/x11"] [dependencies] -chrono = { workspace = true, features = ["js-sys", "wasmbind"] } eframe = { workspace = true, default-features = false, features = ["web_screen_reader"] } egui = { workspace = true, features = ["callstack", "default"] } -egui_demo_lib = { workspace = true, features = ["default", "chrono"] } +egui_demo_lib = { workspace = true, features = ["default", "jiff"] } egui_extras = { workspace = true, features = ["default", "image"] } image = { workspace = true, default-features = false, features = [ # Ensure we can display the test images "png", ] } +jiff = { workspace = true, features = ["std", "tz-system", "js"] } log.workspace = true profiling.workspace = true diff --git a/crates/egui_demo_app/src/lib.rs b/crates/egui_demo_app/src/lib.rs index ea30bda8d..45abccc7f 100644 --- a/crates/egui_demo_app/src/lib.rs +++ b/crates/egui_demo_app/src/lib.rs @@ -9,9 +9,10 @@ pub use wrap_app::{Anchor, WrapApp}; /// Time of day as seconds since midnight. Used for clock in demo app. pub(crate) fn seconds_since_midnight() -> f64 { - use chrono::Timelike as _; - let time = chrono::Local::now().time(); - time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64) + jiff::Zoned::now() + .time() + .duration_since(jiff::civil::Time::midnight()) + .as_secs_f64() } /// Trait that wraps different parts of the demo app. diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 4f3b853e0..dc57fb092 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -26,7 +26,7 @@ rustdoc-args = ["--generate-link-to-definition"] [features] default = [] -chrono = ["egui_extras/datepicker", "dep:chrono"] +jiff = ["egui_extras/datepicker", "dep:jiff"] ## Allow serialization using [`serde`](https://docs.rs/serde). serde = ["egui/serde", "dep:serde", "egui_extras/serde"] @@ -42,7 +42,7 @@ egui_extras = { workspace = true, features = ["image", "svg"] } unicode_names2.workspace = true # this old version has fewer dependencies #! ### Optional dependencies -chrono = { workspace = true, optional = true, features = ["js-sys", "wasmbind"] } +jiff = { workspace = true, optional = true, features = ["std", "js"] } ## Enable this when generating docs. document-features = { workspace = true, optional = true } serde = { workspace = true, optional = true } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 6e23fca92..ec5d5f3eb 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -19,11 +19,11 @@ pub struct WidgetGallery { color: egui::Color32, animate_progress_bar: bool, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] #[cfg_attr(feature = "serde", serde(skip))] - date: Option, + date: Option, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] with_date_button: bool, } @@ -39,19 +39,19 @@ impl Default for WidgetGallery { string: Default::default(), color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5), animate_progress_bar: false, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] date: None, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] with_date_button: true, } } } impl WidgetGallery { - #[allow(clippy::allow_attributes, unused_mut)] // if not chrono + #[allow(clippy::allow_attributes, unused_mut)] // if not jiff #[inline] pub fn with_date_button(mut self, _with_date_button: bool) -> Self { - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] { self.with_date_button = _with_date_button; } @@ -140,9 +140,9 @@ impl WidgetGallery { string, color, animate_progress_bar, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] date, - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] with_date_button, } = self; @@ -242,9 +242,9 @@ impl WidgetGallery { } ui.end_row(); - #[cfg(feature = "chrono")] + #[cfg(feature = "jiff")] if *with_date_button { - let date = date.get_or_insert_with(|| chrono::offset::Utc::now().date_naive()); + let date = date.get_or_insert_with(|| jiff::Zoned::now().date()); ui.add(doc_link_label_with_crate( "egui_extras", "DatePickerButton", @@ -302,7 +302,7 @@ fn doc_link_label_with_crate<'a>( } } -#[cfg(feature = "chrono")] +#[cfg(feature = "jiff")] #[cfg(test)] mod tests { use super::*; @@ -314,7 +314,7 @@ mod tests { pub fn should_match_screenshot() { let mut demo = WidgetGallery { // If we don't set a fixed date, the snapshot test will fail. - date: Some(chrono::NaiveDate::from_ymd_opt(2024, 1, 1).unwrap()), + date: Some(jiff::civil::date(2024, 1, 1)), ..Default::default() }; diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index b124148bc..944576f08 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -34,7 +34,7 @@ default = ["dep:mime_guess2"] all_loaders = ["file", "http", "image", "svg", "gif", "webp"] ## Enable [`DatePickerButton`] widget. -datepicker = ["chrono"] +datepicker = ["jiff"] ## Add support for loading images from `file://` URIs. file = ["dep:mime_guess2"] @@ -83,7 +83,7 @@ profiling.workspace = true serde = { workspace = true, optional = true } # Date operations needed for datepicker widget -chrono = { workspace = true, optional = true, features = ["clock", "js-sys", "std", "wasmbind"] } +jiff = { workspace = true, optional = true, features = ["std", "tz-system", "js"] } ## Enable this when generating docs. document-features = { workspace = true, optional = true } diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index d6f69fe77..692dc9d24 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -1,6 +1,6 @@ use super::popup::DatePickerPopup; -use chrono::NaiveDate; use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget}; +use jiff::civil::Date; use std::ops::RangeInclusive; #[derive(Default, Clone)] @@ -11,7 +11,7 @@ pub(crate) struct DatePickerButtonState { /// Shows a date, and will open a date picker popup when clicked. pub struct DatePickerButton<'a> { - selection: &'a mut NaiveDate, + selection: &'a mut Date, id_salt: Option<&'a str>, combo_boxes: bool, arrows: bool, @@ -20,13 +20,13 @@ pub struct DatePickerButton<'a> { show_icon: bool, format: String, highlight_weekends: bool, - start_end_years: Option>, + start_end_years: Option>, reverse_years: bool, - year_scroll_to: Option, + year_scroll_to: Option, } impl<'a> DatePickerButton<'a> { - pub fn new(selection: &'a mut NaiveDate) -> Self { + pub fn new(selection: &'a mut Date) -> Self { Self { selection, id_salt: None, @@ -95,7 +95,7 @@ impl<'a> DatePickerButton<'a> { } /// Change the format shown on the button. (Default: %Y-%m-%d) - /// See [`chrono::format::strftime`] for valid formats. + /// See [`jiff::fmt::strtime`] for valid formats. #[inline] pub fn format(mut self, format: impl Into) -> Self { self.format = format.into(); @@ -115,7 +115,7 @@ impl<'a> DatePickerButton<'a> { /// For example, if you want to provide the range of years from 2000 to 2035, you can use: /// `start_end_years(2000..=2035)`. #[inline] - pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self { + pub fn start_end_years(mut self, start_end_years: RangeInclusive) -> Self { self.start_end_years = Some(start_end_years); self } @@ -130,7 +130,7 @@ impl<'a> DatePickerButton<'a> { /// Scroll the year dropdown to this year when the picker first opens. /// Defaults to the currently selected year. #[inline] - pub fn year_scroll_to(mut self, year: i32) -> Self { + pub fn year_scroll_to(mut self, year: i16) -> Self { self.year_scroll_to = Some(year); self } @@ -144,9 +144,9 @@ impl Widget for DatePickerButton<'_> { .unwrap_or_default(); let mut text = if self.show_icon { - RichText::new(format!("{} šŸ“†", self.selection.format(&self.format))) + RichText::new(format!("{} šŸ“†", self.selection.strftime(&self.format))) } else { - RichText::new(format!("{}", self.selection.format(&self.format))) + RichText::new(format!("{}", self.selection.strftime(&self.format))) }; let visuals = ui.visuals().widgets.open; if button_state.picker_visible { diff --git a/crates/egui_extras/src/datepicker/mod.rs b/crates/egui_extras/src/datepicker/mod.rs index 7a114b357..f1f6e58fa 100644 --- a/crates/egui_extras/src/datepicker/mod.rs +++ b/crates/egui_extras/src/datepicker/mod.rs @@ -4,32 +4,32 @@ mod button; mod popup; pub use button::DatePickerButton; -use chrono::{Datelike as _, Duration, NaiveDate, Weekday}; +use jiff::civil::{Date, ISOWeekDate, Weekday}; #[derive(Debug)] struct Week { number: u8, - days: Vec, + days: Vec, } -fn month_data(year: i32, month: u32) -> Vec { - let first = NaiveDate::from_ymd_opt(year, month, 1).expect("Could not create NaiveDate"); +fn month_data(year: i16, month: i8) -> Vec { + let first = Date::new(year, month, 1).expect("Could not create Date"); let mut start = first; - while start.weekday() != Weekday::Mon { - start = start.checked_sub_signed(Duration::days(1)).unwrap(); + while start.weekday() != Weekday::Monday { + start = start.yesterday().unwrap(); } let mut weeks = vec![]; let mut week = vec![]; - while start < first || start.month() == first.month() || start.weekday() != Weekday::Mon { + while start < first || start.month() == first.month() || start.weekday() != Weekday::Monday { week.push(start); - if start.weekday() == Weekday::Sun { + if start.weekday() == Weekday::Sunday { weeks.push(Week { - number: start.iso_week().week() as u8, + number: ISOWeekDate::from(start).week() as u8, days: std::mem::take(&mut week), }); } - start = start.checked_add_signed(Duration::days(1)).unwrap(); + start = start.tomorrow().unwrap(); } weeks diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 1c24ca81d..5c0726e5a 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -1,4 +1,4 @@ -use chrono::{Datelike as _, NaiveDate, Weekday}; +use jiff::civil::{Date, Weekday}; use egui::{Align, Button, Color32, ComboBox, Direction, Id, Layout, RichText, Ui, Vec2}; @@ -9,43 +9,39 @@ use crate::{Column, Size, StripBuilder, TableBuilder}; #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct DatePickerPopupState { - year: i32, - month: u32, - day: u32, + year: i16, + month: i8, + day: i8, setup: bool, year_scroll_needed: bool, } impl DatePickerPopupState { - fn last_day_of_month(&self) -> u32 { - let date: NaiveDate = - NaiveDate::from_ymd_opt(self.year, self.month, 1).expect("Could not create NaiveDate"); - date.with_day(31) - .map(|_| 31) - .or_else(|| date.with_day(30).map(|_| 30)) - .or_else(|| date.with_day(29).map(|_| 29)) - .unwrap_or(28) + fn last_day_of_month(&self) -> i8 { + Date::new(self.year, self.month, 1) + .expect("Could not create Date") + .days_in_month() } } pub(crate) struct DatePickerPopup<'a> { - pub selection: &'a mut NaiveDate, + pub selection: &'a mut Date, pub button_id: Id, pub combo_boxes: bool, pub arrows: bool, pub calendar: bool, pub calendar_week: bool, pub highlight_weekends: bool, - pub start_end_years: Option>, + pub start_end_years: Option>, pub reverse_years: bool, - pub year_scroll_to: Option, + pub year_scroll_to: Option, } impl DatePickerPopup<'_> { /// Returns `true` if user pressed `Save` button. pub fn draw(&mut self, ui: &mut Ui) -> bool { let id = ui.make_persistent_id("date_picker"); - let today = chrono::offset::Utc::now().date_naive(); + let today = jiff::Zoned::now().date(); let mut popup_state = ui .data_mut(|data| data.get_persisted::(id)) .unwrap_or_default(); @@ -95,7 +91,7 @@ impl DatePickerPopup<'_> { }; let scroll_to_year = self.year_scroll_to.unwrap_or(popup_state.year); - let years: Vec = if self.reverse_years { + let years: Vec = if self.reverse_years { (start_year..=end_year).rev().collect() } else { (start_year..=end_year).collect() @@ -132,7 +128,7 @@ impl DatePickerPopup<'_> { ComboBox::from_id_salt("date_picker_month") .selected_text(month_name(popup_state.month)) .show_ui(ui, |ui| { - for month in 1..=12 { + for month in 1i8..=12 { if ui .selectable_value( &mut popup_state.month, @@ -156,7 +152,7 @@ impl DatePickerPopup<'_> { ComboBox::from_id_salt("date_picker_day") .selected_text(popup_state.day.to_string()) .show_ui(ui, |ui| { - for day in 1..=popup_state.last_day_of_month() { + for day in 1i8..=popup_state.last_day_of_month() { if ui .selectable_value( &mut popup_state.day, @@ -333,9 +329,10 @@ impl DatePickerPopup<'_> { && popup_state.day == day.day() { ui.visuals().selection.bg_fill - } else if (day.weekday() == Weekday::Sat - || day.weekday() == Weekday::Sun) - && self.highlight_weekends + } else if (matches!( + day.weekday(), + Weekday::Saturday | Weekday::Sunday + )) && self.highlight_weekends { if ui.visuals().dark_mode { Color32::DARK_RED @@ -414,12 +411,12 @@ impl DatePickerPopup<'_> { strip.cell(|ui| { ui.with_layout(Layout::top_down_justified(Align::Center), |ui| { if ui.button("Save").clicked() { - *self.selection = NaiveDate::from_ymd_opt( + *self.selection = Date::new( popup_state.year, popup_state.month, popup_state.day, ) - .expect("Could not create NaiveDate"); + .expect("Could not create Date"); saved = true; close = true; } @@ -442,7 +439,7 @@ impl DatePickerPopup<'_> { } } -fn month_name(i: u32) -> &'static str { +fn month_name(i: i8) -> &'static str { match i { 1 => "January", 2 => "February", diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index 19e0c95a8..99e6dd5e4 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -8,7 +8,7 @@ #![expect(clippy::manual_range_contains)] -#[cfg(feature = "chrono")] +#[cfg(feature = "datepicker")] mod datepicker; pub mod syntax_highlighting; @@ -21,7 +21,7 @@ mod sizing; mod strip; mod table; -#[cfg(feature = "chrono")] +#[cfg(feature = "datepicker")] pub use crate::datepicker::DatePickerButton; pub(crate) use crate::layout::StripLayout; diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml index 7c8444f99..ae484783c 100644 --- a/examples/hello_android/Cargo.toml +++ b/examples/hello_android/Cargo.toml @@ -22,7 +22,7 @@ eframe = { workspace = true, default-features = false, features = [ "glow", "android-native-activity", ] } -egui_demo_lib = { workspace = true, features = ["chrono"] } +egui_demo_lib = { workspace = true, features = ["jiff"] } # For image support: egui_extras = { workspace = true, features = ["default", "image"] } From 307202ab67a8459f1f9e924484b25275e1057596 Mon Sep 17 00:00:00 2001 From: Grrr <163682431+x4exr@users.noreply.github.com> Date: Tue, 24 Mar 2026 08:59:05 -0400 Subject: [PATCH 27/89] egui_wgpu, added disclaimer, discourages people calling render without `update_buffer` (#7971) * Closes * [x] I have followed the instructions in the PR template Co-authored-by: Emil Ernerfeldt --- crates/egui-wgpu/src/renderer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 640f33f51..e55f7581a 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -470,6 +470,9 @@ impl Renderer { /// The render pass internally keeps all referenced resources alive as long as necessary. /// The only consequence of `forget_lifetime` is that any operation on the parent encoder will cause a runtime error /// instead of a compile time error. + /// + /// # Panic + /// Always ensure that [`Renderer::update_buffers`] has been called otherwise calling [`Renderer::render`] will panic! pub fn render( &self, render_pass: &mut wgpu::RenderPass<'static>, From 8137aa350c2165d9824acb79d453150f18a3ebc6 Mon Sep 17 00:00:00 2001 From: Ellie High <6687206+wizzeh@users.noreply.github.com> Date: Tue, 24 Mar 2026 05:59:16 -0700 Subject: [PATCH 28/89] Allow fallback from smithay to arboard when getting clipboard (#7976) * [X] I have followed the instructions in the PR template Quick fix -- when the arboard and smithay features are both enabled, Clipboard::get returns early if it can't find a smithay clipboard. This PR just allows fallback to arboard instead of early-returning. --------- Co-authored-by: Emil Ernerfeldt --- crates/egui-winit/src/clipboard.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 75d0469ec..2410c3ee6 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -65,13 +65,12 @@ impl Clipboard { feature = "smithay-clipboard" ))] if let Some(clipboard) = &mut self.smithay { - return match clipboard.load() { - Ok(text) => Some(text), + match clipboard.load() { + Ok(text) => return Some(text), Err(err) => { log::error!("smithay paste error: {err}"); - None } - }; + } } #[cfg(all( From 405eb8157849eae446e10621ecc9bbefd25d9eed Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 24 Mar 2026 08:03:00 -0500 Subject: [PATCH 29/89] Fix menu keyboard toggle for open submenus (#7957) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary This PR fixes submenu keyboard parity: pressing Enter/Space on an already-open submenu button now collapses that submenu (matching top-level menu button behavior). What changed Updated submenu interaction logic to distinguish pointer primary clicks from keyboard/accessibility-triggered clicks. Kept pointer/touch behavior unchanged (submenu button clicks still don’t auto-close submenu). Added regression tests for: keyboard open of nested submenu, keyboard close (second Enter) of nested submenu, pointer clicks on submenu button keeping submenu open. Validation cargo test -p egui_kittest --test regression_tests Breaking changes None. Behavior change is limited to keyboard/accessibility activation of already-open submenu buttons. * Closes * [ X ] I have followed the instructions in the PR template --------- Co-authored-by: Lucas Meurer --- crates/egui/src/containers/menu.rs | 17 +++- crates/egui_kittest/tests/regression_tests.rs | 98 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index 1bd5954c8..cfdaac827 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -10,7 +10,7 @@ use crate::style::StyleModifier; use crate::{ - Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, Popup, + Button, Color32, Context, Frame, Id, InnerResponse, IntoAtoms, Layout, PointerButton, Popup, PopupCloseBehavior, Response, Style, Ui, UiBuilder, UiKind, UiStack, UiStackInfo, Widget as _, }; use emath::{Align, RectAlign, Vec2, vec2}; @@ -458,6 +458,7 @@ impl SubMenu { let is_any_open = open_item.is_some(); let mut is_open = open_item == Some(id); + let was_open = is_open; let mut set_open = None; // We expand the button rect so there is no empty space where no menu is shown @@ -470,9 +471,21 @@ impl SubMenu { // But since we check if no other menu is open, nothing should be able to cover the button let is_hovered = hover_pos.is_some_and(|pos| button_rect.contains(pos)); + // `clicked` includes keyboard and accessibility click actions. + // We want Enter/Space to toggle an already open submenu, while pointer clicks should keep + // the submenu open (for touch and pointer interactions). + let clicked = button_response.clicked(); + let clicked_by_pointer = button_response.clicked_by(PointerButton::Primary); + let clicked_by_keyboard_or_access = clicked && !clicked_by_pointer; + + if ui.is_enabled() && is_open && clicked_by_keyboard_or_access { + set_open = Some(false); + is_open = false; + } + // The clicked handler is there for accessibility (keyboard navigation) let should_open = - ui.is_enabled() && (button_response.clicked() || (is_hovered && !is_any_open)); + ui.is_enabled() && ((!was_open && clicked) || (is_hovered && !is_any_open)); if should_open { set_open = Some(true); is_open = true; diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 4289fe3a9..94617ff8b 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -262,3 +262,101 @@ pub fn menus_should_close_even_if_submenu_disappears() { ); } } + +fn keyboard_submenu_harness() -> Harness<'static, bool> { + Harness::builder() + .with_size(Vec2::new(400.0, 240.0)) + .build_ui_state( + |ui, checked| { + egui::Panel::top("menu_bar").show_inside(ui, |ui| { + egui::MenuBar::new().ui(ui, |ui| { + ui.menu_button("X", |ui| { + ui.menu_button("Y", |ui| { + ui.checkbox(checked, "Goal"); + }); + }); + }); + }); + }, + false, + ) +} + +#[test] +pub fn keyboard_should_open_nested_submenu() { + let mut harness = keyboard_submenu_harness(); + + harness.get_by_label("X").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + harness.get_by_label_contains("Y").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected nested submenu to open via keyboard" + ); +} + +#[test] +pub fn keyboard_should_close_nested_submenu_with_second_enter() { + let mut harness = keyboard_submenu_harness(); + + harness.get_by_label("X").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + harness.get_by_label_contains("Y").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected nested submenu to open before close attempt" + ); + + harness.get_by_label_contains("Y").focus(); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_none(), + "Expected nested submenu to close when pressing Enter again" + ); +} + +#[test] +pub fn pointer_click_on_open_submenu_button_should_not_close_it() { + let mut harness = keyboard_submenu_harness(); + + harness.get_by_label("X").click(); + harness.run(); + + harness.get_by_label_contains("Y").click(); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected submenu to remain open after pointer click on its button" + ); + + harness.get_by_label_contains("Y").click(); + harness.run(); + + assert!( + harness.query_by_label("Goal").is_some(), + "Expected submenu to remain open on repeated pointer click" + ); +} From 5ed92c3011907df59a0f6d6aa5e5479cbef84d30 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:03:20 +0900 Subject: [PATCH 30/89] Add `Button::left_text` (#7955) feat: Add left_text() to egui::Button This PR introduces the `left_text()` method to `egui::Button`. It enables placing additional text content on the left side of the button's primary label, which is useful for displaying auxiliary information, labels, and for facilitating left-aligned text within the button. ```rust let is_selected = true; let selectable_label_widget = egui::Button::selectable( is_selected, "", ).left_text("Left"); let desired_width = ui.available_width(); let desired_height = ui.spacing().interact_size.y; interaction_response = ui.add_sized( egui::vec2(desired_width, desired_height), selectable_label_widget, ); ``` --- crates/egui/src/widgets/button.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 7ad155f16..7d9dddf0d 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -240,6 +240,18 @@ impl<'a> Button<'a> { self } + /// Show some text on the left side of the button. + #[inline] + pub fn left_text(mut self, left_text: impl IntoAtoms<'a>) -> Self { + self.layout.push_left(Atom::grow()); + + for atom in left_text.into_atoms() { + self.layout.push_left(atom); + } + + self + } + /// Show some text on the right side of the button. #[inline] pub fn right_text(mut self, right_text: impl IntoAtoms<'a>) -> Self { From a1af9abe70ad3a1c049d8d9968886af483af666a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 15:46:56 +0100 Subject: [PATCH 31/89] Shrink the byte-size of `Response` slightly (#8011) Small optimization! 96 -> 88 bytes, so no huge win. --- crates/egui/src/atomics/atom_layout.rs | 5 ++-- crates/egui/src/context.rs | 15 ++++------ crates/egui/src/response.rs | 41 ++++++++++++++++++++++---- crates/egui/src/ui.rs | 2 +- crates/egui/src/widgets/label.rs | 4 +-- crates/emath/src/pos2.rs | 5 ++++ tests/egui_tests/tests/test_atoms.rs | 8 ++--- 7 files changed, 57 insertions(+), 23 deletions(-) diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index c408146d6..7894273f3 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -318,8 +318,9 @@ impl<'a> AtomLayout<'a> { let (_, rect) = ui.allocate_space(frame_size); let mut response = ui.interact(rect, id, sense); - response.intrinsic_size = - Some((Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size)); + response.set_intrinsic_size( + (Vec2::new(intrinsic_width, intrinsic_height) + margin.sum()).at_least(min_size), + ); AllocatedAtomLayout { sized_atoms: sized_items, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index fbc189132..af6c0e6d0 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1378,8 +1378,8 @@ impl Context { interact_rect, sense, flags: Flags::empty(), - interact_pointer_pos: None, - intrinsic_size: None, + interact_pointer_pos_or_nan: Pos2::NAN, + intrinsic_size_or_nan: Vec2::NAN, }; res.flags.set(Flags::ENABLED, enabled); @@ -1470,14 +1470,11 @@ impl Context { || res.long_touched() || clicked || res.drag_stopped(); - if is_interacted_with { - res.interact_pointer_pos = input.pointer.interact_pos(); - if let (Some(to_global), Some(pos)) = ( - memory.to_global.get(&res.layer_id), - &mut res.interact_pointer_pos, - ) { - *pos = to_global.inverse() * *pos; + if is_interacted_with && let Some(mut pos) = input.pointer.interact_pos() { + if let Some(to_global) = memory.to_global.get(&res.layer_id) { + pos = to_global.inverse() * pos; } + res.interact_pointer_pos_or_nan = pos; } if input.pointer.any_down() && !is_interacted_with { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 495d5dcdf..241f5a381 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -55,7 +55,7 @@ pub struct Response { /// Where the pointer (mouse/touch) were when this widget was clicked or dragged. /// `None` if the widget is not being interacted with. #[doc(hidden)] - pub interact_pointer_pos: Option, + pub interact_pointer_pos_or_nan: Pos2, /// The intrinsic / desired size of the widget. /// @@ -67,12 +67,22 @@ pub struct Response { /// At the time of writing, this is only used by external crates /// for improved layouting. /// See for instance [`egui_flex`](https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_flex). - pub intrinsic_size: Option, + #[doc(hidden)] + pub intrinsic_size_or_nan: Vec2, #[doc(hidden)] pub flags: Flags, } +#[test] +fn test_response_size() { + assert_eq!( + std::mem::size_of::(), + 88, + "Keep Response small, because we create them often, and we want to keep it lean and fast" + ); +} + /// A bit set for various boolean properties of `Response`. #[doc(hidden)] #[derive(Copy, Clone, Debug)] @@ -489,7 +499,26 @@ impl Response { /// `None` if the widget is not being interacted with. #[inline] pub fn interact_pointer_pos(&self) -> Option { - self.interact_pointer_pos + let pos = self.interact_pointer_pos_or_nan; + if pos.any_nan() { None } else { Some(pos) } + } + + /// The intrinsic / desired size of the widget. + /// + /// This is the size that a non-wrapped, non-truncated, non-justified version of the widget + /// would have. + /// + /// If this is `None`, use [`Self::rect`] instead. + #[inline] + pub fn intrinsic_size(&self) -> Option { + let size = self.intrinsic_size_or_nan; + if size.any_nan() { None } else { Some(size) } + } + + /// Set the intrinsic / desired size of the widget. + #[inline] + pub fn set_intrinsic_size(&mut self, size: Vec2) { + self.intrinsic_size_or_nan = size; } /// If it is a good idea to show a tooltip, where is pointer? @@ -1007,8 +1036,10 @@ impl Response { interact_rect: self.interact_rect.union(other.interact_rect), sense: self.sense.union(other.sense), flags: self.flags | other.flags, - interact_pointer_pos: self.interact_pointer_pos.or(other.interact_pointer_pos), - intrinsic_size: None, + interact_pointer_pos_or_nan: self + .interact_pointer_pos() + .unwrap_or(other.interact_pointer_pos_or_nan), + intrinsic_size_or_nan: Vec2::NAN, } } } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index daabd10b0..f0b270951 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1281,7 +1281,7 @@ impl Ui { pub fn allocate_response(&mut self, desired_size: Vec2, sense: Sense) -> Response { let (id, rect) = self.allocate_space(desired_size); let mut response = self.interact(rect, id, sense); - response.intrinsic_size = Some(desired_size); + response.set_intrinsic_size(desired_size); response } diff --git a/crates/egui/src/widgets/label.rs b/crates/egui/src/widgets/label.rs index 284cfd12c..7b2d3a3ba 100644 --- a/crates/egui/src/widgets/label.rs +++ b/crates/egui/src/widgets/label.rs @@ -220,7 +220,7 @@ impl Label { .rect_without_leading_space() .translate(pos.to_vec2()); let mut response = ui.allocate_rect(rect, sense); - response.intrinsic_size = Some(galley.intrinsic_size()); + response.set_intrinsic_size(galley.intrinsic_size()); for placed_row in galley.rows.iter().skip(1) { let rect = placed_row.rect().translate(pos.to_vec2()); response |= ui.allocate_rect(rect, sense); @@ -256,7 +256,7 @@ impl Label { let galley = ui.fonts_mut(|fonts| fonts.layout_job(layout_job)); let (rect, mut response) = ui.allocate_exact_size(galley.size(), sense); - response.intrinsic_size = Some(galley.intrinsic_size()); + response.set_intrinsic_size(galley.intrinsic_size()); let galley_pos = match galley.job.halign { Align::LEFT => rect.left_top(), Align::Center => rect.center_top(), diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index fc26686b3..f67767e6b 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -119,6 +119,11 @@ impl Pos2 { /// Same as `Pos2::default()`. pub const ZERO: Self = Self { x: 0.0, y: 0.0 }; + pub const NAN: Self = Self { + x: f32::NAN, + y: f32::NAN, + }; + #[inline(always)] pub const fn new(x: f32, y: f32) -> Self { Self { x, y } diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index f6e9df14a..f7e0a4af1 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -92,17 +92,17 @@ fn test_intrinsic_size() { if let Some(current_intrinsic_size) = intrinsic_size { assert_eq!( Some(current_intrinsic_size), - response.intrinsic_size, + response.intrinsic_size(), "For wrapping: {wrapping:?}" ); } assert!( - response.intrinsic_size.is_some(), + response.intrinsic_size().is_some(), "intrinsic_size should be set for `Button`" ); - intrinsic_size = response.intrinsic_size; + intrinsic_size = response.intrinsic_size(); if wrapping == TextWrapMode::Extend { - assert_eq!(Some(response.rect.size()), response.intrinsic_size); + assert_eq!(Some(response.rect.size()), response.intrinsic_size()); } }); } From 96cae39fb8883065e7460e8ce8780a15a489b345 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 24 Mar 2026 15:59:53 +0100 Subject: [PATCH 32/89] Update a couple of dependencies (#8009) * Closes https://github.com/emilk/egui/pull/7992 --- Cargo.lock | 78 +++++++++++++++++----------- Cargo.toml | 14 ++--- crates/eframe/Cargo.toml | 3 ++ crates/eframe/src/native/app_icon.rs | 2 +- crates/egui-winit/Cargo.toml | 1 + crates/egui-winit/src/safe_area.rs | 4 +- 6 files changed, 63 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a15a00a02..981e7c475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,7 +281,7 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", @@ -590,7 +590,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1147,7 +1147,7 @@ dependencies = [ "bitflags 2.9.4", "block2 0.6.2", "libc", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -1222,9 +1222,9 @@ dependencies = [ "image", "js-sys", "log", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", "pollster", @@ -1290,9 +1290,9 @@ dependencies = [ "document-features", "egui", "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", "profiling", "raw-window-handle", "serde", @@ -1940,7 +1940,7 @@ dependencies = [ "glutin_glx_sys", "glutin_wgl_sys", "libloading", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -2800,9 +2800,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -2831,7 +2831,7 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.4", "block2 0.6.2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", "objc2-foundation 0.3.2", @@ -2881,7 +2881,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", ] [[package]] @@ -2892,7 +2892,7 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] @@ -2947,7 +2947,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.3", + "block2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2958,7 +2959,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -2994,7 +2995,7 @@ checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ "bitflags 2.9.4", "block2 0.6.2", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -3018,7 +3019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -3055,6 +3056,18 @@ dependencies = [ "objc2-user-notifications", ] +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-uniform-type-identifiers" version = "0.2.2" @@ -3652,7 +3665,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" dependencies = [ - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.2", @@ -3777,7 +3790,7 @@ dependencies = [ "js-sys", "libc", "log", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -3818,14 +3831,15 @@ dependencies = [ [[package]] name = "ron" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "base64", "bitflags 2.9.4", + "once_cell", "serde", "serde_derive", + "typeid", "unicode-ident", ] @@ -4621,6 +4635,12 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "uds_windows" version = "1.1.0" @@ -5077,7 +5097,7 @@ dependencies = [ "jni", "log", "ndk-context", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-foundation 0.3.2", "url", "web-sys", @@ -5225,7 +5245,7 @@ dependencies = [ "log", "naga", "ndk-sys", - "objc2 0.6.3", + "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", "objc2-metal 0.3.2", @@ -5655,9 +5675,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" -version = "0.30.12" +version = "0.30.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" dependencies = [ "ahash", "android-activity", @@ -5679,7 +5699,7 @@ dependencies = [ "objc2 0.5.2", "objc2-app-kit 0.2.2", "objc2-foundation 0.2.2", - "objc2-ui-kit", + "objc2-ui-kit 0.2.2", "orbclient", "percent-encoding", "pin-project", diff --git a/Cargo.toml b/Cargo.toml index 6978e4df8..cf631eb25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,10 +104,10 @@ mimalloc = "0.1.48" mime_guess2 = { version = "2.3.1", default-features = false } mint = "0.5.9" nohash-hasher = "0.2.0" -objc2 = "0.5.2" -objc2-app-kit = { version = "0.2.2", default-features = false } -objc2-foundation = { version = "0.2.2", default-features = false } -objc2-ui-kit = { version = "0.2.2", default-features = false } +objc2 = "0.6.4" +objc2-app-kit = { version = "0.3.2", default-features = false } +objc2-foundation = { version = "0.3.2", default-features = false } +objc2-ui-kit = { version = "0.3.2", default-features = false } open = "5.3.2" parking_lot = "0.12.5" percent-encoding = "2.3.2" @@ -121,7 +121,7 @@ raw-window-handle = "0.6.2" rayon = "1.11.0" resvg = { version = "0.45.1", default-features = false } rfd = "0.17.2" -ron = "0.11.0" +ron = "0.12.0" self_cell = "1.2.1" serde = { version = "1.0.228", features = ["derive"] } similar-asserts = "1.7.0" @@ -133,7 +133,7 @@ syntect = { version = "5.3.0", default-features = false } tempfile = "3.23.0" thiserror = "2.0.17" tokio = "1.49" -toml = {version = "1", default-features = false } +toml = {version = "1.0.0", default-features = false } type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-segmentation = "1.12.0" @@ -146,7 +146,7 @@ web-time = "1.1.0" # Timekeeping for native and web webbrowser = "1.0.5" wgpu = { version = "29.0.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" -winit = { version = "0.30.12", default-features = false } +winit = { version = "0.30.13", default-features = false } [workspace.lints.rust] unsafe_code = "deny" diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 376f2ecbb..6219b90fd 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -169,7 +169,10 @@ objc2-foundation = { workspace = true, default-features = false, features = [ objc2-app-kit = { workspace = true, default-features = false, features = [ "std", "NSApplication", + "NSBitmapImageRep", + "NSGraphics", "NSImage", + "NSImageRep", "NSMenu", "NSMenuItem", "NSResponder", diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 3ac61d8e6..85be6754b 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -204,7 +204,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS use crate::icon_data::IconDataExt as _; profiling::function_scope!(); - use objc2::ClassType as _; + use objc2::AnyThread as _; use objc2_app_kit::{NSApplication, NSImage}; use objc2_foundation::NSString; diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index bb3576a2d..dd4aa8f9d 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -81,6 +81,7 @@ objc2.workspace = true objc2-foundation = { workspace = true, features = ["std", "NSThread"] } objc2-ui-kit = { workspace = true, features = [ "std", + "objc2-core-foundation", "UIApplication", "UIGeometry", "UIResponder", diff --git a/crates/egui-winit/src/safe_area.rs b/crates/egui-winit/src/safe_area.rs index 5f4a9f9cf..378f44a94 100644 --- a/crates/egui-winit/src/safe_area.rs +++ b/crates/egui-winit/src/safe_area.rs @@ -36,8 +36,8 @@ mod ios { | UISceneActivationState::ForegroundInactive ) { - // Safe to cast, the class kind was checked above - let window_scene = Retained::cast::(scene.clone()); + // SAFETY: class kind was checked above with `isKindOfClass` + let window_scene = Retained::cast_unchecked::(scene.clone()); if let Some(window) = window_scene.keyWindow() { let insets = window.safeAreaInsets(); return SafeAreaInsets(MarginF32 { From cd3c38cf2a0d65330a645faeadb6f666bc8df8f2 Mon Sep 17 00:00:00 2001 From: Gautier Cailly <109429289+gcailly@users.noreply.github.com> Date: Tue, 24 Mar 2026 16:20:57 +0100 Subject: [PATCH 33/89] Improve behavior of invisible windows (#7905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Closes #5229 * Closes #7776 On Windows, once a window is hidden with `ViewportCommand::Visible(false)`, two problems occur: 1. **Window can never be shown again** — Windows stops sending `RedrawRequested` events to invisible windows, and viewport commands are only processed during `run_ui_and_paint`, which is triggered by `RedrawRequested`. This creates a deadlock: ``` Visible(false) → window hidden → no RedrawRequested → run_ui_and_paint never called → Visible(true) stuck in queue → window stays hidden forever ``` 2. **High CPU usage** — The event loop spins at full speed with `ControlFlow::Poll` even for invisible windows, and repaint requests are scheduled immediately, causing a tight loop that burns CPU. ## Fix **For #5229:** In `check_redraw_requests`, after calling `window.request_redraw()`, detect invisible windows via `window.is_visible() == Some(false)` and call `run_ui_and_paint` directly for them. This ensures pending viewport commands (including `Visible(true)`) are still processed even when the OS doesn't send redraw events. **For #7776:** Three layers of throttling for invisible windows: - **Heartbeat scheduling:** After painting an invisible window, schedule the next repaint 100ms in the future (instead of immediately). This keeps viewport commands flowing while limiting to ~10 repaints/sec. - **Event throttling:** In `user_event`, throttle `RequestRepaint` events for invisible windows to at least 100ms delay, preventing egui's repaint callback from bypassing the heartbeat. - **ControlFlow fix:** Only set `ControlFlow::Poll` for visible windows. Invisible windows use `WaitUntil` instead of spinning. - **Backend sleep:** Add `is_visible() == Some(false)` alongside the existing `is_minimized()` sleep check in both wgpu and glow backends (defense in depth). The fix is platform-agnostic: `is_visible()` returns `Some(false)` only when the platform can confirm invisibility, so it won't trigger on platforms where invisible windows still receive `RedrawRequested`. ## Test plan - [x] `cargo fmt` passes - [x] `cargo clippy -p eframe --all-features` passes with no warnings - [x] Manual test on Windows: window reappears after `Visible(true)` when hidden - [x] Manual test on Windows: CPU stays near 0% while window is invisible (was ~16% before fix) --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/native/glow_integration.rs | 6 +- crates/eframe/src/native/run.rs | 64 +++++++++++++++++-- crates/eframe/src/native/wgpu_integration.rs | 9 ++- crates/eframe/src/native/winit_integration.rs | 8 +++ 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 724ddc6d5..37e3faa69 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -34,7 +34,7 @@ use egui_winit::accesskit_winit; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::epi_integration::EpiIntegration, + native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized}, }; use super::{ @@ -761,9 +761,11 @@ impl GlowWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), Some(&window)); - if window.is_minimized() == Some(true) { + if is_invisible_or_minimized(&window) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 + // On Windows, an invisible window also uses up all CPU: + // https://github.com/emilk/egui/issues/7776 profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 0597d318c..73b58ae61 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,4 +1,4 @@ -use std::time::Instant; +use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, @@ -11,9 +11,20 @@ use ahash::HashMap; use super::winit_integration::{UserEvent, WinitApp}; use crate::{ Result, epi, - native::{event_loop_context, winit_integration::EventResult}, + native::{ + event_loop_context, + winit_integration::{EventResult, is_invisible_or_minimized}, + }, }; +/// Minimum interval between repaints for invisible windows. +/// +/// On Windows, invisible windows don't receive `RedrawRequested` events, +/// so we throttle their repaints to avoid busy-looping while still +/// processing viewport commands like `Visible(true)`. +/// See . +const INVISIBLE_WINDOW_REPAINT_INTERVAL: Duration = Duration::from_millis(100); + // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { #[cfg(target_os = "android")] @@ -177,23 +188,54 @@ impl WinitAppWrapper { fn check_redraw_requests(&mut self, event_loop: &ActiveEventLoop) { let now = Instant::now(); + let mut invisible_window_ids = Vec::new(); + self.windows_next_repaint_times .retain(|window_id, repaint_time| { if now < *repaint_time { return true; // not yet ready } - event_loop.set_control_flow(ControlFlow::Poll); - if let Some(window) = self.winit_app.window(*window_id) { - log::trace!("request_redraw for {window_id:?}"); - window.request_redraw(); + // On Windows, invisible windows don't receive RedrawRequested + // events, so pending viewport commands (e.g. Visible(true)) would + // never be processed. We collect these windows to paint them + // directly below. + // See: https://github.com/emilk/egui/issues/5229 + if is_invisible_or_minimized(&window) { + invisible_window_ids.push(*window_id); + } else { + log::trace!("request_redraw for {window_id:?}"); + event_loop.set_control_flow(ControlFlow::Poll); + window.request_redraw(); + } } else { log::trace!("No window found for {window_id:?}"); } false }); + // Paint invisible windows directly, since they won't receive + // RedrawRequested events on Windows. This ensures that viewport + // commands like Visible(true) are still processed. + for window_id in &invisible_window_ids { + let event_result = self.winit_app.run_ui_and_paint(event_loop, *window_id); + self.handle_event_result(event_loop, event_result); + } + + // Throttle any already-scheduled repaints for invisible windows + // to avoid busy-looping. If no repaint was requested by the app, + // the window will simply sleep. + // See: https://github.com/emilk/egui/issues/7776 + if !invisible_window_ids.is_empty() { + let next_paint = Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL; + for window_id in &invisible_window_ids { + self.windows_next_repaint_times + .entry(*window_id) + .and_modify(|t| *t = (*t).min(next_paint)); + } + } + let next_repaint_time = self.windows_next_repaint_times.values().min().copied(); if let Some(next_repaint_time) = next_repaint_time { event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); @@ -270,6 +312,16 @@ impl ApplicationHandler for WinitAppWrapper { if let Some(window_id) = self.winit_app.window_id_from_viewport_id(viewport_id) { + // Throttle repaints for invisible windows to prevent + // high CPU usage on Windows. + // See: https://github.com/emilk/egui/issues/7776 + let when = if let Some(window) = self.winit_app.window(window_id) + && is_invisible_or_minimized(&window) + { + when.max(Instant::now() + INVISIBLE_WINDOW_REPAINT_INTERVAL) + } else { + when + }; Ok(EventResult::RepaintAt(window_id, when)) } else { Ok(EventResult::Wait) diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index ea96a1845..6d300d513 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -27,7 +27,10 @@ use winit_integration::UserEvent; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::{epi_integration::EpiIntegration, winit_integration::EventResult}, + native::{ + epi_integration::EpiIntegration, + winit_integration::{EventResult, is_invisible_or_minimized}, + }, }; use super::{epi_integration, event_loop_context, winit_integration, winit_integration::WinitApp}; @@ -778,10 +781,12 @@ impl WgpuWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); if let Some(window) = window - && window.is_minimized() == Some(true) + && is_invisible_or_minimized(window) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 + // On Windows, an invisible window also uses up all CPU: + // https://github.com/emilk/egui/issues/7776 profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 012c22f8e..b4ec62c09 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -9,6 +9,14 @@ use egui::ViewportId; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; +/// Returns `true` if the window is invisible or minimized. +/// +/// These windows don't receive `RedrawRequested` events on Windows, +/// so they need special handling to keep processing viewport commands. +pub fn is_invisible_or_minimized(window: &Window) -> bool { + window.is_visible() == Some(false) || window.is_minimized() == Some(true) +} + /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { profiling::function_scope!(); From 0d065f9e78ae611ea57ff4159fcb74f186731cb6 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 16:22:44 +0100 Subject: [PATCH 34/89] Add `Response::parent_id` and improve `warn_if_rect_changes_id` (#8010) Reduces the amount of false positives --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/containers/area.rs | 1 + crates/egui/src/containers/window.rs | 1 + crates/egui/src/context.rs | 10 +++++ crates/egui/src/hit_test.rs | 1 + crates/egui/src/response.rs | 17 +++++++++ crates/egui/src/style.rs | 3 +- crates/egui/src/ui.rs | 4 ++ crates/egui/src/widget_rect.rs | 8 ++++ tests/egui_tests/tests/regression_tests.rs | 43 ++++++++++++++++++++++ 9 files changed, 87 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4333cf73a..d44c0ae41 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -516,6 +516,7 @@ impl Area { let move_response = ctx.create_widget( WidgetRect { id: interact_id, + parent_id: id, layer_id, rect: state.rect(), interact_rect: state.rect().intersect(constrain_rect), diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index c6b739589..ae5fbfc8f 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -962,6 +962,7 @@ fn do_resize_interaction( WidgetRect { layer_id, id, + parent_id: layer_id.id, rect, interact_rect: rect, sense: Sense::DRAG, // Don't use Sense::drag() since we don't want these to be focusable diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index af6c0e6d0..51a663ce5 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1360,6 +1360,7 @@ impl Context { let WidgetRect { id, + parent_id: _, layer_id, rect, interact_rect, @@ -4324,6 +4325,15 @@ fn warn_if_rect_changes_id( continue; } + // Only warn if at least one widget has the same parent_id in both frames. + // If all parent_ids changed too, this is a cascading id shift, not a widget bug. + if !prev_at_rect + .iter() + .any(|pw| new_at_rect.iter().any(|nw| nw.parent_id == pw.parent_id)) + { + continue; + } + let rect = new_at_rect[0].rect; log::warn!( diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 8fe962b36..c7ffd7cda 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -450,6 +450,7 @@ mod tests { fn wr(id: Id, sense: Sense, rect: Rect) -> WidgetRect { WidgetRect { id, + parent_id: Id::NULL, layer_id: LayerId::background(), rect, interact_rect: rect, diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 241f5a381..a0dd6bd91 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -151,6 +151,22 @@ bitflags::bitflags! { } impl Response { + /// The [`Id`] of the parent [`crate::Ui`] that hosts this widget. + /// + /// Looks up the [`WidgetRect`] from the current (or previous) pass. + pub fn parent_id(&self) -> Id { + let id = self.ctx.viewport(|viewport| { + viewport + .this_pass + .widgets + .get(self.id) + .or_else(|| viewport.prev_pass.widgets.get(self.id)) + .map(|w| w.parent_id) + }); + debug_assert!(id.is_some(), "WidgetRect for Response not found!"); + id.unwrap_or(Id::NULL) + } + /// Returns true if this widget was clicked this frame by the primary button. /// /// A click is registered when the mouse or touch is released within @@ -761,6 +777,7 @@ impl Response { WidgetRect { layer_id: self.layer_id, id: self.id, + parent_id: self.parent_id(), rect: self.rect, interact_rect: self.interact_rect, sense: self.sense | sense, diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 5cd980f6c..4f9749663 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1303,7 +1303,8 @@ pub struct DebugOptions { /// Show interesting widgets under the mouse cursor. pub show_widget_hits: bool, - /// Show a warning if the same `Rect` had different `Id` on the previous frame. + /// Show a warning if the same `Rect` had different `Id` and the same parent `Id` on the + /// previous frame. pub warn_if_rect_changes_id: bool, /// If true, highlight widgets that are not aligned to [`emath::GUI_ROUNDING`]. diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index f0b270951..d55f4174f 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -173,6 +173,7 @@ impl Ui { ui.ctx().create_widget( WidgetRect { id: ui.unique_id, + parent_id: ui.id, layer_id: ui.layer_id(), rect: start_rect, interact_rect: start_rect, @@ -339,6 +340,7 @@ impl Ui { child_ui.ctx().create_widget( WidgetRect { id: child_ui.unique_id, + parent_id: self.id, layer_id: child_ui.layer_id(), rect: start_rect, interact_rect: start_rect, @@ -1043,6 +1045,7 @@ impl Ui { self.ctx().create_widget( WidgetRect { id, + parent_id: self.id, layer_id: self.layer_id(), rect, interact_rect: self.clip_rect().intersect(rect), @@ -1112,6 +1115,7 @@ impl Ui { let mut response = self.ctx().create_widget( WidgetRect { id: self.unique_id, + parent_id: self.id, layer_id: self.layer_id(), rect: self.min_rect(), interact_rect: self.clip_rect().intersect(self.min_rect()), diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index a84dde519..b6fc9f7bb 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -17,6 +17,12 @@ pub struct WidgetRect { /// You can ensure globally unique ids using [`crate::Ui::push_id`]. pub id: Id, + /// The [`Id`] of the parent [`crate::Ui`] that hosts this widget. + /// + /// Used by debug checks to distinguish true id-instability from + /// cascading id shifts caused by a parent Ui's auto-id changing. + pub parent_id: Id, + /// What layer the widget is on. pub layer_id: LayerId, @@ -46,6 +52,7 @@ impl WidgetRect { pub fn transform(self, transform: emath::TSTransform) -> Self { let Self { id, + parent_id, layer_id, rect, interact_rect, @@ -54,6 +61,7 @@ impl WidgetRect { } = self; Self { id, + parent_id, layer_id, rect: transform * rect, interact_rect: transform * interact_rect, diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 2d6ab5c67..f32ff7ff3 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -280,6 +280,49 @@ fn warn_if_rect_changes_id() { ); } +/// When a parent Ui's id changes (e.g. via `push_id` with a dynamic value), +/// all child widget ids shift too. This should NOT trigger `warn_if_rect_changes_id` because the +/// `parent_id` also changed — it's a cascading id shift, not a widget bug. +#[test] +fn warn_if_rect_changes_id_false_positive_parent_shift() { + use std::cell::Cell; + + let counter = Cell::new(0); + let button_rect = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(100.0, 30.0)); + + let mut harness = Harness::builder().with_size((200.0, 100.0)).build_ui(|ui| { + // push_id with a changing value causes the child Ui's id to shift, + // which in turn shifts all widget ids inside it. + ui.push_id(counter.get(), |ui| { + let id = ui.id().with("my_widget"); + let _response = ui.interact(button_rect, id, Sense::click()); + }); + }); + + // Frame 1: counter=0 — establishes prev_pass + harness.step(); + assert!( + !has_red_warning_rect(harness.output()), + "Should not warn on first frame" + ); + + // Frame 2: counter=0 — prev_pass == this_pass + harness.step(); + assert!( + !has_red_warning_rect(harness.output()), + "Should not warn when nothing changed" + ); + + // Now change the parent id, shifting all child widget ids + counter.set(1); + harness.step(); + + assert!( + !has_red_warning_rect(harness.output()), + "Should NOT warn when parent Ui's id shifted (cascading id change)" + ); +} + #[test] fn horizontal_wrapped_multiline_row_height() { let mut harness = Harness::builder().with_size((350.0, 300.0)).build_ui(|ui| { From 4feac890aa34b45e36c3ba8ab3a6e9296dade25f Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 24 Mar 2026 17:33:20 +0100 Subject: [PATCH 35/89] Respect `WidgetVisuals::expansion` in TextEdit (#8013) This broke in #7587 --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/text_edit/builder.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 1f103d2f8..ef668a02e 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -707,7 +707,11 @@ impl TextEdit<'_> { .frame .fill(background_color) .corner_radius(corner_radius) - .inner_margin(allocated.frame.inner_margin - Margin::same(stroke.width as i8)) + .inner_margin( + allocated.frame.inner_margin + + Margin::same((visuals.expansion - stroke.width).round() as i8), + ) + .outer_margin(Margin::same(-(visuals.expansion as i8))) .stroke(stroke) } else { allocated.frame From 2ccc8e8baba10af2a4153ed1813e18816a7c62c1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 09:21:39 +0100 Subject: [PATCH 36/89] Add `Context::text_edit_focused` (#8014) --- crates/egui/src/context.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 51a663ce5..a9151f9cd 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1933,7 +1933,7 @@ impl Context { } } -/// Callbacks +/// Plugins impl Context { /// Call the given callback at the start of each pass of each viewport. /// @@ -2945,6 +2945,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, From 12b504563307b6bc7e083f0b2b4dc0b88e5e4d04 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 10:11:21 +0100 Subject: [PATCH 37/89] Add `Context::time` (#8017) --- crates/egui/src/context.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index a9151f9cd..a8751bffc 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -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 From bfbf23b4fb05795f9f51768f8258ec6898f5c3f7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 10:11:30 +0100 Subject: [PATCH 38/89] Add `Ui::is_tooltip` (#8016) --- crates/egui/src/ui.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index d55f4174f..0caf7bec5 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -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 { From 0887b54c93b19aa5ae0054bb5da65bca8741bb62 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 10:54:17 +0100 Subject: [PATCH 39/89] Add `eframe::WindowChromeMetrics` (macOS only) (#8015) When using `egui::ViewportBuilder::with_fullsize_content_view` one must be careful not to paint anything where the "traffic light" buttons are: image `eframe::WindowChromeMetrics` helps you with that! --- crates/eframe/src/lib.rs | 3 ++ crates/eframe/src/native/macos.rs | 76 +++++++++++++++++++++++++++++++ crates/eframe/src/native/mod.rs | 3 ++ 3 files changed, 82 insertions(+) create mode 100644 crates/eframe/src/native/macos.rs diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 151fb79ce..86260ee5f 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -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; diff --git a/crates/eframe/src/native/macos.rs b/crates/eframe/src/native/macos.rs new file mode 100644 index 000000000..b1f2552e5 --- /dev/null +++ b/crates/eframe/src/native/macos.rs @@ -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 { + window_chrome_metrics(window_handle) + } +} + +fn window_chrome_metrics(window_handle: &RawWindowHandle) -> Option { + 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 { + // 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::(); + + // 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() + } + } +} diff --git a/crates/eframe/src/native/mod.rs b/crates/eframe/src/native/mod.rs index eb9413717..771964ae7 100644 --- a/crates/eframe/src/native/mod.rs +++ b/crates/eframe/src/native/mod.rs @@ -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; From e8f04292a9bfed66a8deaaff92efcdd60d2a4142 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 12:41:35 +0100 Subject: [PATCH 40/89] Add `UiStack::bg_color` (#8020) This lets you ask for the background color of a ui with `ui.stack().bg_color()` --- crates/egui/src/ui_stack.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 07026c45b..0686dc086 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -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,24 @@ impl UiStack { pub fn has_visible_frame(&self) -> bool { !self.info.frame.stroke.is_empty() } + + /// The background color of this [`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.is_opaque() { + return fill; + } else if fill != Color32::TRANSPARENT { + total = fill.blend(total); + } + } + total + } } // these methods act on the entire stack From 845b8c2f094e434b81a956519f638062faa3d3fc Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 25 Mar 2026 12:46:49 +0100 Subject: [PATCH 41/89] Make `egui::IdSet` public (#8019) It wasn't public before --- crates/egui/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index d86851a1d..e7d1fc25e 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -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::*, From 0b0c561a813ca03462fd698ad3d880cbfe0c9926 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 12:53:00 +0100 Subject: [PATCH 42/89] Fade out the edges of `ScrollAreas` (#8018) ## Before: image It is very hard here to realize this is a scrollable area ## After image The fade at the bottom tells the user they should try scrolling. You can turn if off with `style.spacing.scroll.fade.enabled` --- crates/egui/src/containers/scroll_area.rs | 96 ++++++++++++++++++- crates/egui/src/layout.rs | 32 +------ crates/egui/src/lib.rs | 2 +- crates/egui/src/style.rs | 49 ++++++++++ crates/egui/src/ui_stack.rs | 2 +- .../tests/snapshots/easymarkeditor.png | 4 +- crates/egui_demo_lib/src/demo/strip_demo.rs | 2 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../snapshots/demos/Window Resize Test.png | 4 +- .../tests/snapshots/test_scroll_initial.png | 4 +- .../tests/snapshots/test_scroll_scrolled.png | 4 +- crates/epaint/src/direction.rs | 27 ++++++ crates/epaint/src/lib.rs | 2 + crates/epaint/src/mesh.rs | 18 +++- crates/epaint/src/shapes/shape.rs | 28 +++++- crates/epaint/src/tessellator.rs | 11 +-- .../snapshots/text_edit_scroll_0_focus.png | 4 +- .../tests/snapshots/text_edit_scroll_1_5.png | 4 +- 22 files changed, 241 insertions(+), 72 deletions(-) create mode 100644 crates/epaint/src/direction.rs diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 2616fb414..6dc264a0a 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -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,89 @@ 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(ui: &Ui, scroll_output: &ScrollAreaOutput) { + let crate::style::ScrollFadeStyle { + enabled, + size: fade_size, + } = ui.spacing().scroll.fade; + + if !enabled { + 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); + 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); + 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); + 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); + 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], + )); + } +} diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index c35fd254b..c97652e8d 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -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". /// /// ``` diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index e7d1fc25e..cd098eaea 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -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}, diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 4f9749663..5549cdbc3 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -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,49 @@ 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 { + /// Show fade areas? + pub enabled: bool, + + /// Size of the fade-area (height for vertical scrolling, + /// width for horizontal scrolling). + pub size: f32, +} + +impl Default for ScrollFadeStyle { + fn default() -> Self { + Self { + enabled: true, + size: 20.0, + } + } +} + +impl ScrollFadeStyle { + pub fn ui(&mut self, ui: &mut Ui) { + let Self { enabled, size } = self; + + ui.horizontal(|ui| { + ui.checkbox(enabled, "Fade edges"); + }); + + if *enabled { + ui.horizontal(|ui| { + ui.add(DragValue::new(size).range(0.0..=64.0)); + ui.label("Fade size"); + }); + } } } diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 0686dc086..4debd76bb 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -256,7 +256,7 @@ impl UiStack { !self.info.frame.stroke.is_empty() } - /// The background color of this [`Ui`]. + /// The background color of this [`crate::Ui`]. /// /// This blend together all [`Frame::fill`] colors /// up to the root. diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index a0c1a52de..c90dcc918 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9ad01a55950f96a3ae9e48a2c026143d11ffee62bff4f83b4529cd884ce11f0 -size 169683 +oid sha256:65776e4d9e31e7163117166915a0cfaaece98b1dd69695533655dee889ac5597 +size 169277 diff --git a/crates/egui_demo_lib/src/demo/strip_demo.rs b/crates/egui_demo_lib/src/demo/strip_demo.rs index 94df7bb57..69bcce4f6 100644 --- a/crates/egui_demo_lib/src/demo/strip_demo.rs +++ b/crates/egui_demo_lib/src/demo/strip_demo.rs @@ -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 }; diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index f73093e3e..af2bb0a0f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c630df841e98043132b920338793745549116890c1bc52d8d10b0def896a1e6 -size 114409 +oid sha256:583003c59f40515a5de435ee1eea2ee6fceae409c7881456401b004c6409896a +size 114575 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 992fb471e..67e44c14e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:627114dcbda4f3d2255d34926ed0b77c679d248ed1d822d723479b1e6652c67a -size 249258 +oid sha256:8815473873602211a95e0a077dc77560de154a0a1a2f9c1418d90746670075f7 +size 248630 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 24e42cb23..aa354bb0a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5b965a7c690fd8e8646812513e2417170b687fd37e29d220c29127ba0cc200c -size 172609 +oid sha256:2881255fa694b713a3c3049f30143ee60a20b68105fb724fbba188f81d34e572 +size 171778 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 3f72922d2..f9c8f991e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:931f38ade8373ff79801c05c5d4397f2c5fcfa27022f2e1abe9eb29d561a3aef -size 76022 +oid sha256:63fb9d15956efa69818cfd5398b4dfaa796dfbe1df3fb171b8ae413a6f641c9f +size 76049 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index 23adea69c..3441b6896 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89986132c5d2a3ccccf0a16cb2b0be07b7c5512838cc10ec9067022e7a238515 -size 63918 +oid sha256:4ac837d03e3e8959196942d030a8511d00cdd0d5ac9bfe761d80b59d32e73581 +size 63967 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index 96ca9949e..2e5e8c717 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c81d5a5915dac60297293b90cd3fc0d188a9335e99b318996e3e2934de7bee1 -size 483497 +oid sha256:4107ba4569b3e37ce6720595cfc4de60aec11dcfdb835720442b244e121209fc +size 482862 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png index e61dc99a0..1eda6d278 100644 --- a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png +++ b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb8737af84c3d3b0c054b7e2a8bcb04685243d84cb13b72a1372dc40dbfd14fb -size 7267 +oid sha256:6f6d516cc3d1439256a19f72153468125bac1647e2e211e841579545570faef7 +size 7366 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png index 0cc3d6d15..87367951c 100644 --- a/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png +++ b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f1651bb1b9bbaa3c65ecd07c39c57527f4beb4c607581a5b2596a49dcf4c5db3 -size 7996 +oid sha256:2e05d28e5541eb5926b4758e611f5c340311269fcd8b63055ff3a6793abbb140 +size 8279 diff --git a/crates/epaint/src/direction.rs b/crates/epaint/src/direction.rs new file mode 100644 index 000000000..b2f317c37 --- /dev/null +++ b/crates/epaint/src/direction.rs @@ -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, + } + } +} diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 0042e0964..6f574e6b4 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -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::*, diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index 5343fdace..d48c98bc2 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -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. diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index fa8a3e75c..f5ca6a325 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -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<'_>, diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 9256ae16e..a732402d7 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -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); } diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png index 1d0a5ed46..da3039325 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81620caad6d420f3bd0f224e5b07a02960a42436208a98d3aa012e5db61a743a -size 1510 +oid sha256:5dfd9b576e0ab4b47a0f8c8acfc2664f92faa88cc7fb088409bff359fa1dfadd +size 1861 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png index bdb8d1b1b..ecba0fcc1 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f915eafb6490ff456c5b0a7c74c38ef143262bdf74a0c6561b9cf6ee66a679ea -size 1501 +oid sha256:4529530bb46af68260ca910a91f888e8b296790be2b976b450cec884799f53b4 +size 1953 From d232be740ffa69ca4ae0da3d416aa118d24f0f08 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 25 Mar 2026 13:40:54 +0100 Subject: [PATCH 43/89] Fix bug in ui stack color blending (#8021) Co-authored-by: Emil Ernerfeldt --- crates/egui/src/ui_stack.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index 4debd76bb..d72b3ea0b 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -265,10 +265,11 @@ impl UiStack { let mut total = Color32::TRANSPARENT; for node in self.iter() { let fill = node.frame().fill; - if fill.is_opaque() { - return fill; - } else if fill != Color32::TRANSPARENT { + if fill != Color32::TRANSPARENT { total = fill.blend(total); + if total.is_opaque() { + break; + } } } total From 02ff040b74cef45876fb8cc7534474351a48a8d6 Mon Sep 17 00:00:00 2001 From: eason <85663565+mango766@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:48:22 +0800 Subject: [PATCH 44/89] Fix: `Visuals::interact_cursor` support in `Button` (#7986) Closes #7947 ## Problem `Visuals::interact_cursor` stopped working for buttons after the `AtomLayout` refactor in commit 6eb7bb6e. Setting `interact_cursor` to e.g. `CursorIcon::PointingHand` no longer changes the cursor when hovering over a `Button`. ## Root Cause When `Button` was rewritten to use `AtomLayout` in #5830, the cursor-override block at the end of the old `Button::ui` was not carried over to the new `Button::atom_ui` method. The old code had: ```rust if let Some(cursor) = ui.visuals().interact_cursor { if response.hovered() { ui.ctx().set_cursor_icon(cursor); } } ``` This was the only place `interact_cursor` was checked, so the setting became entirely non-functional. ## Fix Re-add the same `interact_cursor` check in `Button::atom_ui`, right after painting and before `widget_info`, matching the original behavior. --------- Co-authored-by: easonysliu Co-authored-by: Emil Ernerfeldt --- crates/egui/src/widgets/button.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/egui/src/widgets/button.rs b/crates/egui/src/widgets/button.rs index 7d9dddf0d..8ef047002 100644 --- a/crates/egui/src/widgets/button.rs +++ b/crates/egui/src/widgets/button.rs @@ -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) From 048f8ccd2af21a0074ac2a2f62b2d08f917bbb44 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 25 Mar 2026 15:44:41 +0100 Subject: [PATCH 45/89] Tweak `ScrollArea` fade effect (#8023) * Follow-up to https://github.com/emilk/egui/pull/8018 --- crates/egui/src/containers/scroll_area.rs | 13 ++++++------- crates/egui/src/style.rs | 15 +++++++++------ .../tests/snapshots/easymarkeditor.png | 4 ++-- .../tests/snapshots/demos/Font Book.png | 4 ++-- .../tests/snapshots/demos/Panels.png | 4 ++-- .../tests/snapshots/demos/Scrolling.png | 4 ++-- .../egui_demo_lib/tests/snapshots/demos/Table.png | 4 ++-- .../tests/snapshots/demos/Tooltips.png | 4 ++-- .../tests/snapshots/demos/Window Resize Test.png | 4 ++-- .../tests/snapshots/test_scroll_initial.png | 4 ++-- .../tests/snapshots/test_scroll_scrolled.png | 4 ++-- .../tests/snapshots/text_edit_scroll_0_focus.png | 4 ++-- .../tests/snapshots/text_edit_scroll_1_5.png | 4 ++-- 13 files changed, 37 insertions(+), 35 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 6dc264a0a..b99bcf5da 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1513,11 +1513,11 @@ impl Prepared { /// indicate that more content is available beyond the visible region. fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { let crate::style::ScrollFadeStyle { - enabled, + strength, size: fade_size, } = ui.spacing().scroll.fade; - if !enabled { + if strength <= 0.0 { return; } @@ -1533,7 +1533,7 @@ fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { // 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); + 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(), @@ -1549,7 +1549,7 @@ fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { // 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); + 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), @@ -1563,9 +1563,8 @@ fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { } // 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); + 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(), @@ -1581,7 +1580,7 @@ fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { // 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); + 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()), diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 5549cdbc3..f7b889506 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -790,8 +790,10 @@ impl ScrollStyle { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct ScrollFadeStyle { - /// Show fade areas? - pub enabled: bool, + /// 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). @@ -801,7 +803,7 @@ pub struct ScrollFadeStyle { impl Default for ScrollFadeStyle { fn default() -> Self { Self { - enabled: true, + strength: 0.5, size: 20.0, } } @@ -809,13 +811,14 @@ impl Default for ScrollFadeStyle { impl ScrollFadeStyle { pub fn ui(&mut self, ui: &mut Ui) { - let Self { enabled, size } = self; + let Self { strength, size } = self; ui.horizontal(|ui| { - ui.checkbox(enabled, "Fade edges"); + ui.add(DragValue::new(strength).speed(0.01).range(0.0..=1.0)); + ui.label("Fade strength"); }); - if *enabled { + if 0.0 < *strength { ui.horizontal(|ui| { ui.add(DragValue::new(size).range(0.0..=64.0)); ui.label("Fade size"); diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index c90dcc918..5118065c4 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65776e4d9e31e7163117166915a0cfaaece98b1dd69695533655dee889ac5597 -size 169277 +oid sha256:84f0e72ce337d56f3767ebed1ab6a47f3d27c9fbcce4d8a19aeab358e12920f5 +size 169664 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index af2bb0a0f..ac85845f9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:583003c59f40515a5de435ee1eea2ee6fceae409c7881456401b004c6409896a -size 114575 +oid sha256:deff441cd1d9142352f8759dff4b759f4572f0ddf93752349314da77abe4b254 +size 115028 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 67e44c14e..70e7ce90e 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8815473873602211a95e0a077dc77560de154a0a1a2f9c1418d90746670075f7 -size 248630 +oid sha256:a46457b23b7b32694564d03b42bccac2f017a756225bc54b508bb6fe2ad8ee7b +size 249548 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index aa354bb0a..0924d3aa9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2881255fa694b713a3c3049f30143ee60a20b68105fb724fbba188f81d34e572 -size 171778 +oid sha256:927a497e8b6f9ce3b71dcb67086f477e19d327c163b2b8ad868af10009c2faf2 +size 172981 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index f9c8f991e..727c01434 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63fb9d15956efa69818cfd5398b4dfaa796dfbe1df3fb171b8ae413a6f641c9f -size 76049 +oid sha256:db4c0cf1c4cdae3d416afce5c58efd1cc382be86431e547fa66bcc95a0a17ddb +size 76364 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index 3441b6896..e7ed07b04 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac837d03e3e8959196942d030a8511d00cdd0d5ac9bfe761d80b59d32e73581 -size 63967 +oid sha256:57018beba5e4fb4f1e6de9c58bf898560b3a7669159d5bad91a4e2382ef57ce0 +size 64004 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index 2e5e8c717..c769f61bd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4107ba4569b3e37ce6720595cfc4de60aec11dcfdb835720442b244e121209fc -size 482862 +oid sha256:999f9cc006302b8951d97b510a02f1209969c376ecc7909ed5d7b46da27c0637 +size 483753 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png index 1eda6d278..64ed153a6 100644 --- a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png +++ b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f6d516cc3d1439256a19f72153468125bac1647e2e211e841579545570faef7 -size 7366 +oid sha256:6154c8bb550575bcb9fa0bba06da4d47079a00dffc5754b62ef2a6e7529e2090 +size 7489 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png index 87367951c..a87f09545 100644 --- a/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png +++ b/crates/egui_kittest/tests/snapshots/test_scroll_scrolled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e05d28e5541eb5926b4758e611f5c340311269fcd8b63055ff3a6793abbb140 -size 8279 +oid sha256:df2578c198b29950254ec62c6cc615a4b1c003e7ae3ea027da22fc868b392c74 +size 8342 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png index da3039325..019154ba6 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_0_focus.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dfd9b576e0ab4b47a0f8c8acfc2664f92faa88cc7fb088409bff359fa1dfadd -size 1861 +oid sha256:6f2a57ad8dbdd121cb181e74d76db68d800aba8fdc980d8de4e962e1e85fe8f6 +size 1803 diff --git a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png index ecba0fcc1..6c1ee1d81 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png +++ b/tests/egui_tests/tests/snapshots/text_edit_scroll_1_5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4529530bb46af68260ca910a91f888e8b296790be2b976b450cec884799f53b4 -size 1953 +oid sha256:43dc457cac18107772dbb7e5773961cf502dd685ce1cb4e94338e6b7daedaa77 +size 1861 From f1236f1c612cd4581db2425e8d8548651e19cfc7 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 25 Mar 2026 19:00:28 +0100 Subject: [PATCH 46/89] Fix missing `objc2-app-kit` features (#8025) Was missing some features --- crates/eframe/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 6219b90fd..0f08eb7f1 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -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: From 1c9f74b8bdec0d68795cde4d7f2827209b5857b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Sch=C3=BCtz?= Date: Thu, 26 Mar 2026 08:03:39 +0100 Subject: [PATCH 47/89] Add raw key methods to TypeIdMap (#8007) This PR fundamentally solves the same problem as https://github.com/emilk/egui/pull/5827 just implemented with a lot less ambition and api surface on my end. It contains the bare minimum amount of changes that I need in order to be able to solve my problem. My Problem: I am still suffering from the problem that the TypeIdMap blows up over a very long time when using my application. (The user generally never turns off the application, it is intended to be just kept running forever, some users also never restart their computers) My application generates a lot of content dynamically so it may for some time display widgets with a certain set of TypeId's + Id's later hiding them. Some of the elements that were hidden may turn visible again once an external event occurs, some may forever be discarded. I do know myself when which sections of the UI have to be purged because they will never become visible again, so this PR contains the minimum amount of necessary functions that allow me to implement this housekeeping logic on my end. The existing facilities are insufficient to handle this as the type T which the TypeId and the hash is derived from are sometimes pub(crate) privates of widget subcrates or even pub(crate) of egui internals itself, so its impossible to manually remove those from the TypeIdMap the only build-in method to remove them is to call "clear" on the TypeIdMap, however that gets rid of everything, even the elements that are still shown and should still be in the TypeIdMap. If the changes in this PR are agreeable to you and you want me to write unit test for the 4 functions that I have added then tell me and I will write the tests for you. If you need anything else changed please tell me. I ran cargo clippy and cargo fmt, but your check.sh does not work on my computer. --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/util/id_type_map.rs | 153 ++++++++++++++++++++++------ crates/egui_extras/src/table.rs | 5 +- 2 files changed, 126 insertions(+), 32 deletions(-) diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index b1312a1da..76f0da5d7 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -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(&self) -> Option<&T> { match self { @@ -316,6 +327,41 @@ fn from_ron_str(ron: &str) -> Option { 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::(Id::NULL), + /// RawKey::new::(Id::NULL), + /// ); + /// ``` + #[inline(always)] + pub fn new(id: Id) -> Self { + let type_id = TypeId::of::(); + 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, + map: nohash_hasher::IntMap, 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(&mut self, id: Id, value: T) { - let hash = hash(TypeId::of::(), id); - self.map.insert(hash, Element::new_temp(value)); + pub fn insert_temp( + &mut self, + id: Id, + value: T, + ) -> RawKey { + let key = RawKey::new::(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(&mut self, id: Id, value: T) { - let hash = hash(TypeId::of::(), id); - self.map.insert(hash, Element::new_persisted(value)); + let key = RawKey::new::(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(&self, id: Id) -> Option { - let hash = hash(TypeId::of::(), id); - self.map.get(&hash).and_then(|x| x.get_temp()).cloned() + let key = RawKey::new::(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(&mut self, id: Id) -> Option { - let hash = hash(TypeId::of::(), id); + let key = RawKey::new::(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::(), id); + let key = RawKey::new::(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::(), id); + let key = RawKey::new::(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(&self, id: Id) -> Option { - let element = self.map.get(&hash(TypeId::of::(), id))?; + let element = self.map.get(&RawKey::new::(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(&mut self, id: Id) { - let hash = hash(TypeId::of::(), id); - self.map.remove(&hash); + let key = RawKey::new::(id); + self.map.remove(&key); } /// Remove and fetch the state of this type and id. #[inline] pub fn remove_temp(&mut self, id: Id) -> Option { - let hash = hash(TypeId::of::(), id); - let mut element = self.map.remove(&hash)?; + let key = RawKey::new::(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> { + 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(&mut self) { let key = TypeId::of::(); @@ -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 { + 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, diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 78c6e7435..25257421a 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -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)); } } From 82a578e58c7ad8188c53a8fdc5bf6d4bc789e620 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Thu, 26 Mar 2026 12:15:41 +0100 Subject: [PATCH 48/89] Release 0.34.0 - More `Ui`, less `Context` (#8028) Co-authored-by: Emil Ernerfeldt --- CHANGELOG.md | 122 +++++++++++++++++++++++ Cargo.lock | 32 +++--- Cargo.toml | 26 ++--- crates/ecolor/CHANGELOG.md | 4 + crates/eframe/CHANGELOG.md | 24 +++++ crates/egui-wgpu/CHANGELOG.md | 14 +++ crates/egui-winit/CHANGELOG.md | 7 ++ crates/egui_extras/CHANGELOG.md | 8 ++ crates/egui_glow/CHANGELOG.md | 4 + crates/egui_kittest/CHANGELOG.md | 7 ++ crates/emath/CHANGELOG.md | 4 + crates/epaint/CHANGELOG.md | 19 ++++ crates/epaint_default_fonts/CHANGELOG.md | 4 + 13 files changed, 246 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12010e287..7b6c63ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,128 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.0 - 2026-03-26 + +### Highlights from this release +- Sharper text unlocked by switching font rendering crate to [`skrifa`](https://crates.io/crates/skrifa) +- Fade out edges of `ScrollArea`s +- Use `Ui` as the main entrypoint + +### Skrifa and font hinting +The font rendering backend was switched from `ab_glyph` to `skrifa` + `vello_cpu`. This enabled us support +font hinting and variations. It also paves the way for more font improvements in the future, like support for color +emojis and adding helpers for variations like `RichText::bold`. + +Font hinting makes text more clear (look at the =): + +https://github.com/user-attachments/assets/ea9151ec-869f-4c05-ab59-836114683417 + +We now support setting variable font parameters: + +https://github.com/user-attachments/assets/0febde1c-ebf6-4d85-8f96-86ec0f934ecf + +(Unfortunately there is currently a bug with variations, meaning changing them live like this won't work in practise. +There is a [draft PR](https://github.com/emilk/egui/pull/8029) to fix it, but it didn't make the release) + +* 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) + +### More `Ui`, less `Context` +egui has long had a confusing overlap in responsibilities between `Context` and `Ui`. +In particular, you could add panels to either one (or both!). +In this release, we switch from having `Context` be the main entrypoint, and instead provide whole-app `Ui`. +In egui we've replaced `Context::run` with `Context::run_ui`, and changed viewports to be given a `&mut Ui` instead of `Context`. +In `eframe` we've deprecated `App::update` replaced it with `App::ui` (which provides a `&mut Ui` instead of a `&Context`). + +In addition to this, `Ui` now derefs to `Context`, so all code like `ui.ctx().input(…)` can now be written `ui.input(…)`. +This means you are much less likely to have to use naked `Context`s. +`Context` can still be useful though, since they implement `Clone` and can be sent to other threads so you can call `.request_repaint` on them. + +* Add `Context::run_ui` [#7736](https://github.com/emilk/egui/pull/7736) by [@emilk](https://github.com/emilk) +* Add `Deref` for `Ui` [#7770](https://github.com/emilk/egui/pull/7770) by [@emilk](https://github.com/emilk) +* Replace `App::update` with `fn logic` and `fn ui` [#7775](https://github.com/emilk/egui/pull/7775) by [@emilk](https://github.com/emilk) +* Rename `Context::style` to `global_style`; avoid confusion w/ `Ui::style` [#7772](https://github.com/emilk/egui/pull/7772) by [@emilk](https://github.com/emilk) +* Rename functions in `Context` to avoid confusion [#7773](https://github.com/emilk/egui/pull/7773) by [@emilk](https://github.com/emilk) +* Viewports: give the caller a `Ui` instead of `Context` [#7779](https://github.com/emilk/egui/pull/7779) by [@emilk](https://github.com/emilk) + +### Changed panel API +As part of the above work, we have unified the panel API. +`SidePanel` and `TopBottomPanel` are deprecated, replaced by a single `Panel`. +Furthermore, it is now deprecated to use panels directly on `Context`. Use the `show_inside` functions instead, acting on `Ui`s. + +This unification and simplification will make it easier to maintain and improve panels going forward. + +* Add `Panel` to replace `SidePanel` and `TopBottomPanel` [#5659](https://github.com/emilk/egui/pull/5659) by [@sharky98](https://github.com/sharky98) +* Deprecate using `Panel` directly on a `Context` [#7781](https://github.com/emilk/egui/pull/7781) by [@emilk](https://github.com/emilk) +* Deprecate `CentralPanel::show` [#7783](https://github.com/emilk/egui/pull/7783) by [@emilk](https://github.com/emilk) +* Deprecate `Context::used_size` and `Context::available_rect` [#7788](https://github.com/emilk/egui/pull/7788) by [@emilk](https://github.com/emilk) + +### ⭐ Added +* 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) +* Allow multiple atoms in `Button::shortcut_text` and `right_text` [#7696](https://github.com/emilk/egui/pull/7696) by [@emilk](https://github.com/emilk) +* Add `ScrollArea::content_margin` [#7722](https://github.com/emilk/egui/pull/7722) by [@emilk](https://github.com/emilk) +* Per-widget style [#7667](https://github.com/emilk/egui/pull/7667) by [@AdrienZianne](https://github.com/AdrienZianne) +* Plugin: export `TypedPluginGuard` and `TypedPluginHandle` [#7780](https://github.com/emilk/egui/pull/7780) by [@apekros](https://github.com/apekros) +* Add `ViewportInfo::occluded` and `visible` [#7948](https://github.com/emilk/egui/pull/7948) by [@emilk](https://github.com/emilk) +* Add `Atom` prefix/suffix support to `DragValue` [#7949](https://github.com/emilk/egui/pull/7949) by [@lucasmerlin](https://github.com/lucasmerlin) +* āš ļø Atom improvements: `Atom::id`, `align`, `closure`, `max_size` [#7958](https://github.com/emilk/egui/pull/7958) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `DebugOptions::warn_if_rect_changes_id` [#7984](https://github.com/emilk/egui/pull/7984) by [@emilk](https://github.com/emilk) +* `TextEdit` `Atom` prefix/suffix [#7587](https://github.com/emilk/egui/pull/7587) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `Button::left_text` [#7955](https://github.com/emilk/egui/pull/7955) by [@rustbasic](https://github.com/rustbasic) +* Add `Response::parent_id` [#8010](https://github.com/emilk/egui/pull/8010) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `Context::text_edit_focused` [#8014](https://github.com/emilk/egui/pull/8014) by [@emilk](https://github.com/emilk) +* Add `Context::time` [#8017](https://github.com/emilk/egui/pull/8017) by [@emilk](https://github.com/emilk) +* Add `Ui::is_tooltip` [#8016](https://github.com/emilk/egui/pull/8016) by [@emilk](https://github.com/emilk) +* Add `UiStack::bg_color` [#8020](https://github.com/emilk/egui/pull/8020) by [@emilk](https://github.com/emilk) +* Make `egui::IdSet` public [#8019](https://github.com/emilk/egui/pull/8019) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add raw key methods to TypeIdMap [#8007](https://github.com/emilk/egui/pull/8007) by [@AlexanderSchuetz97](https://github.com/AlexanderSchuetz97) + +### šŸ”§ Changed +* Remove `accesskit` feature and always depend on `accesskit` [#7701](https://github.com/emilk/egui/pull/7701) by [@emilk](https://github.com/emilk) +* Update MSRV from 1.88 to 1.92 [#7793](https://github.com/emilk/egui/pull/7793) by [@JasperBRiedel](https://github.com/JasperBRiedel) +* Improve modifier handling when scrolling [#7678](https://github.com/emilk/egui/pull/7678) by [@emilk](https://github.com/emilk) +* Apply preferred font weight when loading variable fonts [#7790](https://github.com/emilk/egui/pull/7790) by [@pmnxis](https://github.com/pmnxis) +* Make scroll bars and resize splitters visible to accesskit [#7804](https://github.com/emilk/egui/pull/7804) by [@emilk](https://github.com/emilk) +* Allow moving existing widgets to the top of interaction stack [#7805](https://github.com/emilk/egui/pull/7805) by [@emilk](https://github.com/emilk) +* Slightly change interact behavior around thin splitters [#7806](https://github.com/emilk/egui/pull/7806) by [@emilk](https://github.com/emilk) +* Move window resize interaction to be over contents [#7807](https://github.com/emilk/egui/pull/7807) by [@emilk](https://github.com/emilk) +* Don't expand widgets on hover [#7808](https://github.com/emilk/egui/pull/7808) by [@emilk](https://github.com/emilk) +* Make `FrameCache::get` return a reference instead of cloning the cached value [#7834](https://github.com/emilk/egui/pull/7834) by [@KonaeAkira](https://github.com/KonaeAkira) +* Update selected dependencies [#7920](https://github.com/emilk/egui/pull/7920) by [@oscargus](https://github.com/oscargus) +* Make `Galley::pos_from_layout_cursor` `pub` [#7864](https://github.com/emilk/egui/pull/7864) by [@dionb](https://github.com/dionb) +* Update accesskit to 0.24.0 (and related deps) [#7850](https://github.com/emilk/egui/pull/7850) by [@delan](https://github.com/delan) +* Quit on Ctrl-Q [#7985](https://github.com/emilk/egui/pull/7985) by [@emilk](https://github.com/emilk) +* Fade out the edges of `ScrollAreas` [#8018](https://github.com/emilk/egui/pull/8018) by [@emilk](https://github.com/emilk) + +### šŸ”„ Removed +* Remove `CacheTrait::as_any_mut` [#7833](https://github.com/emilk/egui/pull/7833) by [@emilk](https://github.com/emilk) + +### šŸ› Fixed +* Fix: ensure `CentralPanel::show_inside` allocates space in parent [#7778](https://github.com/emilk/egui/pull/7778) by [@emilk](https://github.com/emilk) +* Heed constrain rect when auto-positioning windows [#7786](https://github.com/emilk/egui/pull/7786) by [@emilk](https://github.com/emilk) +* Fix jitter when hovering edge of scroll area close to resize splitter [#7803](https://github.com/emilk/egui/pull/7803) by [@emilk](https://github.com/emilk) +* Don't focus Areas, Windows and ScrollAreas [#7827](https://github.com/emilk/egui/pull/7827) by [@lucasmerlin](https://github.com/lucasmerlin) +* 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) +* Implemented distance threshold for double/triple clicks [#7817](https://github.com/emilk/egui/pull/7817) by [@bl4ze4447](https://github.com/bl4ze4447) +* Fix `CentralPanel::show_inside_dyn` to round `panel_rect` [#7868](https://github.com/emilk/egui/pull/7868) by [@ripopov](https://github.com/ripopov) +* Stop ctrl+arrow etc from moving focus [#7897](https://github.com/emilk/egui/pull/7897) by [@emilk](https://github.com/emilk) +* Fix scroll area not consuming scroll events [#7904](https://github.com/emilk/egui/pull/7904) by [@lucasmerlin](https://github.com/lucasmerlin) +* Pass in an explicit id in `UiBuilder`, to avoid wrapping passed in ids with Id::new() [#7925](https://github.com/emilk/egui/pull/7925) by [@lucasmerlin](https://github.com/lucasmerlin) +* Fix crash when dragging a DragValue through small floats [#7939](https://github.com/emilk/egui/pull/7939) by [@Fyrecean](https://github.com/Fyrecean) +* Fix emoji icon font [#7940](https://github.com/emilk/egui/pull/7940) by [@Jhynjhiruu](https://github.com/Jhynjhiruu) +* 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: repaint on drag-and-drop files [#7953](https://github.com/emilk/egui/pull/7953) by [@emilk](https://github.com/emilk) +* Fix instable IDs following animated panels [#7994](https://github.com/emilk/egui/pull/7994) by [@emilk](https://github.com/emilk) +* Enables every combination of `TextEdit` and `LayoutJob` alignments [#7831](https://github.com/emilk/egui/pull/7831) by [@RndUsr123](https://github.com/RndUsr123) +* Fix `horizontal_wrapping` row height after using `text_edit_multiline` [#8000](https://github.com/emilk/egui/pull/8000) by [@optozorax](https://github.com/optozorax) +* Fix menu keyboard toggle for open submenus [#7957](https://github.com/emilk/egui/pull/7957) by [@fjkorf](https://github.com/fjkorf) +* Fix: `Visuals::interact_cursor` support in `Button` [#7986](https://github.com/emilk/egui/pull/7986) by [@mango766](https://github.com/mango766) + +### šŸš€ Performance +* Shrink the byte-size of `Response` slightly [#8011](https://github.com/emilk/egui/pull/8011) by [@emilk](https://github.com/emilk) + + ## 0.33.3 - 2025-12-11 * Treat `.` as a word-splitter in text navigation [#7741](https://github.com/emilk/egui/pull/7741) by [@emilk](https://github.com/emilk) * Change text color of selected text [#7691](https://github.com/emilk/egui/pull/7691) by [@emilk](https://github.com/emilk) diff --git a/Cargo.lock b/Cargo.lock index 981e7c475..526f8dd10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,7 +1193,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.33.3" +version = "0.34.0" dependencies = [ "bytemuck", "cint", @@ -1205,7 +1205,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.33.3" +version = "0.34.0" dependencies = [ "ahash", "bytemuck", @@ -1244,7 +1244,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.33.3" +version = "0.34.0" dependencies = [ "accesskit", "ahash", @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.33.3" +version = "0.34.0" dependencies = [ "ahash", "bytemuck", @@ -1282,7 +1282,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.33.3" +version = "0.34.0" dependencies = [ "accesskit_winit", "arboard", @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.33.3" +version = "0.34.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -1335,7 +1335,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.33.3" +version = "0.34.0" dependencies = [ "criterion", "document-features", @@ -1352,7 +1352,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.33.3" +version = "0.34.0" dependencies = [ "ahash", "document-features", @@ -1371,7 +1371,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.33.3" +version = "0.34.0" dependencies = [ "bytemuck", "document-features", @@ -1390,7 +1390,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.33.3" +version = "0.34.0" dependencies = [ "dify", "document-features", @@ -1410,7 +1410,7 @@ dependencies = [ [[package]] name = "egui_tests" -version = "0.33.3" +version = "0.34.0" dependencies = [ "egui", "egui_extras", @@ -1440,7 +1440,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.33.3" +version = "0.34.0" dependencies = [ "bytemuck", "document-features", @@ -1538,7 +1538,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.33.3" +version = "0.34.0" dependencies = [ "ahash", "bytemuck", @@ -1564,7 +1564,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.33.3" +version = "0.34.0" [[package]] name = "equivalent" @@ -3432,7 +3432,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "popups" -version = "0.33.3" +version = "0.34.0" dependencies = [ "eframe", "env_logger", @@ -5820,7 +5820,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.33.3" +version = "0.34.0" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index cf631eb25..460a9b667 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.92" -version = "0.33.3" +version = "0.34.0" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.33.3", path = "crates/emath", default-features = false } -ecolor = { version = "0.33.3", path = "crates/ecolor", default-features = false } -epaint = { version = "0.33.3", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.33.3", path = "crates/epaint_default_fonts" } -egui = { version = "0.33.3", path = "crates/egui", default-features = false } -egui-winit = { version = "0.33.3", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.33.3", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.33.3", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.33.3", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.33.3", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.33.3", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.33.3", path = "crates/eframe", default-features = false } +emath = { version = "0.34.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.34.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.34.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.34.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.34.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.34.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.34.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.34.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.34.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.34.0", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.34.0", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.34.0", path = "crates/eframe", default-features = false } accesskit = "0.24.0" accesskit_consumer = "0.35.0" diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index a4fe1f3de..161a32ae8 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 74a705251..ae8f41a02 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,30 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index cef640184..08c9d7187 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,20 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index f88a8c84a..b4a9186b2 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,13 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index 15eaf8704..4a480c366 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,14 @@ This file is updated upon each release. Changes since the last release can be found at 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) diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 2fccf1166..d43a4be20 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,10 @@ Changes since the last release can be found at 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) diff --git a/crates/emath/CHANGELOG.md b/crates/emath/CHANGELOG.md index f559265c4..f82686d71 100644 --- a/crates/emath/CHANGELOG.md +++ b/crates/emath/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index dd6b4e2a5..fdbc3704c 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,25 @@ This file is updated upon each release. Changes since the last release can be found at 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 diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 58fd310f4..925f6f89c 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at 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 From 3cf3141e8f07ce1a2c736e2cf1c4d500a6c5f7a7 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 27 Mar 2026 11:12:46 +0100 Subject: [PATCH 49/89] `wgpu` backend: Enable WebGL fallback (#8038) * Fix for https://github.com/emilk/eframe_template/issues/223 * Related: https://github.com/gfx-rs/wgpu/pull/9319 By default, we would only turn on the WebGPU backend on web, which means browsers without WebGPU support would just crash. You can still opt-out of all the default `wgpu` features by enabling `eframe/wgpu_no_default_features` instead of `eframe/wgpu` --- Cargo.lock | 1 - crates/eframe/Cargo.toml | 3 +-- crates/eframe/src/lib.rs | 2 +- crates/egui-wgpu/Cargo.toml | 7 ++++++- crates/egui_demo_app/Cargo.toml | 5 +---- crates/egui_demo_app/src/backend_panel.rs | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 526f8dd10..d9da12ef5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,7 +1330,6 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "wgpu", ] [[package]] diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 0f08eb7f1..0530b64a8 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -89,7 +89,7 @@ web_screen_reader = ["web-sys/SpeechSynthesis", "web-sys/SpeechSynthesisUtteranc ## See for more details. ## ## By default, eframe will prefer WebGPU over WebGL, but -## you can configure this at run-time with [`NativeOptions::wgpu_options`]. +## you can configure this at run-time with `WebOptions::wgpu_options`. wgpu = ["wgpu_no_default_features", "egui-wgpu/default"] ## This is exactly like the `wgpu` feature, but does NOT enable the default features of `wgpu` and `egui-wgpu`. @@ -155,7 +155,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe "wgl", ] } home = { workspace = true, optional = true } -wgpu = { workspace = true, optional = true } # mac: [target.'cfg(any(target_os = "macos"))'.dependencies] diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 86260ee5f..c644289e0 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -159,7 +159,7 @@ pub use {egui, egui::emath, egui::epaint}; pub use {egui_glow, glow}; #[cfg(feature = "wgpu_no_default_features")] -pub use {egui_wgpu, wgpu}; +pub use {egui_wgpu, egui_wgpu::wgpu}; mod epi; diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 86cd3192b..0b081e134 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -25,7 +25,12 @@ all-features = true rustdoc-args = ["--generate-link-to-definition"] [features] -default = ["fragile-send-sync-non-atomic-wasm", "macos-window-resize-jitter-fix", "wgpu/default"] +default = [ + "fragile-send-sync-non-atomic-wasm", + "macos-window-resize-jitter-fix", + "wgpu/default", + "wgpu/webgl", # A very important fallback for web support +] ## Enables the `capture` module for capturing screenshots. capture = ["dep:egui"] diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 49609746f..5bb093ced 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -37,7 +37,7 @@ serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] syntect = ["egui_demo_lib/syntect"] glow = ["eframe/glow"] -wgpu = ["eframe/wgpu", "bytemuck", "dep:wgpu"] +wgpu = ["eframe/wgpu", "bytemuck"] wayland = ["eframe/wayland"] x11 = ["eframe/x11"] @@ -60,9 +60,6 @@ accesskit_consumer = { workspace = true, optional = true } bytemuck = { workspace = true, optional = true } puffin = { workspace = true, optional = true } puffin_http = { workspace = true, optional = true } -# Enable both WebGL & WebGPU when targeting the web (these features have no effect when not targeting wasm32) -# Also enable the default features so we have a supported backend for every platform. -wgpu = { workspace = true, features = ["default", "webgpu", "webgl"], optional = true } # feature "http": diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index dfc4d116b..cd17afd4a 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -211,7 +211,7 @@ fn integration_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame) { let wgpu_adapter_details_ui = |ui: &mut egui::Ui, adapter: &eframe::wgpu::Adapter| { let info = &adapter.get_info(); - let wgpu::AdapterInfo { + let eframe::wgpu::AdapterInfo { name, vendor, device, From f3250976112d669b11c6f43f2cd40a84ba1d893c Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Fri, 27 Mar 2026 06:18:52 -0400 Subject: [PATCH 50/89] Only apply cursor style to the (#8036) This improves cases where the canvas does not cover the full screen, which was a goal in [release 0.28.0](https://github.com/emilk/egui/releases/tag/0.28.0). * Closes * [X] I have followed the instructions in the PR template --- crates/eframe/src/web/app_runner.rs | 2 +- crates/eframe/src/web/mod.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index b90b8a5e1..7161a665e 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -393,7 +393,7 @@ impl AppRunner { } } - super::set_cursor_icon(cursor_icon); + super::set_cursor_icon(self.canvas(), cursor_icon); if self.has_focus() { // The eframe app has focus. diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 87771f722..dc743ec49 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -178,10 +178,8 @@ fn canvas_size_in_points(canvas: &web_sys::HtmlCanvasElement, ctx: &egui::Contex // ---------------------------------------------------------------------------- /// Set the cursor icon. -fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { - let document = web_sys::window()?.document()?; - document - .body()? +fn set_cursor_icon(canvas: &web_sys::HtmlCanvasElement, cursor: egui::CursorIcon) -> Option<()> { + canvas .style() .set_property("cursor", cursor_web_name(cursor)) .ok() From a01193d032ae89e1ae80c56834993437b9467256 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 27 Mar 2026 11:24:51 +0100 Subject: [PATCH 51/89] Release 0.34.1: Enable WebGL fallback in eframe --- CHANGELOG.md | 4 +++ Cargo.lock | 32 ++++++++++++------------ Cargo.toml | 26 +++++++++---------- crates/ecolor/CHANGELOG.md | 4 +++ crates/eframe/CHANGELOG.md | 5 ++++ crates/egui-wgpu/CHANGELOG.md | 4 +++ crates/egui-winit/CHANGELOG.md | 4 +++ crates/egui_extras/CHANGELOG.md | 4 +++ crates/egui_glow/CHANGELOG.md | 4 +++ crates/egui_kittest/CHANGELOG.md | 4 +++ crates/emath/CHANGELOG.md | 4 +++ crates/epaint/CHANGELOG.md | 4 +++ crates/epaint_default_fonts/CHANGELOG.md | 4 +++ 13 files changed, 74 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b6c63ab4..e531dddc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 0.34.0 - 2026-03-26 ### Highlights from this release diff --git a/Cargo.lock b/Cargo.lock index d9da12ef5..ec0b421f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,7 +1193,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.34.0" +version = "0.34.1" dependencies = [ "bytemuck", "cint", @@ -1205,7 +1205,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.34.0" +version = "0.34.1" dependencies = [ "ahash", "bytemuck", @@ -1244,7 +1244,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.34.0" +version = "0.34.1" dependencies = [ "accesskit", "ahash", @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.34.0" +version = "0.34.1" dependencies = [ "ahash", "bytemuck", @@ -1282,7 +1282,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.34.0" +version = "0.34.1" dependencies = [ "accesskit_winit", "arboard", @@ -1305,7 +1305,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.34.0" +version = "0.34.1" dependencies = [ "accesskit", "accesskit_consumer", @@ -1334,7 +1334,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.34.0" +version = "0.34.1" dependencies = [ "criterion", "document-features", @@ -1351,7 +1351,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.34.0" +version = "0.34.1" dependencies = [ "ahash", "document-features", @@ -1370,7 +1370,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.34.0" +version = "0.34.1" dependencies = [ "bytemuck", "document-features", @@ -1389,7 +1389,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.34.0" +version = "0.34.1" dependencies = [ "dify", "document-features", @@ -1409,7 +1409,7 @@ dependencies = [ [[package]] name = "egui_tests" -version = "0.34.0" +version = "0.34.1" dependencies = [ "egui", "egui_extras", @@ -1439,7 +1439,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.34.0" +version = "0.34.1" dependencies = [ "bytemuck", "document-features", @@ -1537,7 +1537,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.34.0" +version = "0.34.1" dependencies = [ "ahash", "bytemuck", @@ -1563,7 +1563,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.34.0" +version = "0.34.1" [[package]] name = "equivalent" @@ -3431,7 +3431,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "popups" -version = "0.34.0" +version = "0.34.1" dependencies = [ "eframe", "env_logger", @@ -5819,7 +5819,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.34.0" +version = "0.34.1" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 460a9b667..6c6cb0c03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.92" -version = "0.34.0" +version = "0.34.1" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.34.0", path = "crates/emath", default-features = false } -ecolor = { version = "0.34.0", path = "crates/ecolor", default-features = false } -epaint = { version = "0.34.0", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.34.0", path = "crates/epaint_default_fonts" } -egui = { version = "0.34.0", path = "crates/egui", default-features = false } -egui-winit = { version = "0.34.0", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.34.0", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.34.0", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.34.0", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.34.0", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.34.0", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.34.0", path = "crates/eframe", default-features = false } +emath = { version = "0.34.1", path = "crates/emath", default-features = false } +ecolor = { version = "0.34.1", path = "crates/ecolor", default-features = false } +epaint = { version = "0.34.1", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.34.1", path = "crates/epaint_default_fonts" } +egui = { version = "0.34.1", path = "crates/egui", default-features = false } +egui-winit = { version = "0.34.1", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.34.1", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.34.1", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.34.1", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.34.1", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.34.1", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.34.1", path = "crates/eframe", default-features = false } accesskit = "0.24.0" accesskit_consumer = "0.35.0" diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 161a32ae8..fe0333748 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 0.34.0 - 2026-03-26 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index ae8f41a02..d9ba794b1 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,11 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +* `wgpu` backend: Enable WebGL fallback [#8038](https://github.com/emilk/egui/pull/8038) by [@emilk](https://github.com/emilk) +* Only apply cursor style to the `` [#8036](https://github.com/emilk/egui/pull/8036) by [@mkeeter](https://github.com/mkeeter) + + ## 0.34.0 - 2026-03-26 ### ⭐ Added * Add feature `wgpu_no_default_features` [#7700](https://github.com/emilk/egui/pull/7700) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 08c9d7187..4cc344bf2 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +* `wgpu` backend: Enable WebGL fallback [#8038](https://github.com/emilk/egui/pull/8038) by [@emilk](https://github.com/emilk) + + ## 0.34.0 - 2026-03-26 ### ⭐ Added * Add error message when calling `.render()` without `.update_buffers()` [#8005](https://github.com/emilk/egui/pull/8005) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index b4a9186b2..68fe5b2cd 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 0.34.0 - 2026-03-26 * Add `is_scrolling`/`is_smooth_scrolling` util, checking for active scroll action [#7669](https://github.com/emilk/egui/pull/7669) by [@IsseW](https://github.com/IsseW) * Fix backspacing leaving last character in IME prediction not removed on macOS native and Safari [#7810](https://github.com/emilk/egui/pull/7810) by [@umajho](https://github.com/umajho) diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index 4a480c366..9d2bfd2d7 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 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) diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index d43a4be20..a33d24bec 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,10 @@ Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 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) diff --git a/crates/emath/CHANGELOG.md b/crates/emath/CHANGELOG.md index f82686d71..8c55b647f 100644 --- a/crates/emath/CHANGELOG.md +++ b/crates/emath/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 0.34.0 - 2026-03-26 Nothing new diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index fdbc3704c..0d577b120 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 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) diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 925f6f89c..daf46d4de 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.1 - 2026-03-27 +Nothing new + + ## 0.34.0 - 2026-03-26 * Fix emoji icon font [#7940](https://github.com/emilk/egui/pull/7940) by [@Jhynjhiruu](https://github.com/Jhynjhiruu) From eb35f7d12f2a941707eee3406aab28e1df0262c9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 29 Mar 2026 10:00:36 +0200 Subject: [PATCH 52/89] Improve test of text rendering --- crates/egui_demo_lib/tests/misc.rs | 5 +++-- .../tests/snapshots/image_kerning/image_dark_x1.png | 4 ++-- .../tests/snapshots/image_kerning/image_dark_x2.png | 4 ++-- .../tests/snapshots/image_kerning/image_light_x1.png | 4 ++-- .../tests/snapshots/image_kerning/image_light_x2.png | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index d5f6a3a3c..6d66abfb1 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -13,8 +13,9 @@ fn test_kerning() { ui.label("Hello world!"); ui.label("Repeated characters: iiiiiiiiiiiii lllllllll mmmmmmmmmmmmmmmm"); ui.label("Thin spaces: āˆ’123 456 789"); - ui.label("Ligature: fi :)"); - ui.label("\ttabbed"); + ui.label("Ligature: fi fl ffi ffl"); + ui.label("Kerning: AVATAR"); + ui.label("\ttabbed\ttext"); }); harness.run(); harness.fit_contents(); diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png index 5cc884a55..e48dfae92 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8ea98c65376d9f6ac66d0a9471c4bf3add0904294e7ca1a105458b90654a2e2 -size 12476 +oid sha256:9c2990a81dfa8832f0cb1c4c0ce2f86e468a7a6f693e09efffa131ed3259e2e8 +size 15428 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png index 1359fd607..3f9da5333 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:968c478d986fc71d8655492b19e833ca07bc0ab85899dc04022bc7cf1dcf782f -size 29319 +oid sha256:07d987ff87c9f41ec71ceea0caff25795bf4dff525ef4ef241d0ba786acee3e1 +size 35960 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png index b223bbb3d..84b808a83 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3793a5e83ef9bdffef99bcd8905a094acb69cde356e3a7125a544045296c3926 -size 13070 +oid sha256:f5c5ce0d46231d90ccb04e158947d793d99cd5cce911c72b960b6d04feba2134 +size 16122 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png index 80e561325..ff848168a 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61e59f8360c567e20bf03b401362de7bb0f87716f13e817cc8da3df742ab68bf -size 31869 +oid sha256:60dcd590b1d00361278b135ce9ef084c7382875c71c72b19fb6e23dba68f7902 +size 39279 From c2b482ff7ef7c9093d3a59398e94791a395ad9bf Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 4 Apr 2026 12:03:41 +0200 Subject: [PATCH 53/89] Flip if-else:s with a negation (#8063) --- crates/eframe/src/web/events.rs | 10 +++++----- crates/egui/src/response.rs | 6 +++--- .../src/text_selection/text_cursor_state.rs | 8 ++++---- crates/egui/src/widgets/slider.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 12 +++++------ crates/egui_demo_app/src/wrap_app.rs | 12 +++++------ crates/epaint/src/stats.rs | 6 +++--- examples/file_dialog/src/main.rs | 12 +++++------ tests/test_viewports/src/main.rs | 20 +++++++++---------- 9 files changed, 44 insertions(+), 44 deletions(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index d77444563..e24e99fee 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -1114,16 +1114,16 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32 } else if JsValue::from_str("contentBoxSize").js_in(entry.as_ref()) { let content_box_size = entry.content_box_size(); let idx0 = content_box_size.at(0); - if !idx0.is_undefined() { - let size: web_sys::ResizeObserverSize = idx0.dyn_into()?; - width = size.inline_size(); - height = size.block_size(); - } else { + if idx0.is_undefined() { // legacy let size = JsValue::clone(content_box_size.as_ref()); let size: web_sys::ResizeObserverSize = size.dyn_into()?; width = size.inline_size(); height = size.block_size(); + } else { + let size: web_sys::ResizeObserverSize = idx0.dyn_into()?; + width = size.inline_size(); + height = size.block_size(); } if DEBUG_RESIZE { log::info!("contentBoxSize {width}x{height}"); diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index a0dd6bd91..54d53f2b4 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -272,10 +272,10 @@ impl Response { false } else if let Some(pos) = pointer_interact_pos { let layer_under_pointer = self.ctx.layer_id_at(pos); - if layer_under_pointer != Some(self.layer_id) { - true - } else { + if layer_under_pointer == Some(self.layer_id) { !self.interact_rect.contains(pos) + } else { + true } } else { false // clicked without a pointer, weird diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 9c9b0a263..12e0656d8 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -156,13 +156,13 @@ fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange { let min = ccursor_previous_line(text, ccursor); let max = ccursor_next_line(text, min); CCursorRange::two(min, max) - } else if !is_linebreak(char_after_cursor) { - let max = ccursor_next_line(text, ccursor); - CCursorRange::two(ccursor, max) - } else { + } else if is_linebreak(char_after_cursor) { let min = ccursor_previous_line(text, ccursor); let max = ccursor_next_line(text, ccursor); CCursorRange::two(min, max) + } else { + let max = ccursor_next_line(text, ccursor); + CCursorRange::two(ccursor, max) } } else { let min = ccursor_previous_line(text, ccursor); diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index 69d08c44e..1a9edf02d 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -314,7 +314,7 @@ impl<'a> Slider<'a> { /// Default: `0.0` (disabled). #[inline] pub fn step_by(mut self, step: f64) -> Self { - self.step = if step != 0.0 { Some(step) } else { None }; + self.step = if step == 0.0 { None } else { Some(step) }; self } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index ef668a02e..6f4d9a044 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -680,7 +680,9 @@ impl TextEdit<'_> { .wrap_mode(wrap_mode) .allocate(ui); - allocated.frame = if !custom_frame { + allocated.frame = if custom_frame { + allocated.frame + } else { let visuals = ui.style().interact(&allocated.response); let background_color = background_color.unwrap_or_else(|| ui.visuals().text_edit_bg_color()); @@ -713,8 +715,6 @@ impl TextEdit<'_> { ) .outer_margin(Margin::same(-(visuals.expansion as i8))) .stroke(stroke) - } else { - allocated.frame }; allocated.paint(ui) @@ -1019,7 +1019,9 @@ fn events( } } Event::Paste(text_to_insert) => { - if !text_to_insert.is_empty() { + if text_to_insert.is_empty() { + None + } else { let mut ccursor = text.delete_selected(&cursor_range); if multiline { text.insert_text_at(&mut ccursor, text_to_insert, char_limit); @@ -1029,8 +1031,6 @@ fn events( } Some(CCursorRange::one(ccursor)) - } else { - None } } Event::Text(text_to_insert) => { diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 313a1a685..9f43f8624 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -466,10 +466,10 @@ impl WrapApp { for file in &i.raw.hovered_files { if let Some(path) = &file.path { write!(text, "\n{}", path.display()).ok(); - } else if !file.mime.is_empty() { - write!(text, "\n{}", file.mime).ok(); - } else { + } else if file.mime.is_empty() { text += "\n???"; + } else { + write!(text, "\n{}", file.mime).ok(); } } text @@ -505,10 +505,10 @@ impl WrapApp { for file in &self.dropped_files { let mut info = if let Some(path) = &file.path { path.display().to_string() - } else if !file.name.is_empty() { - file.name.clone() - } else { + } else if file.name.is_empty() { "???".to_owned() + } else { + file.name.clone() }; let mut additional_info = vec![]; diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index 1eef4f444..de8f275cf 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -135,10 +135,10 @@ impl AllocInfo { what, self.megabytes() ) - } else if self.element_size != ElementSize::Heterogenous { + } else if self.element_size == ElementSize::Heterogenous { format!( "{:6} {:16} {} {:3} allocations", - self.num_elements(), + "", what, self.megabytes(), self.num_allocs() @@ -146,7 +146,7 @@ impl AllocInfo { } else { format!( "{:6} {:16} {} {:3} allocations", - "", + self.num_elements(), what, self.megabytes(), self.num_allocs() diff --git a/examples/file_dialog/src/main.rs b/examples/file_dialog/src/main.rs index 63e7bb0a8..33bda7a96 100644 --- a/examples/file_dialog/src/main.rs +++ b/examples/file_dialog/src/main.rs @@ -50,10 +50,10 @@ impl eframe::App for MyApp { for file in &self.dropped_files { let mut info = if let Some(path) = &file.path { path.display().to_string() - } else if !file.name.is_empty() { - file.name.clone() - } else { + } else if file.name.is_empty() { "???".to_owned() + } else { + file.name.clone() }; let mut additional_info = vec![]; @@ -95,10 +95,10 @@ fn preview_files_being_dropped(ctx: &egui::Context) { for file in &i.raw.hovered_files { if let Some(path) = &file.path { write!(text, "\n{}", path.display()).ok(); - } else if !file.mime.is_empty() { - write!(text, "\n{}", file.mime).ok(); - } else { + } else if file.mime.is_empty() { text += "\n???"; + } else { + write!(text, "\n{}", file.mime).ok(); } } text diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 3330c1313..25787c912 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -416,16 +416,7 @@ fn drag_source( ) -> InnerResponse { let is_being_dragged = ui.ctx().is_being_dragged(id); - if !is_being_dragged { - let res = ui.scope(body); - - // Check for drags: - let response = ui.interact(res.response.rect, id, egui::Sense::drag()); - if response.hovered() { - ui.set_cursor_icon(egui::CursorIcon::Grab); - } - res - } else { + if is_being_dragged { ui.set_cursor_icon(egui::CursorIcon::Grabbing); // Paint the body to a new layer: @@ -440,6 +431,15 @@ fn drag_source( ); } + res + } else { + let res = ui.scope(body); + + // Check for drags: + let response = ui.interact(res.response.rect, id, egui::Sense::drag()); + if response.hovered() { + ui.set_cursor_icon(egui::CursorIcon::Grab); + } res } } From 0435d2a9a1417b9f3ff726d377656ce81b02d538 Mon Sep 17 00:00:00 2001 From: Michael Grupp Date: Sat, 4 Apr 2026 12:09:56 +0200 Subject: [PATCH 54/89] Add `HarnessBuilder::with_render_options()` (closes #7630) (#8060) Allows to override the default `PREDICTABLE` render options, e.g. if it's desired to create snapshots with the exact texture options used by the app. See #7630 for details / examples. --- crates/egui_kittest/src/builder.rs | 19 ++++++++++++++++++- crates/egui_kittest/src/lib.rs | 5 +++++ crates/egui_kittest/src/wgpu.rs | 21 +++++++++++++++++---- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index b33e37e39..a81e9dca0 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -18,6 +18,9 @@ pub struct HarnessBuilder { #[cfg(feature = "snapshot")] pub(crate) default_snapshot_options: crate::SnapshotOptions, + + #[cfg(feature = "wgpu")] + pub(crate) render_options: egui_wgpu::RendererOptions, } impl Default for HarnessBuilder { @@ -35,6 +38,9 @@ impl Default for HarnessBuilder { #[cfg(feature = "snapshot")] default_snapshot_options: crate::SnapshotOptions::default(), + + #[cfg(feature = "wgpu")] + render_options: egui_wgpu::RendererOptions::PREDICTABLE, } } } @@ -119,6 +125,16 @@ impl HarnessBuilder { self } + /// Configures the [`egui_wgpu::RendererOptions`] used by this harness. + /// + /// The default is [`egui_wgpu::RendererOptions::PREDICTABLE`]. + #[cfg(feature = "wgpu")] + #[inline] + pub fn with_render_options(mut self, options: egui_wgpu::RendererOptions) -> Self { + self.render_options = options; + self + } + /// Set the [`TestRenderer`] to use for rendering. /// /// By default, a [`LazyRenderer`] is used. @@ -133,7 +149,8 @@ impl HarnessBuilder { /// This sets up a [`crate::wgpu::WgpuTestRenderer`] with the default setup. #[cfg(feature = "wgpu")] pub fn wgpu(self) -> Self { - self.renderer(crate::wgpu::WgpuTestRenderer::default()) + let test_renderer = crate::wgpu::WgpuTestRenderer::with_render_options(self.render_options); + self.renderer(test_renderer) } /// Enable wgpu rendering with the given setup. diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index b9f7dc7c2..9b2319caf 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -116,6 +116,11 @@ impl<'a, State> Harness<'a, State> { #[cfg(feature = "snapshot")] default_snapshot_options, + + // rustfmt adds this weird indentation below. + // See: https://github.com/rust-lang/rustfmt/issues/5920 + #[cfg(feature = "wgpu")] + render_options: _, } = builder; let ctx = ctx.unwrap_or_default(); ctx.set_theme(theme); diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index a9f0de9ad..45152e81e 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -58,7 +58,10 @@ pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup { egui_wgpu::WgpuSetup::CreateNew(setup) } -pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { +pub fn create_render_state( + setup: WgpuSetup, + options: egui_wgpu::RendererOptions, +) -> egui_wgpu::RenderState { // No display handle needed for headless testing — we don't present to a window. let instance = pollster::block_on(setup.new_instance()); @@ -69,7 +72,7 @@ pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState { }, &instance, None, - egui_wgpu::RendererOptions::PREDICTABLE, + options, )) .expect("Failed to create render state") } @@ -89,14 +92,17 @@ impl WgpuTestRenderer { /// Create a new [`WgpuTestRenderer`] with the default setup. pub fn new() -> Self { Self { - render_state: create_render_state(default_wgpu_setup()), + render_state: create_render_state( + default_wgpu_setup(), + egui_wgpu::RendererOptions::PREDICTABLE, + ), } } /// Create a new [`WgpuTestRenderer`] with the given setup. pub fn from_setup(setup: WgpuSetup) -> Self { Self { - render_state: create_render_state(setup), + render_state: create_render_state(setup, egui_wgpu::RendererOptions::PREDICTABLE), } } @@ -115,6 +121,13 @@ impl WgpuTestRenderer { ); Self { render_state } } + + /// Create a new [`WgpuTestRenderer`] with custom render options. + pub fn with_render_options(options: egui_wgpu::RendererOptions) -> Self { + Self { + render_state: create_render_state(default_wgpu_setup(), options), + } + } } impl crate::TestRenderer for WgpuTestRenderer { From 8ada641ee2b7df6cf11f2346537a98293664dcb8 Mon Sep 17 00:00:00 2001 From: psyche <180074435+EtherealPsyche@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:28:06 +0800 Subject: [PATCH 55/89] perf: remove redundant clone in about.rs demo (#8057) As shown in the commit, this is exactly what it contains. There was a redundant and seemingly meaningless `.clone()` in `about.rs`, which I removed. Was it because the function signature of `Image::new()` was different in older versions of egui? Co-authored-by: EtherealPsyche --- crates/egui_demo_lib/src/demo/about.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/about.rs b/crates/egui_demo_lib/src/demo/about.rs index 853ebf490..227aff429 100644 --- a/crates/egui_demo_lib/src/demo/about.rs +++ b/crates/egui_demo_lib/src/demo/about.rs @@ -29,9 +29,8 @@ impl crate::View for About { ui.vertical_centered(|ui| { ui.add_space(4.0); - let egui_icon = egui::include_image!("../../data/egui-logo.svg"); ui.add( - egui::Image::new(egui_icon.clone()) + egui::Image::new(egui::include_image!("../../data/egui-logo.svg")) .max_height(30.0) .tint(ui.visuals().strong_text_color()), ); From 4a09782fce11c03b8afa30b8a4a5a2d0942b3303 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 4 Apr 2026 16:20:29 +0200 Subject: [PATCH 56/89] Enable a few more clippy lints (#8064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable these new clippy lints and fix all warnings: * `format_push_string` — use `write!` instead of `s += &format!(…)` to avoid extra allocations * `ignored_unit_patterns` — use `()` instead of `_` when matching unit * `missing_fields_in_debug` — ensure manual `Debug` impls account for all fields * `needless_raw_string_hashes` — remove unnecessary `r#` on string literals * `ref_option` — prefer `Option<&T>` over `&Option` in function signatures --- Cargo.toml | 5 +++ crates/eframe/src/native/file_storage.rs | 2 +- crates/egui-wgpu/src/lib.rs | 33 ++++++++++++++----- crates/egui-wgpu/src/setup.rs | 17 +++++++--- crates/egui/src/callstack.rs | 14 +++++--- crates/egui/src/containers/window.rs | 6 ++-- crates/egui/src/context.rs | 15 ++++++--- crates/egui/src/data/input.rs | 2 ++ .../text_selection/label_text_selection.rs | 11 +++++-- crates/egui/src/widgets/drag_value.rs | 8 ++--- crates/egui_demo_app/src/main.rs | 3 +- crates/egui_demo_app/src/wrap_app.rs | 3 +- crates/egui_extras/src/table.rs | 12 +++---- crates/egui_kittest/src/snapshot.rs | 8 ++--- examples/custom_3d_glow/src/main.rs | 8 ++--- examples/file_dialog/src/main.rs | 3 +- tests/test_background_logic/src/main.rs | 3 +- 17 files changed, 100 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6c6cb0c03..d589ef954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -218,10 +218,12 @@ flat_map_option = "warn" float_cmp_const = "warn" fn_params_excessive_bools = "warn" fn_to_numeric_cast_any = "warn" +format_push_string = "warn" from_iter_instead_of_collect = "warn" get_unwrap = "warn" if_let_mutex = "warn" ignore_without_reason = "warn" +ignored_unit_patterns = "warn" implicit_clone = "warn" implied_bounds_in_impls = "warn" imprecise_flops = "warn" @@ -270,6 +272,7 @@ mismatching_type_param_order = "warn" missing_assert_message = "warn" missing_enforced_import_renames = "warn" missing_errors_doc = "warn" +missing_fields_in_debug = "warn" missing_safety_doc = "warn" mixed_attributes_style = "warn" mut_mut = "warn" @@ -279,6 +282,7 @@ needless_continue = "warn" needless_for_each = "warn" needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" +needless_raw_string_hashes = "warn" negative_feature_names = "warn" non_std_lazy_statics = "warn" non_zero_suggestions = "warn" @@ -299,6 +303,7 @@ rc_mutex = "warn" readonly_write_lock = "warn" redundant_type_annotations = "warn" ref_as_ptr = "warn" +ref_option = "warn" ref_option_ref = "warn" ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index f26a6df74..13e22fa75 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -207,7 +207,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { profiling::scope!("ron::serialize"); if let Err(err) = ron::Options::default() .to_io_writer_pretty(&mut writer, &kv, config) - .and_then(|_| writer.flush().map_err(|err| err.into())) + .and_then(|()| writer.flush().map_err(|err| err.into())) { log::warn!("Failed to serialize app state: {err}"); } else { diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index eb04173b5..180b305be 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -427,37 +427,52 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { // > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: "" // > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: "" + use std::fmt::Write as _; + let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}"); if !name.is_empty() { - summary += &format!(", name: {name:?}"); + write!(summary, ", name: {name:?}").ok(); } if !driver.is_empty() { - summary += &format!(", driver: {driver:?}"); + write!(summary, ", driver: {driver:?}").ok(); } if !driver_info.is_empty() { - summary += &format!(", driver_info: {driver_info:?}"); + write!(summary, ", driver_info: {driver_info:?}").ok(); } if *vendor != 0 { #[cfg(not(target_arch = "wasm32"))] { - summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor)); + write!( + summary, + ", vendor: {} (0x{vendor:04X})", + parse_vendor_id(*vendor) + ) + .ok(); } #[cfg(target_arch = "wasm32")] { - summary += &format!(", vendor: 0x{vendor:04X}"); + write!(summary, ", vendor: 0x{vendor:04X}").ok(); } } if *device != 0 { - summary += &format!(", device: 0x{device:02X}"); + write!(summary, ", device: 0x{device:02X}").ok(); } if !device_pci_bus_id.is_empty() { - summary += &format!(", pci_bus_id: {device_pci_bus_id:?}"); + write!(summary, ", pci_bus_id: {device_pci_bus_id:?}").ok(); } if *subgroup_min_size != 0 || *subgroup_max_size != 0 { - summary += &format!(", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"); + write!( + summary, + ", subgroup_size: {subgroup_min_size}..={subgroup_max_size}" + ) + .ok(); } - summary += &format!(", transient_saves_memory: {transient_saves_memory}"); + write!( + summary, + ", transient_saves_memory: {transient_saves_memory}" + ) + .ok(); summary } diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index c5b3f0421..cf8b652c2 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -296,15 +296,22 @@ impl Clone for WgpuSetupCreateNew { impl std::fmt::Debug for WgpuSetupCreateNew { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + instance_descriptor, + display_handle, + power_preference, + native_adapter_selector, + device_descriptor: _, + } = self; f.debug_struct("WgpuSetupCreateNew") - .field("instance_descriptor", &self.instance_descriptor) - .field("display_handle", &self.display_handle) - .field("power_preference", &self.power_preference) + .field("instance_descriptor", instance_descriptor) + .field("display_handle", display_handle) + .field("power_preference", power_preference) .field( "native_adapter_selector", - &self.native_adapter_selector.is_some(), + &native_adapter_selector.is_some(), ) - .finish() + .finish_non_exhaustive() } } diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs index fef9b2166..b1239db01 100644 --- a/crates/egui/src/callstack.rs +++ b/crates/egui/src/callstack.rs @@ -1,3 +1,5 @@ +use std::fmt::Write as _; + #[derive(Clone)] struct Frame { /// `_main` is usually as the deepest depth. @@ -23,7 +25,7 @@ pub fn capture() -> String { if let Some(file_and_line) = &mut file_and_line && let Some(line_nr) = symbol.lineno() { - file_and_line.push_str(&format!(":{line_nr}")); + write!(file_and_line, ":{line_nr}").ok(); } let file_and_line = file_and_line.unwrap_or_default(); @@ -130,12 +132,14 @@ pub fn capture() -> String { if frame.depth + 1 < last_depth || last_depth + 1 < frame.depth { // Show that some frames were elided - formatted.push_str(&format!("{:widest_depth$} …\n", "")); + writeln!(formatted, "{:widest_depth$} …", "").ok(); } - formatted.push_str(&format!( - "{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}\n" - )); + writeln!( + formatted, + "{depth:widest_depth$}: {file_and_line:widest_file_line$} {name}" + ) + .ok(); last_depth = frame.depth; } diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index ae5fbfc8f..5ade37014 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -673,7 +673,7 @@ impl Window<'_> { title_bar.ui( &mut area_content_ui, - &content_response, + content_response.as_ref(), open.as_deref_mut(), &mut collapsing, collapsible, @@ -1256,7 +1256,7 @@ impl TitleBar { fn ui( self, ui: &mut Ui, - content_response: &Option, + content_response: Option<&Response>, open: Option<&mut bool>, collapsing: &mut CollapsingState, collapsible: bool, @@ -1300,7 +1300,7 @@ impl TitleBar { ui.visuals().text_color(), ); - if let Some(content_response) = &content_response { + if let Some(content_response) = content_response { // Paint separator between title and content: let content_rect = content_response.rect; if false { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index a8751bffc..824a5318c 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2428,6 +2428,7 @@ impl Context { #[cfg(debug_assertions)] fn debug_painting(&self) { #![expect(clippy::iter_over_hash_type)] // ok to be sloppy in debug painting + use std::fmt::Write as _; let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { let rect = widget.interact_rect; @@ -2500,13 +2501,17 @@ impl Context { for id in contains_pointer { let mut widget_text = format!("{id:?}"); if let Some(rect) = widget_rects.get(id) { - widget_text += - &format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense); + write!( + widget_text, + " {:?} {:?} {:?}", + rect.layer_id, rect.rect, rect.sense + ) + .ok(); } if let Some(info) = widget_rects.info(id) { - widget_text += &format!(" {info:?}"); + write!(widget_text, " {info:?}").ok(); } - debug_text += &format!("{widget_text}\n"); + writeln!(debug_text, "{widget_text}").ok(); } self.debug_text(debug_text); } @@ -2571,7 +2576,7 @@ impl Context { ); self.viewport(|vp| { for reason in &vp.output.request_discard_reasons { - warning += &format!("\n {reason}"); + write!(warning, "\n {reason}").ok(); } }); diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 5e1680334..00cf59cba 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -376,12 +376,14 @@ impl ViewportInfo { ui.label(opt_as_str(&visible)); ui.end_row(); + #[expect(clippy::ref_option)] fn opt_rect_as_string(v: &Option) -> String { v.as_ref().map_or(String::new(), |r| { format!("Pos: {:?}, size: {:?}", r.min, r.size()) }) } + #[expect(clippy::ref_option)] fn opt_as_str(v: &Option) -> String { v.as_ref().map_or(String::new(), |v| format!("{v:?}")) } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 1dc825ad3..3df17d17c 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -49,10 +49,15 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 { impl std::fmt::Debug for WidgetTextCursor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + widget_id, + ccursor, + pos: _, + } = self; f.debug_struct("WidgetTextCursor") - .field("widget_id", &self.widget_id.short_debug_format()) - .field("ccursor", &self.ccursor.index) - .finish() + .field("widget_id", &widget_id.short_debug_format()) + .field("ccursor", &ccursor.index) + .finish_non_exhaustive() } } diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 1297b614b..cca43ea2f 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -551,7 +551,7 @@ impl Widget for DragValue<'_> { if let Some(value_text) = value_text { // We were editing the value as text last frame, but lost focus. // Make sure we applied the last text value: - let parsed_value = parse(&custom_parser, &value_text); + let parsed_value = parse(custom_parser.as_ref(), &value_text); if let Some(mut parsed_value) = parsed_value { // User edits always clamps: parsed_value = clamp_value_to_range(parsed_value, range.clone()); @@ -591,7 +591,7 @@ impl Widget for DragValue<'_> { response.lost_focus() && !ui.input(|i| i.key_pressed(Key::Escape)) }; if update { - let parsed_value = parse(&custom_parser, &value_text); + let parsed_value = parse(custom_parser.as_ref(), &value_text); if let Some(mut parsed_value) = parsed_value { // User edits always clamps: parsed_value = clamp_value_to_range(parsed_value, range.clone()); @@ -733,8 +733,8 @@ impl Widget for DragValue<'_> { } } -fn parse(custom_parser: &Option>, value_text: &str) -> Option { - match &custom_parser { +fn parse(custom_parser: Option<&NumParser<'_>>, value_text: &str) -> Option { + match custom_parser { Some(parser) => parser(value_text), None => default_parser(value_text), } diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index a4ffdb5f9..38da5751d 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -38,7 +38,8 @@ fn main() { }); for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] { if !rust_log.contains(&format!("{loud_crate}=")) { - rust_log += &format!(",{loud_crate}=warn"); + use std::fmt::Write as _; + write!(rust_log, ",{loud_crate}=warn").ok(); } } diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 9f43f8624..64ecbce56 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -519,7 +519,8 @@ impl WrapApp { additional_info.push(format!("{} bytes", bytes.len())); } if !additional_info.is_empty() { - info += &format!(" ({})", additional_info.join(", ")); + use std::fmt::Write as _; + write!(info, " ({})", additional_info.join(", ")).ok(); } ui.label(info); diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 25257421a..f62b71122 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -996,7 +996,7 @@ impl<'a> TableBody<'a> { overline: false, response: &mut response, }); - self.capture_hover_state(&response, self.row_index); + self.capture_hover_state(response.as_ref(), self.row_index); let bottom_y = self.layout.cursor.y; if Some(self.row_index) == self.scroll_to_row { @@ -1078,7 +1078,7 @@ impl<'a> TableBody<'a> { overline: false, response: &mut response, }); - self.capture_hover_state(&response, row_index); + self.capture_hover_state(response.as_ref(), row_index); } if total_rows - max_row > 0 { @@ -1160,7 +1160,7 @@ impl<'a> TableBody<'a> { overline: false, response: &mut response, }); - self.capture_hover_state(&response, row_index); + self.capture_hover_state(response.as_ref(), row_index); break; } } @@ -1183,7 +1183,7 @@ impl<'a> TableBody<'a> { selected: false, response: &mut response, }); - self.capture_hover_state(&response, row_index); + self.capture_hover_state(response.as_ref(), row_index); cursor_y += (row_height + spacing.y) as f64; if Some(row_index) == self.scroll_to_row { @@ -1234,8 +1234,8 @@ impl<'a> TableBody<'a> { // Capture the hover information for the just created row. This is used in the next render // to ensure that the entire row is highlighted. - fn capture_hover_state(&self, response: &Option, row_index: usize) { - let is_row_hovered = response.as_ref().is_some_and(|r| r.hovered()); + fn capture_hover_state(&self, response: Option<&Response>, row_index: usize) { + let is_row_hovered = response.is_some_and(|r| r.hovered()); if is_row_hovered { self.layout .ui diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 28b5ac986..adbe32399 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -563,7 +563,7 @@ pub fn image_snapshot_options( options: &SnapshotOptions, ) { match try_image_snapshot_options(current, name, options) { - Ok(_) => {} + Ok(()) => {} Err(err) => { panic!("{err}"); } @@ -582,7 +582,7 @@ pub fn image_snapshot_options( #[track_caller] pub fn image_snapshot(current: &image::RgbaImage, name: impl Into) { match try_image_snapshot(current, name) { - Ok(_) => {} + Ok(()) => {} Err(err) => { panic!("{err}"); } @@ -884,14 +884,14 @@ impl Drop for SnapshotResults { #[expect(clippy::manual_assert)] if count >= 2 { panic!( - r#" + " Multiple SnapshotResults were dropped without being handled. In order to allow consistent snapshot updates, all snapshot results within a test should be merged in a single SnapshotResults instance. Usually this is handled internally in a harness. If you have multiple harnesses, you can merge the results using `Harness::take_snapshot_results` and `SnapshotResults::extend`. The SnapshotResult was constructed at {} - "#, + ", self.location ); } diff --git a/examples/custom_3d_glow/src/main.rs b/examples/custom_3d_glow/src/main.rs index 6b804ed1c..e0a3e3dd2 100644 --- a/examples/custom_3d_glow/src/main.rs +++ b/examples/custom_3d_glow/src/main.rs @@ -106,7 +106,7 @@ impl RotatingTriangle { let program = gl.create_program().expect("Cannot create program"); let (vertex_shader_source, fragment_shader_source) = ( - r#" + " const vec2 verts[3] = vec2[3]( vec2(0.0, 1.0), vec2(-1.0, -1.0), @@ -124,15 +124,15 @@ impl RotatingTriangle { gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); gl_Position.x *= cos(u_angle); } - "#, - r#" + ", + " precision mediump float; in vec4 v_color; out vec4 out_color; void main() { out_color = v_color; } - "#, + ", ); let shader_sources = [ diff --git a/examples/file_dialog/src/main.rs b/examples/file_dialog/src/main.rs index 33bda7a96..a2cef84e7 100644 --- a/examples/file_dialog/src/main.rs +++ b/examples/file_dialog/src/main.rs @@ -64,7 +64,8 @@ impl eframe::App for MyApp { additional_info.push(format!("{} bytes", bytes.len())); } if !additional_info.is_empty() { - info += &format!(" ({})", additional_info.join(", ")); + use std::fmt::Write as _; + write!(info, " ({})", additional_info.join(", ")).ok(); } ui.label(info); diff --git a/tests/test_background_logic/src/main.rs b/tests/test_background_logic/src/main.rs index ea80cfa9e..b1c559916 100644 --- a/tests/test_background_logic/src/main.rs +++ b/tests/test_background_logic/src/main.rs @@ -56,7 +56,8 @@ fn viewport_info(ctx: &egui::Context) -> String { ]; for (name, value) in flags { if let Some(value) = value { - s += &format!(" {name}={value}"); + use std::fmt::Write as _; + write!(s, " {name}={value}").ok(); } } s From 64341d9242403c482f8a791acc2c99e1077e6391 Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Mon, 6 Apr 2026 10:17:01 +0200 Subject: [PATCH 57/89] Update of selected dependencies (#8042) Update some dependencies to get rid of old ones (especially nice to get rid of windows-sys 0.45...). * Closes * [x] I have followed the instructions in the PR template --- Cargo.lock | 402 ++++++++++++++++++++++++----------------------------- deny.toml | 2 +- 2 files changed, 182 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec0b421f6..5814d9a85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,7 +135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", "serde", "version_check", @@ -159,23 +159,22 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-activity" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", "bitflags 2.9.4", "cc", - "cesu8", "jni", - "jni-sys", "libc", "log", "ndk", "ndk-context", "ndk-sys", "num_enum", - "thiserror 1.0.66", + "simd_cesu8", + "thiserror 2.0.18", ] [[package]] @@ -369,7 +368,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.38", + "rustix 0.38.44", "slab", "tracing", "windows-sys 0.59.0", @@ -401,7 +400,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 0.38.38", + "rustix 0.38.44", "tracing", ] @@ -428,7 +427,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.38", + "rustix 0.38.44", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -512,7 +511,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -625,9 +624,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] @@ -670,7 +669,7 @@ dependencies = [ "bitflags 2.9.4", "log", "polling", - "rustix 0.38.38", + "rustix 0.38.44", "slab", "thiserror 1.0.66", ] @@ -682,7 +681,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ "calloop", - "rustix 0.38.38", + "rustix 0.38.44", "wayland-backend", "wayland-client", ] @@ -705,12 +704,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -1273,7 +1266,7 @@ dependencies = [ "epaint", "log", "profiling", - "thiserror 2.0.17", + "thiserror 2.0.18", "type-map", "web-time", "wgpu", @@ -1589,9 +1582,9 @@ checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "euclid" -version = "0.22.11" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", ] @@ -1723,9 +1716,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4d2d0cf79d38430cc9dc9aadec84774bff2e1ba30ae2bf6c16cfce9385a23" +checksum = "73829a7b5c91198af28a99159b7ae4afbb252fb906159ff7f189f3a2ceaa3df2" dependencies = [ "bytemuck", "serde", @@ -1850,7 +1843,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" dependencies = [ - "rustix 1.0.8", + "rustix 1.1.4", "windows-targets 0.52.6", ] @@ -1871,19 +1864,19 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasip2", ] [[package]] @@ -2001,7 +1994,7 @@ dependencies = [ "hashbrown 0.16.1", "log", "presser", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", ] @@ -2376,25 +2369,61 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-macros", + "jni-sys 0.4.1", "log", - "thiserror 1.0.66", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] [[package]] name = "jobserver" @@ -2523,7 +2552,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.18", ] [[package]] @@ -2540,15 +2569,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2657,7 +2686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.52.0", ] @@ -2691,7 +2720,7 @@ dependencies = [ "once_cell", "rustc-hash 1.1.0", "spirv", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-ident", ] @@ -2702,7 +2731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ "bitflags 2.9.4", - "jni-sys", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2722,7 +2751,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -3196,9 +3225,9 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.18", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3418,7 +3447,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.38", + "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] @@ -3484,9 +3513,9 @@ checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -3587,6 +3616,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -3643,7 +3678,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.4", ] [[package]] @@ -3711,9 +3746,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -3726,7 +3761,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3867,29 +3902,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rustix" -version = "0.38.38" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "bitflags 2.9.4", - "errno", - "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "semver", ] [[package]] name = "rustix" -version = "1.0.8" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -4000,6 +4044,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -4093,6 +4143,22 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -4175,7 +4241,7 @@ dependencies = [ "libc", "log", "memmap2", - "rustix 0.38.38", + "rustix 0.38.44", "thiserror 1.0.66", "wayland-backend", "wayland-client", @@ -4301,7 +4367,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", "yaml-rust", ] @@ -4313,9 +4379,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.8", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4388,11 +4454,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4408,9 +4474,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -4535,26 +4601,17 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.2+spec-1.1.0" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1dfefef6a142e93f346b64c160934eb13b5594b84ab378133ac6815cb2bd57f" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" dependencies = [ "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -4566,12 +4623,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] @@ -4642,13 +4699,13 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -4890,12 +4947,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -4965,7 +5022,7 @@ checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 1.0.8", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -4978,7 +5035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ "bitflags 2.9.4", - "rustix 1.0.8", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -5000,7 +5057,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" dependencies = [ - "rustix 1.0.8", + "rustix 1.1.4", "wayland-client", "xcursor", ] @@ -5088,9 +5145,9 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.0.5" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf4f3c0ba838e82b4e5ccc4157003fb8c324ee24c058470ffb82820becbde98" +checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16" dependencies = [ "core-foundation 0.10.1", "jni", @@ -5171,7 +5228,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-wasm", @@ -5260,7 +5317,7 @@ dependencies = [ "raw-window-metal", "renderdoc-sys", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "wayland-sys", "web-sys", @@ -5294,22 +5351,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.9" @@ -5319,12 +5360,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" version = "0.62.2" @@ -5354,7 +5389,7 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", + "windows-link", "windows-result", "windows-strings", ] @@ -5366,7 +5401,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", - "windows-link 0.2.1", + "windows-link", "windows-threading", ] @@ -5392,12 +5427,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -5411,7 +5440,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -5420,7 +5449,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -5429,16 +5458,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-link", ] [[package]] @@ -5465,7 +5485,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] @@ -5474,22 +5494,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-link", ] [[package]] @@ -5510,11 +5515,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.1.3", + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -5531,15 +5536,9 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5552,12 +5551,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5570,12 +5563,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5600,12 +5587,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5618,12 +5599,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5636,12 +5611,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5654,12 +5623,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5704,7 +5667,7 @@ dependencies = [ "pin-project", "raw-window-handle", "redox_syscall 0.4.1", - "rustix 0.38.38", + "rustix 0.38.44", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -5734,13 +5697,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags 2.9.4", -] +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -5770,7 +5730,7 @@ dependencies = [ "libc", "libloading", "once_cell", - "rustix 1.0.8", + "rustix 1.1.4", "x11rb-protocol", ] diff --git a/deny.toml b/deny.toml index 2d3231535..fb6d88e66 100644 --- a/deny.toml +++ b/deny.toml @@ -52,12 +52,12 @@ skip = [ { name = "core-foundation" }, # version conflict between winit and wgpu ecosystems { name = "core-graphics-types" }, # version conflict between winit and wgpu ecosystems { name = "getrandom" }, # ring / rustls (and thus ehttp) still depend on getrandom 0.2 + { name = "jni-sys" }, # 0.3.1 depends on 0.4 { name = "kurbo" }, # Old version because of resvg { name = "redox_syscall" }, # old version via winit { name = "rustc-hash" }, # Small enough { name = "thiserror" }, # ecosystem is in the process of migrating from 1.x to 2.x { name = "thiserror-impl" }, # same as above - { name = "toml_datetime" }, # required while eco-system updates to toml 1.0 ] skip-tree = [ { name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15 From 33e89e33bec3e0009096323f0832e73265c29e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:24:50 +0800 Subject: [PATCH 58/89] Improve IME handling, add public method `owns_ime_events` on `Memory` (#7983) * Depends on #7967 * Closes #7485 * Should fix #7906 (This issue doesn't seem to have been resolved, but the author closed it; I personally don't have the environment to verify whether it is fixed.) * Replaces #4137, #4896, and partially #7810 * [x] I have followed the instructions in the PR template This PR started as a fix for #7485, but has since evolved into a broader rewrite of IME-related logic. ## Overview This PR primarily introduces a new public method, `owns_ime_events`, on [`Memory`], and refactors parts of [`TextEdit`] to integrate with it. Previously, each [`TextEdit`] widget independently determined whether to handle IME events and stored its own IME-related state. This approach made ownership-handling fragmented and was therefore error-prone. With this PR: - IME event ownership is centralized, ensuring that at most a single widget owns IME events per frame. - [`PlatformOutput`]'s `ime` field can be set to `None` for at least one frame when IME composition is interrupted, allowing the IME to be properly dismissed. ## Details Two new public methods are introduced on [`Memory`]: - `fn owns_ime_events(&self, id: Id) -> bool`: check IME event ownership for the current frame for the widget with the given `id`. - `fn interrupt_ime(&mut self)`: interrupt the current IME composition, if any. Since the newly added methods on [`Memory`] are public, other widgets can also participate in IME handling without risking ownership conflicts of IME events. I also added an internal (`pub(crate)`) field on [`TextEditState`], called `cursor_purpose`, to distinguish the role of the [`TextEdit`] cursor. Additionally, `egui::ImeEvent::Enabled` and `egui::ImeEvent::Disabled` have been removed, as they are no longer used anywhere. ## Demonstrations ### Windows: The Korean IME text duplication bug fixed in #4137 does not reappear.
With this PR Without this PR
Behavior Correct (no regression) Correct
Screencast ![win-kor-after](https://github.com/user-attachments/assets/1b080c8f-2031-406f-8781-aacafd5c879a) ![win-kor-before](https://github.com/user-attachments/assets/20258841-72fe-4652-b9a9-9b40e338ccf2)
### Windows: Chinese and Japanese IMEs now behave more consistently with the Korean IME in similar scenarios. This change does not matter much, as composition is rarely interrupted mid-process with these IMEs in typical usage.
With this PR Without this PR
Behavior Composition can be interrupted by clicking (like Korean IMEs) Composition can not interrupted by clicking
Screencast (Builtin Chinese IME) ![win-cmn-after](https://github.com/user-attachments/assets/2c76b0a9-da6d-48e1-84e0-47d9631f1196) ![win-cmn-before](https://github.com/user-attachments/assets/ea125fb8-c325-48d5-abaf-17d495b8f075)
Screencast (Builtin Japanese IME) ![win-jpn-after](https://github.com/user-attachments/assets/c69e5f48-65b1-4c0f-af4a-522d2f47b75d) ![win-jpn-before](https://github.com/user-attachments/assets/a0f1fdad-4f6c-40c2-af57-029f42acf6d5)
### macOS: was buggy, still buggy Likely due to this upstream bug in `winit`: https://github.com/rust-windowing/winit/issues/4432 Once `winit` is updated to a version that includes the fix, the behavior should become correct with this PR.
With this PR Without this PR
Behavior Buggy as before Buggy: Characters are duplicated
Screencast ![mac-kor-after](https://github.com/user-attachments/assets/c2bd90e8-e473-49c8-9537-c970c92889bf) ![mac-kor-before](https://github.com/user-attachments/assets/63b6cd8a-8903-4743-98bf-ee15296354ba)
### Wayland + iBus: Korean IME duplication bug fixed
With this PR Without this PR
Behavior Correct Buggy: Characters are duplicated
Screencast ![wayland-kor-after-2](https://github.com/user-attachments/assets/b154add5-a1ce-4e3a-b243-e72480820c1b) ![wayland-kor-before-2](https://github.com/user-attachments/assets/43b28374-f273-4b6f-9845-3efd96ec9a37)
### Wayland + iBus: #7485 is fixed
With this PR Without this PR
Behavior Correct Buggy: Only a single ASCII character can be typed after TextEdit is focused
Screencast ![wayland-7485-after](https://github.com/user-attachments/assets/ec33a54d-1d4e-40f9-8c82-202104bd2d85) ![wayland-7485-before](https://github.com/user-attachments/assets/20d2d395-03fd-4966-a376-87249a41aab3)
### Wayland + iBus: selection is also not broken This PR does not reintroduce the selection bug fixed in #7973.
With this PR
Behavior Correct
Screencast ![wayland-focus-after](https://github.com/user-attachments/assets/daa29197-f7f7-4a7b-b454-c28ee9afa9c1)
### X11 + Fcitx5: IME composition can be interrupted But due to #7975, the experience is still subpar. (Uncommitted text is lost after interruption.)
With this PR Without this PR
Screencast ![x11-after](https://github.com/user-attachments/assets/e626d9ed-89a2-4825-9cde-3a67723bcb82) ![x11-before](https://github.com/user-attachments/assets/da93b351-9488-4da9-aa56-b64190e84ec3)
[`Memory`]: https://docs.rs/egui/latest/egui/struct.Memory.html [`TextEdit`]: https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEdit.html [`PlatformOutput`]: https://docs.rs/egui/latest/egui/struct.PlatformOutput.html [`TextEditState`]: https://docs.rs/egui/latest/egui/widgets/text_edit/struct.TextEditState.html --- crates/eframe/src/web/text_agent.rs | 4 - crates/egui-winit/src/lib.rs | 64 +--------- crates/egui/src/data/input.rs | 7 ++ crates/egui/src/data/output.rs | 3 + crates/egui/src/memory/mod.rs | 60 ++++++++++ crates/egui/src/widgets/text_edit/builder.rs | 120 ++++++++++--------- crates/egui/src/widgets/text_edit/state.rs | 22 ++-- 7 files changed, 154 insertions(+), 126 deletions(-) diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index ac917329f..e3d4f8860 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -75,11 +75,7 @@ impl TextAgent { }; let on_composition_start = { - let input = input.clone(); move |_: web_sys::CompositionEvent, runner: &mut AppRunner| { - input.set_value(""); - let event = egui::Event::Ime(egui::ImeEvent::Enabled); - runner.input.raw.events.push(event); // Repaint moves the text agent into place, // see `move_to` in `AppRunner::handle_platform_output`. runner.needs_repaint.repaint_asap(); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 90f0311d5..99a9894f3 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -101,9 +101,6 @@ pub struct State { /// Only one touch will be interpreted as pointer at any time. pointer_touch_id: Option, - /// track ime state - has_sent_ime_enabled: bool, - #[cfg(feature = "accesskit")] pub accesskit: Option, @@ -150,8 +147,6 @@ impl State { simulate_touch_screen: false, pointer_touch_id: None, - has_sent_ime_enabled: false, - #[cfg(feature = "accesskit")] accesskit: None, @@ -689,17 +684,11 @@ impl State { // } match ime { - winit::event::Ime::Enabled => { - if cfg!(target_os = "linux") { - // This event means different things in X11 and Wayland, but we can just - // ignore it and enable IME on the preedit event. - // See - } else { - self.ime_event_enable(); - } - } - winit::event::Ime::Preedit(text, Some(_cursor)) => { - self.ime_event_enable(); + // [`winit::event::Ime::Enabled`] means different things in X11 and + // Wayland, but it doesn't matter to us. + // See + winit::event::Ime::Enabled | winit::event::Ime::Disabled => {} + winit::event::Ime::Preedit(text, _) => { self.egui_input .events .push(egui::Event::Ime(egui::ImeEvent::Preedit(text.clone()))); @@ -708,53 +697,10 @@ impl State { self.egui_input .events .push(egui::Event::Ime(egui::ImeEvent::Commit(text.clone()))); - self.ime_event_disable(); - } - winit::event::Ime::Disabled => { - self.ime_event_disable(); - } - winit::event::Ime::Preedit(_, None) => { - if cfg!(target_os = "macos") { - // On macOS, when the user presses backspace to delete the - // last character in an IME composition, `winit` only emits - // `winit::event::Ime::Preedit("", None)` without a - // preceding `winit::event::Ime::Preedit("", Some(0, 0))`. - // - // The current implementation of `egui::TextEdit` relies on - // receiving an `egui::ImeEvent::Preedit("")` to remove the - // last character in the composition in this case, so we - // emit it here. - // - // This is guarded to macOS-only, as applying it on other - // platforms is unnecessary and can cause undesired - // behavior. - // See: https://github.com/emilk/egui/pull/7973 - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Preedit(String::new()))); - } - - self.ime_event_disable(); } } } - pub fn ime_event_enable(&mut self) { - if !self.has_sent_ime_enabled { - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Enabled)); - self.has_sent_ime_enabled = true; - } - } - - pub fn ime_event_disable(&mut self) { - self.egui_input - .events - .push(egui::Event::Ime(egui::ImeEvent::Disabled)); - self.has_sent_ime_enabled = false; - } - /// Returns `true` if the event was sent to egui. pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool { if !self.is_pointer_in_window() && !self.any_pointer_button_down { diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 00cf59cba..7a104a95e 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -605,15 +605,22 @@ pub enum Event { #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum ImeEvent { /// Notifies when the IME was enabled. + #[deprecated = "No longer used by egui"] Enabled, /// A new IME candidate is being suggested. + /// + /// An empty preedit string indicates that the IME has been dismissed, while + /// a non-empty preedit string indicates that the IME is active. Preedit(String), /// IME composition ended with this final result. + /// + /// The IME is considered dismissed after this event. Commit(String), /// Notifies when the IME was disabled. + #[deprecated = "No longer used by egui"] Disabled, } diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 2d2c74430..ea3ff4eec 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -123,6 +123,9 @@ pub struct PlatformOutput { /// This is set if, and only if, the user is currently editing text. /// /// Useful for IME. + /// + /// This field should only be set by the widget that currently owns IME + /// events (see [`crate::Memory::owns_ime_events`]). pub ime: Option, /// The difference in the widget tree since last frame. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 08b08a462..34e0fe319 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -116,6 +116,22 @@ pub struct Memory { /// (e.g. relative to some other widget). #[cfg_attr(feature = "persistence", serde(skip))] popups: ViewportIdMap, + + /// When the last IME interruption was made. + #[cfg_attr(feature = "persistence", serde(skip))] + ime_interruption_time: ImeInterruptionTime, +} + +#[derive(Clone, Copy, Debug, Default)] +enum ImeInterruptionTime { + #[default] + None, + + /// The IME was interrupted in the current frame. + ThisFrame, + + /// The IME was interrupted in the previous frame. + LastFrame, } impl Default for Memory { @@ -133,6 +149,7 @@ impl Default for Memory { popups: Default::default(), everything_is_visible: Default::default(), add_fonts: Default::default(), + ime_interruption_time: Default::default(), }; slf.interactions.entry(slf.viewport_id).or_default(); slf.areas.entry(slf.viewport_id).or_default(); @@ -761,6 +778,16 @@ impl Memory { self.areas.entry(self.viewport_id).or_default(); + match self.ime_interruption_time { + ImeInterruptionTime::ThisFrame => { + self.ime_interruption_time = ImeInterruptionTime::LastFrame; + } + ImeInterruptionTime::LastFrame => { + self.ime_interruption_time = ImeInterruptionTime::None; + } + ImeInterruptionTime::None => {} + } + // self.interactions is handled elsewhere self.options.begin_pass(new_raw_input); @@ -875,9 +902,12 @@ impl Memory { /// Give keyboard focus to a specific widget. /// See also [`crate::Response::request_focus`]. + /// + /// Calling this will interrupt IME composition. #[inline(always)] pub fn request_focus(&mut self, id: Id) { self.focus_mut().focused_widget = Some(FocusWidget::new(id)); + self.interrupt_ime(); } /// Surrender keyboard focus for a specific widget. @@ -993,6 +1023,36 @@ impl Memory { pub(crate) fn focus_mut(&mut self) -> &mut Focus { self.focus.entry(self.viewport_id).or_default() } + + /// Check if the widget owns IME events. + /// + /// A widget should only consume IME events if this returns `true`. At most + /// one widget can own IME events for each frame. + pub fn owns_ime_events(&self, id: Id) -> bool { + let Some(focus) = self.focus() else { + return false; + }; + // We check across two frames because the widget that called + // `interrupt_ime` may run after other widgets that call this method + // within the same frame. + if matches!( + self.ime_interruption_time, + ImeInterruptionTime::ThisFrame | ImeInterruptionTime::LastFrame + ) { + return false; + } + focus.focused() == Some(id) + } + + /// Interrupt the current IME composition, if any. + /// + /// This causes [`Self::owns_ime_events`] to return `false` for all widgets + /// for the remainder of this frame and the next frame, giving time + /// for the IME to be dismissed (by making `platform_output.ime` be `None` + /// for at least one frame). + pub fn interrupt_ime(&mut self) { + self.ime_interruption_time = ImeInterruptionTime::ThisFrame; + } } /// State of an open popup. diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 6f4d9a044..9905a2a55 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -10,8 +10,11 @@ use crate::{ TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint, os::OperatingSystem, output::OutputEvent, - response, text_selection, - text_selection::{CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection}, + response, + text_edit::state::TextEditCursorPurpose, + text_selection::{ + self, CCursorRange, text_cursor_state::cursor_rect, visuals::paint_text_selection, + }, vec2, }; @@ -858,33 +861,23 @@ impl TextEdit<'_> { now - state.last_interaction_time, ); } - - // Set IME output (in screen coords) when text is editable and visible - let to_global = ui - .ctx() - .layer_transform_to_global(ui.layer_id()) - .unwrap_or_default(); - - ui.output_mut(|o| { - o.ime = Some(crate::output::IMEOutput { - rect: to_global * inner_rect, - cursor_rect: to_global * primary_cursor_rect, + if ui.memory(|mem| mem.owns_ime_events(id)) { + // Set IME output (in screen coords) when text is editable and visible + let to_global = ui + .ctx() + .layer_transform_to_global(ui.layer_id()) + .unwrap_or_default(); + ui.output_mut(|o| { + o.ime = Some(crate::output::IMEOutput { + rect: to_global * inner_rect, + cursor_rect: to_global * primary_cursor_rect, + }); }); - }); + } } } } - // Ensures correct IME behavior when the text input area gains or loses focus. - if state.ime_enabled && (response.gained_focus() || response.lost_focus()) { - state.ime_enabled = false; - if let Some(mut ccursor_range) = state.cursor.char_range() { - ccursor_range.secondary.index = ccursor_range.primary.index; - state.cursor.set_char_range(Some(ccursor_range)); - } - ui.input_mut(|i| i.events.retain(|e| !matches!(e, Event::Ime(_)))); - } - state.clone().store(ui.ctx(), id); if response.changed() { @@ -999,6 +992,11 @@ fn events( let events = ui.input(|i| i.filtered_events(&event_filter)); + let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id)); + if !owns_ime_events { + state.cursor_purpose = TextEditCursorPurpose::Selection; + } + for event in &events { let did_mutate_text = match event { // First handle events that only changes the selection cursor, not the text: @@ -1126,7 +1124,7 @@ fn events( .. } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), - Event::Ime(ime_event) => { + Event::Ime(ime_event) if owns_ime_events => { /// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")` /// might be emitted from different integrations to signify that /// the current IME composition should be cleared. @@ -1160,46 +1158,58 @@ fn events( } match ime_event { - ImeEvent::Enabled => { - state.ime_enabled = true; - state.ime_cursor_range = cursor_range; + #[expect(deprecated)] + ImeEvent::Enabled | ImeEvent::Disabled => None, + // Ignore `Preedit`/`Commit` events with empty text when + // there is no active IME composition. + // + // Some integrations may emit these events when there is no + // active IME composition (e.g. when `set_ime_allowed` or + // `set_ime_cursor_area` is called on `winit`'s `Window` on + // Wayland). Without this guard, they would clear any + // selected text. + // + // TODO(umajho): Ideally this would be handled by the + // integration, but since this guard is harmless for well- + // behaved integrations and also fixes the issue described + // above, it is good enough for now. + ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text) + if composition_text.is_empty() + && !matches!( + state.cursor_purpose, + TextEditCursorPurpose::ImeComposition + ) => + { + None + } + ImeEvent::Preedit(composition_text) | ImeEvent::Commit(composition_text) + if composition_text == "\n" || composition_text == "\r" => + { None } ImeEvent::Preedit(preedit_text) => { - if preedit_text == "\n" || preedit_text == "\r" { - None + state.cursor_purpose = if preedit_text.is_empty() { + TextEditCursorPurpose::Selection } else { - let mut ccursor = clear_preedit_text(text, &cursor_range); + TextEditCursorPurpose::ImeComposition + }; + let mut ccursor = clear_preedit_text(text, &cursor_range); - let start_cursor = ccursor; - if !preedit_text.is_empty() { - text.insert_text_at(&mut ccursor, preedit_text, char_limit); - } - state.ime_cursor_range = cursor_range; - Some(CCursorRange::two(start_cursor, ccursor)) + let start_cursor = ccursor; + if !preedit_text.is_empty() { + text.insert_text_at(&mut ccursor, preedit_text, char_limit); } + Some(CCursorRange::two(start_cursor, ccursor)) } ImeEvent::Commit(commit_text) => { - if commit_text == "\n" || commit_text == "\r" { - None - } else { - state.ime_enabled = false; + state.cursor_purpose = TextEditCursorPurpose::Selection; + let mut ccursor = clear_preedit_text(text, &cursor_range); - let mut ccursor = clear_preedit_text(text, &cursor_range); - - if !commit_text.is_empty() - && cursor_range.secondary.index - == state.ime_cursor_range.secondary.index - { - text.insert_text_at(&mut ccursor, commit_text, char_limit); - } - - Some(CCursorRange::one(ccursor)) + if !commit_text.is_empty() { + text.insert_text_at(&mut ccursor, commit_text, char_limit); } - } - ImeEvent::Disabled => { - state.ime_enabled = false; - None + + Some(CCursorRange::one(ccursor)) } } } diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 5827aac4b..48935c4f7 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -37,18 +37,14 @@ pub struct TextEditState { /// Controls the text selection. pub cursor: TextCursorState, + /// The purpose of the cursor. + #[cfg_attr(feature = "serde", serde(skip))] + pub(crate) cursor_purpose: TextEditCursorPurpose, + /// Wrapped in Arc for cheaper clones. #[cfg_attr(feature = "serde", serde(skip))] pub(crate) undoer: Arc>, - // If IME candidate window is shown on this text edit. - #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) ime_enabled: bool, - - // cursor range for IME candidate. - #[cfg_attr(feature = "serde", serde(skip))] - pub(crate) ime_cursor_range: CCursorRange, - // Text offset within the widget area. // Used for sensing and singleline text clipping. #[cfg_attr(feature = "serde", serde(skip))] @@ -82,3 +78,13 @@ impl TextEditState { self.set_undoer(TextEditUndoer::default()); } } + +#[derive(Clone, Default)] +pub(crate) enum TextEditCursorPurpose { + /// The cursor is used for text selection. + #[default] + Selection, + + /// The cursor is used for IME composition. + ImeComposition, +} From 16cad760a51497bb14202c4f61ca49e418f4f895 Mon Sep 17 00:00:00 2001 From: Gautier Cailly <109429289+gcailly@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:25:04 +0200 Subject: [PATCH 59/89] Integrate harfrust for text shaping (#8031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Related to #56 (Improve text — tracking issue) ## Summary This PR integrates [harfrust](https://crates.io/crates/harfrust) (a pure-Rust port of HarfBuzz) into epaint's text layout pipeline, replacing the character-by-character glyph positioning with proper OpenType text shaping. ### What this enables - **GPOS kerning**: most modern fonts only ship kerning in GPOS tables (not the legacy `kern` table). Pairs like "AV", "VA", "AT" are now properly tightened. - **GSUB substitutions**: ligatures (fi, fl), contextual alternates, and other OpenType features. - **Combining marks**: diacritics (e.g. É”Ģƒ) are positioned via anchor tables instead of being rendered as standalone replacement glyphs. ### Before/After #### Kerning, etc. before_main after_harfrust #### Ligatures before_closeup after_closeup ### Architecture The shaping integrates into the existing pipeline without changing the public API: 1. **`Font::segment_into_runs`** — segments text into contiguous runs by font face (grapheme-cluster aware, never splits combining sequences) 2. **`FontFace::shape_text`** — calls harfrust to shape each run, returning glyph IDs + positioned advances/offsets 3. **`layout_shaped_run`** — emits `Glyph` structs from the shaping output, with NOTDEF fallback to other font faces for missing glyphs 4. **Buffer recycling** — `FontsImpl` pools a `harfrust::UnicodeBuffer` to avoid per-layout allocations ### Disclaimer I'm far from being a good Rust programmer. Claude Code did most of the heavy lifting here. I did my best and used my limited knowledge to avoid making too many mistakes. If this PR isn't up to quality standards, please don't hesitate to close it. ## Test plan - [x] `cargo test -p epaint` — all 18 text tests pass, including 6 new ones - [x] `cargo clippy -p epaint --all-features` — clean - [x] `cargo fmt` — clean - [ ] Snapshot tests need regeneration (expected: shaping changes glyph positions) - New tests added: - `test_gpos_kerning` — verifies GPOS kerning tightens "AV", "VA", "AT" pairs - `test_combining_diacritics` — combining tilde doesn't add extra width - `test_shaping_basic_latin` — sanity check for Latin text - `test_shaping_empty_string` — empty input doesn't panic - `test_shaping_multiple_newlines` — newline splitting works correctly - `test_shaping_mixed_font_fallback` — Latin + emoji in same string --------- Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 62 +- Cargo.toml | 4 +- .../egui_demo_app/tests/snapshots/clock.png | 4 +- .../tests/snapshots/custom3d.png | 4 +- .../tests/snapshots/easymarkeditor.png | 4 +- .../tests/snapshots/imageviewer.png | 4 +- .../tests/snapshots/demos/BĆ©zier Curve.png | 4 +- .../tests/snapshots/demos/Clipboard Test.png | 4 +- .../tests/snapshots/demos/Code Editor.png | 4 +- .../tests/snapshots/demos/Code Example.png | 4 +- .../tests/snapshots/demos/Cursor Test.png | 4 +- .../tests/snapshots/demos/Dancing Strings.png | 4 +- .../tests/snapshots/demos/Drag and Drop.png | 4 +- .../tests/snapshots/demos/Extra Viewport.png | 4 +- .../tests/snapshots/demos/Font Book.png | 4 +- .../tests/snapshots/demos/Frame.png | 4 +- .../tests/snapshots/demos/Grid Test.png | 4 +- .../tests/snapshots/demos/Highlighting.png | 4 +- .../tests/snapshots/demos/ID Test.png | 4 +- .../snapshots/demos/Input Event History.png | 4 +- .../tests/snapshots/demos/Input Test.png | 4 +- .../snapshots/demos/Interactive Container.png | 4 +- .../tests/snapshots/demos/Layout Test.png | 4 +- .../snapshots/demos/Manual Layout Test.png | 4 +- .../tests/snapshots/demos/Misc Demos.png | 4 +- .../tests/snapshots/demos/Modals.png | 4 +- .../tests/snapshots/demos/Multi Touch.png | 4 +- .../tests/snapshots/demos/Painting.png | 4 +- .../tests/snapshots/demos/Panels.png | 4 +- .../tests/snapshots/demos/Popups.png | 4 +- .../tests/snapshots/demos/SVG Test.png | 4 +- .../tests/snapshots/demos/Scene.png | 4 +- .../tests/snapshots/demos/Screenshot.png | 4 +- .../tests/snapshots/demos/Scrolling.png | 4 +- .../tests/snapshots/demos/Sliders.png | 4 +- .../tests/snapshots/demos/Strip.png | 4 +- .../tests/snapshots/demos/Table.png | 4 +- .../snapshots/demos/Tessellation Test.png | 4 +- .../tests/snapshots/demos/Text Layout.png | 4 +- .../tests/snapshots/demos/TextEdit.png | 4 +- .../tests/snapshots/demos/Tooltips.png | 4 +- .../tests/snapshots/demos/Undo Redo.png | 4 +- .../tests/snapshots/demos/Window Options.png | 4 +- .../snapshots/demos/Window Resize Test.png | 4 +- .../snapshots/image_kerning/image_dark_x1.png | 4 +- .../snapshots/image_kerning/image_dark_x2.png | 4 +- .../image_kerning/image_light_x1.png | 4 +- .../image_kerning/image_light_x2.png | 4 +- .../snapshots/italics/image_dark_x1.00.png | 4 +- .../snapshots/italics/image_dark_x1.41.png | 4 +- .../snapshots/italics/image_dark_x2.00.png | 4 +- .../snapshots/italics/image_light_x1.00.png | 4 +- .../snapshots/italics/image_light_x1.41.png | 4 +- .../snapshots/italics/image_light_x2.00.png | 4 +- .../tests/snapshots/modals_1.png | 4 +- .../tests/snapshots/modals_2.png | 4 +- .../tests/snapshots/modals_3.png | 4 +- ...rop_should_prevent_focusing_lower_area.png | 4 +- .../snapshots/rendering_test/dpi_1.00.png | 4 +- .../snapshots/rendering_test/dpi_1.25.png | 4 +- .../snapshots/rendering_test/dpi_1.50.png | 4 +- .../snapshots/rendering_test/dpi_1.67.png | 4 +- .../snapshots/rendering_test/dpi_1.75.png | 4 +- .../snapshots/rendering_test/dpi_2.00.png | 4 +- .../tests/snapshots/tessellation_test.png | 3 - .../tessellation_test/Additive rectangle.png | 4 +- .../tessellation_test/Blurred stroke.png | 4 +- .../snapshots/tessellation_test/Blurred.png | 4 +- .../tessellation_test/Minimal rounding.png | 4 +- .../snapshots/tessellation_test/Normal.png | 4 +- .../Thick stroke, minimal rounding.png | 4 +- .../tessellation_test/Thin filled.png | 4 +- .../tessellation_test/Thin stroked.png | 4 +- .../tests/snapshots/text_selection_0.png | 4 +- .../tests/snapshots/text_selection_1.png | 4 +- .../snapshots/widget_gallery_dark_x1.png | 4 +- .../snapshots/widget_gallery_dark_x2.png | 4 +- .../snapshots/widget_gallery_light_x1.png | 4 +- .../snapshots/widget_gallery_light_x2.png | 4 +- .../tests/snapshots/combobox_closed.png | 4 +- .../tests/snapshots/combobox_opened.png | 4 +- .../tests/snapshots/menu/closed_hovered.png | 4 +- .../tests/snapshots/menu/opened.png | 4 +- .../tests/snapshots/menu/submenu.png | 4 +- .../tests/snapshots/menu/subsubmenu.png | 4 +- .../tests/snapshots/readme_example.png | 4 +- .../snapshots/should_wait_for_images.png | 4 +- .../tests/snapshots/test_masking.png | 4 +- .../tests/snapshots/test_scroll_initial.png | 4 +- .../tests/snapshots/test_shrink.png | 4 +- .../tests/snapshots/test_tooltip_hidden.png | 4 +- .../tests/snapshots/test_tooltip_shown.png | 4 +- crates/epaint/Cargo.toml | 3 + crates/epaint/src/text/font.rs | 145 ++-- crates/epaint/src/text/fonts.rs | 14 + crates/epaint/src/text/text_layout.rs | 665 +++++++++++++++--- .../tests/snapshots/atom_letter_spacing.png | 4 +- .../tests/snapshots/button_shortcut.png | 4 +- tests/egui_tests/tests/snapshots/grow_all.png | 4 +- ...orizontal_wrapped_multiline_row_height.png | 4 +- ...wrapped_multiline_row_height_reference.png | 4 +- .../hovering_should_preserve_text_format.png | 4 +- .../tests/snapshots/layout/atoms_image.png | 4 +- .../tests/snapshots/layout/atoms_minimal.png | 4 +- .../snapshots/layout/atoms_multi_grow.png | 4 +- .../tests/snapshots/layout/button.png | 4 +- .../tests/snapshots/layout/button_image.png | 4 +- .../layout/button_image_shortcut.png | 4 +- .../tests/snapshots/layout/checkbox.png | 4 +- .../snapshots/layout/checkbox_checked.png | 4 +- .../tests/snapshots/layout/drag_value.png | 4 +- .../tests/snapshots/layout/radio.png | 4 +- .../tests/snapshots/layout/radio_checked.png | 4 +- .../snapshots/layout/selectable_value.png | 4 +- .../layout/selectable_value_selected.png | 4 +- .../tests/snapshots/layout/slider.png | 4 +- .../tests/snapshots/layout/text_edit.png | 4 +- .../tests/snapshots/layout/text_edit_clip.png | 4 +- .../snapshots/layout/text_edit_no_clip.png | 4 +- .../layout/text_edit_placeholder_clip.png | 4 +- .../layout/text_edit_prefix_suffix.png | 4 +- .../egui_tests/tests/snapshots/max_width.png | 4 +- .../tests/snapshots/max_width_and_grow.png | 4 +- .../tests/snapshots/rotated_ellipse.png | 4 +- .../tests/snapshots/rotated_rect.png | 4 +- .../tests/snapshots/shrink_first_text.png | 4 +- .../tests/snapshots/shrink_last_text.png | 4 +- .../tests/snapshots/sides/default_long.png | 4 +- .../sides/default_long_fit_contents.png | 4 +- .../snapshots/sides/shrink_left_long.png | 4 +- .../sides/shrink_left_long_fit_contents.png | 4 +- .../snapshots/sides/shrink_right_long.png | 4 +- .../sides/shrink_right_long_fit_contents.png | 4 +- .../tests/snapshots/sides/wrap_left_long.png | 4 +- .../sides/wrap_left_long_fit_contents.png | 4 +- .../tests/snapshots/sides/wrap_right_long.png | 4 +- .../sides/wrap_right_long_fit_contents.png | 4 +- .../tests/snapshots/size_max_size.png | 4 +- .../snapshots/text_edit_delay_0_empty.png | 4 +- .../text_edit_delay_1_h_invisible.png | 4 +- .../tests/snapshots/text_edit_halign.png | 4 +- .../tests/snapshots/visuals/button.png | 4 +- .../tests/snapshots/visuals/button_image.png | 4 +- .../visuals/button_image_shortcut.png | 4 +- .../button_image_shortcut_selected.png | 4 +- .../tests/snapshots/visuals/checkbox.png | 4 +- .../snapshots/visuals/checkbox_checked.png | 4 +- .../tests/snapshots/visuals/drag_value.png | 4 +- .../tests/snapshots/visuals/radio.png | 4 +- .../tests/snapshots/visuals/radio_checked.png | 4 +- .../snapshots/visuals/selectable_value.png | 4 +- .../visuals/selectable_value_selected.png | 4 +- .../tests/snapshots/visuals/slider.png | 4 +- .../tests/snapshots/visuals/text_edit.png | 4 +- .../snapshots/visuals/text_edit_clip.png | 4 +- .../snapshots/visuals/text_edit_no_clip.png | 4 +- .../visuals/text_edit_placeholder_clip.png | 4 +- .../visuals/text_edit_prefix_suffix.png | 4 +- 158 files changed, 998 insertions(+), 502 deletions(-) delete mode 100644 crates/egui_demo_lib/tests/snapshots/tessellation_test.png diff --git a/Cargo.lock b/Cargo.lock index 5814d9a85..58817670a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1540,6 +1540,7 @@ dependencies = [ "emath", "epaint_default_fonts", "font-types", + "harfrust", "log", "mimalloc", "nohash-hasher", @@ -1551,6 +1552,8 @@ dependencies = [ "similar-asserts", "skrifa", "smallvec", + "unicode-general-category", + "unicode-segmentation", "vello_cpu", ] @@ -1658,12 +1661,9 @@ dependencies = [ [[package]] name = "fearless_simd" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" -dependencies = [ - "bytemuck", -] +checksum = "76258897e51fd156ee03b6246ea53f3e0eb395d0b327e9961c4fc4c8b2fa151a" [[package]] name = "file_dialog" @@ -2018,6 +2018,15 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + [[package]] name = "half" version = "2.6.0" @@ -2029,6 +2038,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "harfrust" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" +dependencies = [ + "bitflags 2.9.4", + "bytemuck", + "core_maths", + "read-fonts", + "smallvec", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -3732,6 +3754,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" dependencies = [ "bytemuck", + "core_maths", "font-types", ] @@ -4732,6 +4755,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" +[[package]] +name = "unicode-general-category" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -4899,28 +4928,41 @@ dependencies = [ ] [[package]] -name = "vello_common" -version = "0.0.6" +name = "vello_api" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +checksum = "a5088cd0113bc5332c753f24503825e3bc93e26c7883c9dc3ad9637bb62c4634" +dependencies = [ + "bytemuck", + "peniko", +] + +[[package]] +name = "vello_common" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986dc49a501a683477614bf07b8e7b6c79ae4828efce3bf22e51850f4a0a8a4c" dependencies = [ "bytemuck", "fearless_simd", + "guillotiere", "hashbrown 0.16.1", "log", "peniko", "skrifa", "smallvec", + "thiserror 2.0.18", ] [[package]] name = "vello_cpu" -version = "0.0.6" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +checksum = "a678f91c7524a3a9ac9a19df9f83552866ec70b2ca26441b916a6b219b6aa2de" dependencies = [ "bytemuck", "hashbrown 0.16.1", + "vello_api", "vello_common", ] diff --git a/Cargo.toml b/Cargo.toml index d589ef954..2f5d5cf50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ font-types = { version = "0.11.0", default-features = false, features = ["std"] glow = "0.17.0" glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } +harfrust = "0.5.2" home = "0.5.9" image = { version = "0.25.6", default-features = false } jiff = { version = "0.2.23", default-features = false } @@ -136,8 +137,9 @@ tokio = "1.49" toml = {version = "1.0.0", default-features = false } type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } +unicode-general-category = "1.1.0" unicode-segmentation = "1.12.0" -vello_cpu = { version = "0.0.6", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } +vello_cpu = { version = "0.0.7", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766 wasm-bindgen-futures = "0.4.58" wayland-cursor = { version = "0.31.11", default-features = false } diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 0c5228a26..347bdd568 100644 --- a/crates/egui_demo_app/tests/snapshots/clock.png +++ b/crates/egui_demo_app/tests/snapshots/clock.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63021012cccfca02d09aa424333453140ae4da3ae58fa32b422f6152ba25741c -size 335394 +oid sha256:288e11a1fa684575155826a760d5aecc5855e1f4b68bc8954441bf3ac015ee84 +size 335175 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index e14066758..6778d8b92 100644 --- a/crates/egui_demo_app/tests/snapshots/custom3d.png +++ b/crates/egui_demo_app/tests/snapshots/custom3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4470063fe210d2e5170d6609c2603fff1984b8ee76fb65a1f60a1c4cfdf46ce8 -size 92796 +oid sha256:d674918c635bfc865043f2123c0f5d4a671dd21ba7b878c056e817b19f2e8f00 +size 92770 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 5118065c4..f8237d940 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84f0e72ce337d56f3767ebed1ab6a47f3d27c9fbcce4d8a19aeab358e12920f5 -size 169664 +oid sha256:fa67354cfe4d9cb8774c8de614be5975853a4a817ceaf96c3ec7a968de638d8d +size 169740 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index ae238b029..c47f260e9 100644 --- a/crates/egui_demo_app/tests/snapshots/imageviewer.png +++ b/crates/egui_demo_app/tests/snapshots/imageviewer.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6030f2f3da3dbbdf8bf3eaf429f222acffb624c7696b654d8b6e64273d49be58 -size 99008 +oid sha256:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390 +size 98934 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png b/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png index 71bed3294..96d7706ee 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/BĆ©zier Curve.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bff16d453b960bb9abcdd9f72dab73f8f25da6339a0e1c310ed352f57080db93 -size 32426 +oid sha256:10d64e017d1d0eba736a4471d28b1602a0cb69d8e2ab53f4ee604b01c9343116 +size 32475 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png index f987b948d..c9a05a629 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Clipboard Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24f4a9745c60c0353ece5f8fc48200671dcb185f4f0b964bbe66bf4a2fe71d7a -size 27067 +oid sha256:ab0d730ce0cf5f1d79947601def4f60c0a015e23a5dcd780df65c7ddc7ae7156 +size 27194 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png index f4b7690fc..6134dfad9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Editor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75a9cd9a3315b236c23a53e890de1a821d39c3327813d06df85ba86d2ed50cc7 -size 26887 +oid sha256:6fccbda741a056ae30a7bfd7497f7b0adfb337bdb885d247fbdfef43cc24b54c +size 26948 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png index 8c6077a1c..d43e856aa 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Code Example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b849adf0ff9a06e1f7bfa22ef32b13224c7e62429cf675286110729156434d12 -size 76531 +oid sha256:26d247655398bae33c724ad3c3bdcab330d194093b07442708d5069e256f636b +size 76542 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png index d4c9508f9..60d3993ef 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Cursor Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4e33c7f817100d8414bba245ee7886354b86109f383d59e87a197e39501f0a0 -size 62604 +oid sha256:e3389491f9f5c54cfc2e295abe76ad53f223c31e9489e1d07a8323cf14fcf37d +size 62628 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png index 40153611c..c59717683 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Dancing Strings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93fcc271831167cb077f3de0a9f0e27037f9e5a2ce94e056bd6f1ede9890cb7e -size 27818 +oid sha256:6e42e801758bb9e6130a4e94bd0857d6f254e68a7dedebec5af5cb7f7d896068 +size 27822 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png index 049d585ec..f3c65337f 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Drag and Drop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d194db1705b36c1ee6189878a323a591555493c9319b7c2ddfc0cb0541055aca -size 20948 +oid sha256:41e68fd3a679e2a6e3dd81129de5aa1ded3a8f24cc7b1f2ba0b876240d309d9b +size 21019 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png index 1440d2be2..6a050e0e8 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Extra Viewport.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62af1b6d248b731b0930c51e481118ae01a5d8c08f27697ff121a47fefea83bf -size 10795 +oid sha256:3db502ec416b322e0f98e9737faa52d5d2fd308d47649710fffa0b0bc5996f52 +size 10783 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png index ac85845f9..b187b4cfd 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Font Book.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:deff441cd1d9142352f8759dff4b759f4572f0ddf93752349314da77abe4b254 -size 115028 +oid sha256:c39ae3420fe01d696a032d8d052c405c5623a9208fc673f5cf09188b1e6a539b +size 115447 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png index b8791ed2e..658cb0922 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Frame.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20ea4f93ee50c7a3585aef74c66d7700083ac1c16519b0704b70387849d9d2bc -size 25057 +oid sha256:19322248e8335301b2ce31fc1f1352993a374dda945d227784fecea2ee831761 +size 25088 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png index b8f0440b6..f6f9a3cda 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Grid Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b72a4c0e6d441190a7a156b8bba709e81b6c1fe7b0eacedc1ee7a3bfcf881f6 -size 99297 +oid sha256:98f8865d866a6f28ae3e3a16c815770ed691a031b1d06f7d3662a7e94f564606 +size 99318 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png index a965901d5..ab7de6c2d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Highlighting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08c40934d4bd2a239bdcc1928d1e5eba56bac03fdded2c85cf47b020d669f07e -size 18281 +oid sha256:39208c3c2c95f68fb37880b011f866bedd8dbccee33d163a725cb2a5cc6bb1b3 +size 18290 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png index abd7c485b..b899d7356 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82878e4150e38fdc4b2e78203c8c661c2d9e716ab32595c298392faf6ba96105 -size 113803 +oid sha256:d251ffd6c34e4ffa7b5de7fe5ae57a2d8f48ba7c2e03711da2c3f1a3fd84648c +size 113998 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png index 46a949571..5b3a63aeb 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Event History.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72d49d9a35e16e7413158ba14ce9cab762925f5f5e52fbbe16292de499a177f1 -size 25791 +oid sha256:4d959e17ee5c8a32534ab98b6f08db884b2fbf25476d8dc2dc90edc79bd87ea4 +size 25821 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png index 8c3634222..85c205be0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Input Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb7e3887cf12bd00ea09b297ac361562a961f64c15b28360bc87f72f270a4065 -size 51649 +oid sha256:c923f523cc77d678929f294b360f60b9f546ddec66d5317ad0eb44bd61a5f927 +size 51733 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png index d01e4bb7b..b7d365f3a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Interactive Container.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58cd3aba4392332a45f57c7dd90a9b5da386cb396c0c6319e7a7dae71e03ff30 -size 22563 +oid sha256:c9c98d7bfa08e22e217dd9e7031cc49e4b4486f1a9fdd223bd122be07af72365 +size 22550 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png index 0f50709d3..a4a82635b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26ffcf6b71108b82ce15d4cf3f9dd0ce9fe0b9563f02725fef1b74f40e749439 -size 47281 +oid sha256:68afa93605427e12fe527f3ca9613095664b4983f1f585a60f14bc2370c0a1f2 +size 47224 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png index 1ba4655fd..b0cb0c27b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Manual Layout Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:faedf9631149e231d510165215c24fccec50502d58000d5f893aa047a637a68f -size 23148 +oid sha256:33959851f1bd2386fcca8fb5700133d14d90db5a8f783fbfaa9f3aebb7b0d5b2 +size 23119 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png index f34ac0cd4..28e6d1845 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Misc Demos.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6b4c2e55c02fa4caf5f9f8bd2d8c0311cc4cbcf1fc2f568fe112e8e6125c675 -size 65308 +oid sha256:7b918565d66594fe53ed62bb14e357d3489335f3d037ccb088f198d944eb367d +size 65307 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png index 28fe7b683..7f0357df1 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Modals.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Modals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a65927cd8bd8d24e3ffbea8eb421eb22849b27dc77d36f8acd82bf5d5e63959 -size 33469 +oid sha256:f8c3c2912a3a11892e65f94792c77c79400a81d8c913109d39e8ce12f5b095c6 +size 33503 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png index 4e2c0a458..59609d76a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Multi Touch.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:115398e2b4b459afc2f1c49c4e60ef3f63e562650f05581372002c93030da632 -size 38374 +oid sha256:0c2c7b0d4913b59ef932f6d6349ddab5cc8619b2e8e9a8b5eb3e055a62e6ed60 +size 38382 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png index da06d85bf..4d4b5c355 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Painting.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Painting.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c595ee9b7ada33780178a6a35e26a98055a707f2ff99f6bb36e8db4ed819791 -size 18242 +oid sha256:9567f56f2dd030608798347e1ab755d95730ba5c5dd1721f1c61147be7216e87 +size 18304 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 70e7ce90e..553f8aa0b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a46457b23b7b32694564d03b42bccac2f017a756225bc54b508bb6fe2ad8ee7b -size 249548 +oid sha256:d19d694e6a70ab6acb45668b391751e82f7beab19f9918e23821c667e8cc9cdb +size 249733 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png index 0aab4baf8..6a18dc9e5 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Popups.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Popups.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c218115d305dfa6c9ab883ac6f3a21584b4840b3ba273ea765c8a8381d78935f -size 57181 +oid sha256:9e1c73e28020371429b3e03d540f72dbf886fd40f0ba08bb194e868bbb3c95ff +size 57230 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png index 4b560e20a..92d19c470 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/SVG Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fadea24444c402695db6cbc9e03aef8a0ed3c5db487a324fb255d38c14f73dce -size 19804 +oid sha256:c2facd3881e6b107a0dcce9d6e00008a3c9b0f31ad270c35357a87e487180f56 +size 19814 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png index 2d57b2074..de52524ac 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scene.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scene.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6105c95470d1342f9003ab03e71243b5e18a6f225261aee94b15f8f0501572c -size 33542 +oid sha256:0ad81eb762150360368a97858ef30bb0d5aff72e71743fa40c3fd4d70ec84cdc +size 33400 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png index 5517e03ef..14603f2f2 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2ce9062c5d1f0b0861d5df49ae64e56ba0e6501e8bd3f8a92c53aea748be78b -size 23629 +oid sha256:c22130891755ac095d73e0494f11ee8e89d0fd0c31a321d3afb969648ece11ef +size 23675 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png index 0924d3aa9..525f23435 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Scrolling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:927a497e8b6f9ce3b71dcb67086f477e19d327c163b2b8ad868af10009c2faf2 -size 172981 +oid sha256:465f33e53ebe15b776e2d6d0710f13f2453f00bab1f6ef4319b3491f1d1d3a26 +size 173487 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png index 28864a446..34ec70a65 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Sliders.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ffba8bb50b42e47f855f62682f6d5ec10bf67b01d3aa2e843f6bf787f150d0d -size 118562 +oid sha256:001f7a1310ddf37be4d9a7f56a95a3079f713b741824a348544561bb16c291fa +size 118614 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png index c28cac211..b9dea21f0 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Strip.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Strip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d8d87e4cb944def0dc28e3515243a8c1b07b9b0f88e802924d4381c1cda74db -size 26763 +oid sha256:8105f7c2b519716cc0a45dffd5f08980f53f35c6c6b788592e1d82506cdacccc +size 26665 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Table.png b/crates/egui_demo_lib/tests/snapshots/demos/Table.png index 727c01434..909b766b9 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Table.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Table.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db4c0cf1c4cdae3d416afce5c58efd1cc382be86431e547fa66bcc95a0a17ddb -size 76364 +oid sha256:a9e83d5e83e3003830f7f719b02dd93273733a9b72d388aa42083387d02c1a20 +size 76310 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png index c1b6f506a..b1ed6bcde 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tessellation Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57bf5220ae8f47485a07e9117abaaad36924d8c6c0f9e278cb05c455f342bff6 -size 70250 +oid sha256:3d46c87417c49c8462fac6c488e46b1482bbc75f70fe8f7af8391f0d5d28dac3 +size 70271 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index e9b302746..61065db1d 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c964d07a39ad286a562b53cdfe514d568d91955e6c1ca06a0cb5e45dbe3977e -size 60947 +oid sha256:4c47faa75a002577220a92efceab5c0ccee1ddaa17c2d2f3d6d1429dbf8fe718 +size 60887 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 6b1a9946b..81b49688b 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5e9d645e1decf7624bc8f031b5e28213e64fc5f568dd3eeb1768a57fcb988c3 -size 21814 +oid sha256:9c09529c3a1c26c8f28c00fc15cc5f495842862276870c24b5ee0713954f97fc +size 21916 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index e7ed07b04..bea2050ab 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57018beba5e4fb4f1e6de9c58bf898560b3a7669159d5bad91a4e2382ef57ce0 -size 64004 +oid sha256:9d69b9a12e777ee559a481aec012935a5bfb2ca8b0d48725ecd33e7f0880b2b8 +size 64273 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png index 92992cd83..728dc59b6 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Undo Redo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99fa5a5cb10c7d277eafb258af6019eda24a3c96075a50db321f52a521dede92 -size 13700 +oid sha256:f6c020860fe9cb7503cea548afbf298ebb9dc620133870b528fe7508d04150c8 +size 13691 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png index cdc1a43dd..951a6fd41 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Options.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1cc61413bcce62cc8e0a55460a974bb56ac40936cd2e5512c4a0e0c521eaaae4 -size 35874 +oid sha256:d033935ca18ebee7e3c35629233cc3e3a73766ac8c0627fcdd8a12660eed703c +size 35873 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png index c769f61bd..933343f25 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Window Resize Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:999f9cc006302b8951d97b510a02f1209969c376ecc7909ed5d7b46da27c0637 -size 483753 +oid sha256:77c9058b036770a644f1bfcf9ed1ba7a29a7c98107b1823474c55ccc2880e9e7 +size 485880 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png index e48dfae92..218a4e258 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c2990a81dfa8832f0cb1c4c0ce2f86e468a7a6f693e09efffa131ed3259e2e8 -size 15428 +oid sha256:2ef07080ca2aa10e128c646479b8b322a092d63f15b35c52ed59015e7c2a0f60 +size 15434 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png index 3f9da5333..8da2d19a9 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07d987ff87c9f41ec71ceea0caff25795bf4dff525ef4ef241d0ba786acee3e1 -size 35960 +oid sha256:4edb61c6619d5e44892fa29da0aa0a624306ae637dbbaa057e3fa47c14dc06bd +size 35988 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png index 84b808a83..1085f9969 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5c5ce0d46231d90ccb04e158947d793d99cd5cce911c72b960b6d04feba2134 -size 16122 +oid sha256:f8da0c2e37497968864a91fa6bef7c545c37791f4f9b788ea9a2f43dd4ac16b1 +size 16116 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png index ff848168a..d89daa1c0 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60dcd590b1d00361278b135ce9ef084c7382875c71c72b19fb6e23dba68f7902 -size 39279 +oid sha256:2a603fcb2eb97943d6be3239b91cacee093adcb55a6dd14af93a72fc8b3a61fa +size 39270 diff --git a/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.00.png index 5018d53c8..081f36a0e 100644 --- a/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7389e319d9153af313cc113d97b57d462da00feb0d5f99da211552af3ac7e18a -size 6704 +oid sha256:19c9ae55906e0ad18a9507898884ec31ef785c498fb8d10267ee848bd3f17186 +size 6705 diff --git a/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.41.png b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.41.png index 3a1b72bb3..0bd058217 100644 --- a/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.41.png +++ b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x1.41.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d4480dd34ed36c6bdbc2084843dd136448b3934c22b3df3e40314ba6324b5b39 -size 10306 +oid sha256:6f13116cb76e52620221b41c8dbd37b74a0da513402cbb2cb7e70c89027f5e31 +size 10313 diff --git a/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x2.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x2.00.png index 0f18eb109..330ecd636 100644 --- a/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/italics/image_dark_x2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:624bfa884431c35cc5d852b96653f13da17e60f8545471f9fb1c3bb85b40ffc8 -size 16555 +oid sha256:6869b180f2d1dcfd9fcbb215ed3febfbe548f30035560272a4e11e465f20c592 +size 16567 diff --git a/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.00.png index 6ebc6addc..86374449e 100644 --- a/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf665389ef43524e097a7ae4eec0aa01bb788bdbb306144f20f9133f74a64b2c -size 6941 +oid sha256:132f060b2e20b4cc0c55062367381f47856a5cc10310b903dbddc3790f475581 +size 6942 diff --git a/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.41.png b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.41.png index 54fdc064b..30b722245 100644 --- a/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.41.png +++ b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x1.41.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2480d0f49a929993de70612572b321457b2507c149a25112064cfc27840e6ee -size 11005 +oid sha256:e5ec63c5e2f5bb5c0048ee67df5b821a30f89ba35dbedf72e7bc91e04a5f8359 +size 11007 diff --git a/crates/egui_demo_lib/tests/snapshots/italics/image_light_x2.00.png b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x2.00.png index 138fd8c83..ca90da21d 100644 --- a/crates/egui_demo_lib/tests/snapshots/italics/image_light_x2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/italics/image_light_x2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e0013b499934f47370a3a20b3d3a19f8a8c6db360752a35a3fb1d676d122263 -size 18068 +oid sha256:491bb98b22cf56e50d3ed10a10649db15aaffb93d7e045dfecfa26e98f537c35 +size 18070 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_1.png b/crates/egui_demo_lib/tests/snapshots/modals_1.png index 96d31e11d..d1abde4fa 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_1.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:941582e2e20a9459db1f2cb7f07fa1930acfdb12cbbe7f96f9aafbeabf8b37f6 -size 47076 +oid sha256:8bd53b56322123940496700cbd2a73e336fd80eaf49dcb19a958888d66570ddb +size 47159 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_2.png b/crates/egui_demo_lib/tests/snapshots/modals_2.png index 3c0a88ee0..8f6976ea5 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_2.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2735a021f171f5c95888cda76e8668e1e023588c8c6c7cd382c03d8e31988fe3 -size 48209 +oid sha256:d15954e6183558141e05e1b566ec4a794d372503baf436da2a8c8c67299056f1 +size 48238 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_3.png b/crates/egui_demo_lib/tests/snapshots/modals_3.png index 28221255f..ab4a99a12 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_3.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:867bef6b55b73d127306a461e115b6f0047d582904999de80aeabae00e60c967 -size 44295 +oid sha256:1738ecf660979888c70b1046dd759fe0b082e4958814fb19077c4fed2fb4bdef +size 44338 diff --git a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png index bb1935741..cbac0d342 100644 --- a/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png +++ b/crates/egui_demo_lib/tests/snapshots/modals_backdrop_should_prevent_focusing_lower_area.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:936ec8b223ae7f0f32c640c127e1b6b14033bb7d168a4d1f0e6b3bd08a761e36 -size 44055 +oid sha256:dae7239dc065068147387eb313afdbaa3f0df8b81d060dbbc29f5a6a31ad76c8 +size 44309 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png index b48827b6a..ea32cd32f 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fba7387f5deba5e144e2106154b15ab956a50a418857bd34e16b306d7f1a29e4 -size 588252 +oid sha256:ce6e44aeb60b6cc2179e14c467edc2d570ea1adbce08511650fc733e39f7757c +size 590679 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png index d1286d6a1..1bff62c8d 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.25.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4656f3255d7859c07b269ff655eafe21bdddb949a07aa91477b826f6e2af8c28 -size 740616 +oid sha256:2edd6679d434df3407048472aaa95e410158f28e9a13c9bd88e3f8be95b08d9d +size 745771 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png index f1892bf26..39c2a6b36 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b18ff644ba5bd0c7f094bf8eac079d8a72bc6918638b1b110002f2f0a7a362cc -size 967860 +oid sha256:20cb796b59113853345c98e5238021a9c5c977c25689ee9201fdec1e4d98123d +size 973781 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png index c32762306..fb0da4a80 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.67.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:134caff5b8a4969055c32e8f51ca9c6eae1528b84d348691d860913e839de0d9 -size 1076746 +oid sha256:9658cc6b4dd2dd33356facc6b48cf69f9a86f976ac2dd4f16e362a2fd3f73827 +size 1081864 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png index d021a1e71..736c3dd67 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_1.75.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d731b4ce039315e096113f3c83168165020949e57564e641e778728e35901169 -size 1125286 +oid sha256:b5d5941c8fa0456f8a92fce4888e7a2888945f5df7cfed867ef073c439c7ad9f +size 1131030 diff --git a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png index 03d4fb69a..b4bf73cb7 100644 --- a/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png +++ b/crates/egui_demo_lib/tests/snapshots/rendering_test/dpi_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfac3518220555984d47c9fdfea2202a37102250aefcc2509794f337b3a7baae -size 1361407 +oid sha256:78ef6dba3dcf5e26a9289ce226165224c712c22f9b5dfe699b599c7d2531133b +size 1363589 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test.png deleted file mode 100644 index 7c65aef2c..000000000 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260 -size 72501 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 1d3f1785e..09763f516 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf21fe763e9762bca1b0f486e29a6024efcbc106a7f1ac195104acd0621cf8db -size 45107 +oid sha256:9c0bf76e2a4d60fd4ba302b2217beaa4b0627614ff7d23294c7f5e6b81a028c7 +size 45061 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png index 646eaa1e0..7254be665 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred stroke.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f09338e652b965cc9ae7bbb261845cd9c15d79f3d15f3c5b5326ef6d163b606 -size 86885 +oid sha256:de7db1296bdd127dfd827c6d1cc1a5a840391c3abb558be07faeb7e6612ed6e8 +size 86830 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index e667fc387..85a4fa0da 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e298244953653e46875053b12b4fe06ee692cb58fc131233ac4172677f0f8b44 -size 118961 +oid sha256:645519e1e4c196b985b6b422ddf1ba51e7e92d1b0c80972318dcf44dab3022d3 +size 118907 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png index 843c3ba3b..a3c23b831 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b9b36acf821cca71f97a3c8468fb925561f3bc2030742aef1e3c1d9e69ccc6f -size 51419 +oid sha256:465a3aa235ea69bce6650cf6e100ffc719bba67baa0cb13d7015a0fc54d53d9a +size 51376 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png index e738e22eb..ec1182d15 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5ad7a37546d48fc5426c32534a1c452fd0bf8280346dbe6e67ac26f17f3ba8a -size 54626 +oid sha256:aac429243686d35096760eb1b7c6a2bcad1c1d00a3341a858a6c71aaff2cf128 +size 54582 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png index d46e593ba..fa5ea442d 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thick stroke, minimal rounding.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0b61e9d1c2bcbf891a7acd4f3c1d2bd7524133d8165e7e7984998670de5a085 -size 55090 +oid sha256:8afb610d9e86b324db22849457eb23419c4f3c240e5ee951a13ce8901003bca8 +size 55053 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index 31a1dd365..ec2750465 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2e4975e9328a6d72f2c932daddfbb00cebdb2249aceb53f667d4060a1c0ea8a -size 36006 +oid sha256:8e1a958b753fee4bec405a74dd7727e5c477c946484ed086c5c4ffee058dd5e8 +size 35960 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png index db20010e0..b725ec08f 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin stroked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac6f9adeef92be9f69cb288ccafda8d522b8c3cde64352cd5369ae63668240c0 -size 35973 +oid sha256:1887d632d0994efec3dd7cecc41e3ad460c109baa1012d69e005cdc916e3cd24 +size 35925 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png index 7930dff48..a6d590fdf 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:344d90928510855dc718a2e36e31a97f084f1163ab750d0217fb8620469b621a -size 5276 +oid sha256:16e993bb27d32162fcf573cc4d977af775e6e0b94373fc5e8e1a9890453e508c +size 5278 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png index 8691211cb..3506845cb 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60449af267336663304e44e254d0984e037bebfa2d1efdf32234cab4374e8c79 -size 5301 +oid sha256:79676693e9f1804c7e0a32ffa9bec3bde281446ffc184b55a1a3fcf34074ac34 +size 5304 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png index 4495bf173..5ea006f1f 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef245aae271ccae628bb4171f7e601194c77fd18888ef2ea829bea75bd38b0e5 -size 64965 +oid sha256:8c0054dd1717833ae6f19726b58bffbf70bd4dc784b5f2774b194800a3adffed +size 64973 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png index 7c47f522d..c37f49163 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e621561567539ff24b4d22b53b65fac6cddae71d92fccd7800a90972a6de3e0e -size 151100 +oid sha256:9be7321d8184c3d06cfeb54410c1f839892c4f836767e9b1950e70306cde6c54 +size 151090 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png index 520895ff5..54f2d37fd 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6c2d538be7971169bbc4473945e6815eac8c5dd6372bc1f1897a032b6bca12b -size 59962 +oid sha256:f07c25c053e9c4d5f7416cc00e101e13b7007b65c46ba13d0853d95ca43fddc4 +size 59950 diff --git a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png index 90311fddc..318fb89e4 100644 --- a/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/widget_gallery_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d705af99624cd2824cd1f520fa05481ac67b8913feebae836db7b99ac60cb466 -size 145841 +oid sha256:b6a9bc30b4d6ed100555ee553554848d20d93f25643e72eeef1826483ed307d9 +size 145940 diff --git a/crates/egui_kittest/tests/snapshots/combobox_closed.png b/crates/egui_kittest/tests/snapshots/combobox_closed.png index 073ae79a3..7df09f4bd 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_closed.png +++ b/crates/egui_kittest/tests/snapshots/combobox_closed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00fb02e0cc2c1454d3a3dc0635be24086234c2bc5e2c9fd73741b179622e16d6 -size 4514 +oid sha256:698d1e9ecf490e04fb58ebf81cf9c633be29712abe172a6e6adfdc7fe442732d +size 4517 diff --git a/crates/egui_kittest/tests/snapshots/combobox_opened.png b/crates/egui_kittest/tests/snapshots/combobox_opened.png index 78e1baaca..20dd3e232 100644 --- a/crates/egui_kittest/tests/snapshots/combobox_opened.png +++ b/crates/egui_kittest/tests/snapshots/combobox_opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d8757e2db9a3892d9347495ad59f14d2bd9164a9ba258375a53c9faf8176b597 -size 8016 +oid sha256:84e857521c20ea2458d9f9d724578e0495c06e75571c25104bf64a0be0be617d +size 8028 diff --git a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png index 2a4621b0e..c498163ee 100644 --- a/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png +++ b/crates/egui_kittest/tests/snapshots/menu/closed_hovered.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38ee4acc23d9c66f127d377ac8a0dd3b683a1465ca319fba092f6d3cdff8c266 -size 11166 +oid sha256:aac8c973f26d03c25155f0454185dc3ab27fffae1c63443e36f07c49fb0828bf +size 11183 diff --git a/crates/egui_kittest/tests/snapshots/menu/opened.png b/crates/egui_kittest/tests/snapshots/menu/opened.png index c698cdb4b..4ef240cc9 100644 --- a/crates/egui_kittest/tests/snapshots/menu/opened.png +++ b/crates/egui_kittest/tests/snapshots/menu/opened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac1941f5eab71bfad020132eae47e1995efa17410b7861aa9f260032e5b0472c -size 21785 +oid sha256:8dd0d91d0866848e7965e88223ff3ddae2231728197fbcd3fb5b1a6b034e179d +size 21804 diff --git a/crates/egui_kittest/tests/snapshots/menu/submenu.png b/crates/egui_kittest/tests/snapshots/menu/submenu.png index f277511c1..20c6bed89 100644 --- a/crates/egui_kittest/tests/snapshots/menu/submenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/submenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1f1a4dd9de1d8405c527c7f8f04b42ed9d403d0ec507bb3ff650a6896f28df0 -size 28628 +oid sha256:11d0081c272986e6fbd00cde05c0931321bc9670d6a0fa3a650323cd7b56d795 +size 28667 diff --git a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png index dfc2b707c..01a316007 100644 --- a/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png +++ b/crates/egui_kittest/tests/snapshots/menu/subsubmenu.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:af05a9b66340e0c128d823d3935a23bcf17cfeac02a822e7277234a9c8eb26e0 -size 33393 +oid sha256:6fc08e05c0ab0e167503324fb7c82777afd5866384ae8bbacb9b933f717feb56 +size 33412 diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 050a4a43e..55a13d3e4 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1dd1f5013587463f002b1becac1560876c462295dbe5dfbb1a9dbce58991e53d -size 2209 +oid sha256:57211d5ac8021223ed288b9fd6f651ed901ec91599ed67d54ac540f3a2037937 +size 2205 diff --git a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png index 6ceffde99..461a2e695 100644 --- a/crates/egui_kittest/tests/snapshots/should_wait_for_images.png +++ b/crates/egui_kittest/tests/snapshots/should_wait_for_images.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfc03625c268f0ae067d2f4521a8668b47e4bc8525350d77a480840a09cd5083 -size 2046 +oid sha256:4ae52d6761a4d0b95b38ca4ca34c10751eef1ab2a54a62786c55bee3a3522ba1 +size 2050 diff --git a/crates/egui_kittest/tests/snapshots/test_masking.png b/crates/egui_kittest/tests/snapshots/test_masking.png index 5bf5dd6fa..6473e407e 100644 --- a/crates/egui_kittest/tests/snapshots/test_masking.png +++ b/crates/egui_kittest/tests/snapshots/test_masking.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:be0bd449166878ced27eff4966d1741731e926f9baabe8b590375c20103036dd -size 5527 +oid sha256:7ddac0bff10e6699c0ab2d9ea3679d5047d6099b7ba3540d1ebc144361f07d86 +size 5517 diff --git a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png index 64ed153a6..e03db834f 100644 --- a/crates/egui_kittest/tests/snapshots/test_scroll_initial.png +++ b/crates/egui_kittest/tests/snapshots/test_scroll_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6154c8bb550575bcb9fa0bba06da4d47079a00dffc5754b62ef2a6e7529e2090 -size 7489 +oid sha256:59939d3347679ff27c1de500a559cdc0e6387f633c86ec7ce51f2a36cd92547b +size 7511 diff --git a/crates/egui_kittest/tests/snapshots/test_shrink.png b/crates/egui_kittest/tests/snapshots/test_shrink.png index e4ff540f4..de27685a8 100644 --- a/crates/egui_kittest/tests/snapshots/test_shrink.png +++ b/crates/egui_kittest/tests/snapshots/test_shrink.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600 -size 2830 +oid sha256:db6f35e7dabe8f6d3766df022e5643f8461c3c2476959682622b793d59bf7c40 +size 2833 diff --git a/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png b/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png index e4ff540f4..de27685a8 100644 --- a/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png +++ b/crates/egui_kittest/tests/snapshots/test_tooltip_hidden.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888f8a4d995d718a9a158e563d8ac1434775660b33aebb5f34feea54ffd12600 -size 2830 +oid sha256:db6f35e7dabe8f6d3766df022e5643f8461c3c2476959682622b793d59bf7c40 +size 2833 diff --git a/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png b/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png index d6053700b..7396758ea 100644 --- a/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png +++ b/crates/egui_kittest/tests/snapshots/test_tooltip_shown.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:037f3e356d32e1a2c32767460399f919452bff0933e1db7aa113e7e2bdb083f0 -size 4927 +oid sha256:d785e2073627f33a2e7a23a47e0e0b8ca9570f45d4de3d8b545b473c1a9e75b5 +size 4940 diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 9d6e3eade..705f1f6a9 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -63,6 +63,7 @@ ecolor.workspace = true ahash.workspace = true font-types.workspace = true +harfrust.workspace = true log.workspace = true nohash-hasher.workspace = true parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. @@ -70,6 +71,8 @@ profiling.workspace = true self_cell.workspace = true skrifa.workspace = true smallvec.workspace = true +unicode-general-category.workspace = true +unicode-segmentation.workspace = true vello_cpu.workspace = true #! ### Optional dependencies diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 61f9f9f2f..48659fbe9 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -2,10 +2,7 @@ use emath::{GuiRounding as _, OrderedFloat, Vec2, vec2}; use self_cell::self_cell; -use skrifa::{ - MetadataProvider as _, - raw::{TableProvider as _, tables::kern::SubtableKind}, -}; +use skrifa::{GlyphId, MetadataProvider as _}; use std::collections::BTreeMap; use vello_cpu::{color, kurbo}; @@ -44,12 +41,10 @@ impl UvRect { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct GlyphInfo { - /// Used for pair-kerning. - /// /// Doesn't need to be unique. /// /// Is `None` for a special "invisible" glyph. - pub(crate) id: Option, + pub(crate) id: Option, /// In [`skrifa`]s "unscaled" coordinate system. pub advance_width_unscaled: OrderedFloat, @@ -124,17 +119,8 @@ impl SubpixelBin { } } -#[derive(Clone, Copy, Debug, PartialEq, Default)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct GlyphAllocation { - /// Used for pair-kerning. - /// - /// Doesn't need to be unique. - /// Use [`skrifa::GlyphId::NOTDEF`] if you just want to have an id, and don't care. - pub(crate) id: skrifa::GlyphId, - - /// Unit: screen pixels. - pub advance_width_px: f32, - /// UV rectangle for drawing. pub uv_rect: UvRect, } @@ -145,7 +131,7 @@ struct GlyphCacheKey(u64); impl nohash_hasher::IsEnabled for GlyphCacheKey {} impl GlyphCacheKey { - fn new(glyph_id: skrifa::GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self { + fn new(glyph_id: GlyphId, metrics: &StyledMetrics, bin: SubpixelBin) -> Self { let StyledMetrics { pixels_per_point, px_scale_factor, @@ -198,12 +184,10 @@ impl FontCell { &mut self, atlas: &mut TextureAtlas, metrics: &StyledMetrics, - glyph_info: &GlyphInfo, + glyph_id: GlyphId, bin: SubpixelBin, location: skrifa::instance::LocationRef<'_>, ) -> Option { - let glyph_id = glyph_info.id?; - debug_assert!( glyph_id != skrifa::GlyphId::NOTDEF, "Can't allocate glyph for id 0" @@ -288,11 +272,7 @@ impl FontCell { } }; - Some(GlyphAllocation { - id: glyph_id, - advance_width_px: glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor, - uv_rect, - }) + Some(GlyphAllocation { uv_rect }) } } @@ -337,6 +317,10 @@ pub struct FontFace { font: FontCell, tweak: FontTweak, + /// Cached `harfrust` shaper data (parsed GSUB/GPOS tables). + /// `ShaperData` is `Copy` — lives outside the `self_cell`. + shaper_data: harfrust::ShaperData, + glyph_info_cache: ahash::HashMap, glyph_alloc_cache: ahash::HashMap, } @@ -393,10 +377,13 @@ impl FontFace { }) })?; + let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa); + Ok(Self { name, font, tweak, + shaper_data, glyph_info_cache: Default::default(), glyph_alloc_cache: Default::default(), }) @@ -483,7 +470,7 @@ impl FontFace { let glyph_id = font_data .charmap .map(c) - .filter(|id| *id != skrifa::GlyphId::NOTDEF)?; + .filter(|id| *id != GlyphId::NOTDEF)?; let glyph_info = GlyphInfo { id: Some(glyph_id), @@ -497,38 +484,6 @@ impl FontFace { Some(glyph_info) } - #[inline] - pub(super) fn pair_kerning_pixels( - &self, - metrics: &StyledMetrics, - last_glyph_id: skrifa::GlyphId, - glyph_id: skrifa::GlyphId, - ) -> f32 { - let skrifa_font = &self.font.borrow_dependent().skrifa; - let Ok(kern) = skrifa_font.kern() else { - return 0.0; - }; - kern.subtables() - .find_map(|st| match st.ok()?.kind().ok()? { - SubtableKind::Format0(table_ref) => table_ref.kerning(last_glyph_id, glyph_id), - SubtableKind::Format1(_) => None, - SubtableKind::Format2(subtable2) => subtable2.kerning(last_glyph_id, glyph_id), - SubtableKind::Format3(table_ref) => table_ref.kerning(last_glyph_id, glyph_id), - }) - .unwrap_or_default() as f32 - * metrics.px_scale_factor - } - - #[inline] - pub fn pair_kerning( - &self, - metrics: &StyledMetrics, - last_glyph_id: skrifa::GlyphId, - glyph_id: skrifa::GlyphId, - ) -> f32 { - self.pair_kerning_pixels(metrics, last_glyph_id, glyph_id) / metrics.pixels_per_point - } - #[inline(always)] pub fn styled_metrics( &self, @@ -571,51 +526,67 @@ impl FontFace { } } + pub(crate) fn skrifa_font_ref(&self) -> &skrifa::FontRef<'_> { + &self.font.borrow_dependent().skrifa + } + + pub(crate) fn tweak(&self) -> &FontTweak { + &self.tweak + } + + pub(crate) fn shaper_data(&self) -> &harfrust::ShaperData { + &self.shaper_data + } + pub fn allocate_glyph( &mut self, atlas: &mut TextureAtlas, metrics: &StyledMetrics, - glyph_info: GlyphInfo, - chr: char, - h_pos: f32, + shaped: &ShapedGlyph, ) -> (GlyphAllocation, i32) { - let advance_width_px = glyph_info.advance_width_unscaled.0 * metrics.px_scale_factor; + let ShapedGlyph { + glyph_id, + h_pos, + is_cjk, + } = *shaped; - let Some(glyph_id) = glyph_info.id else { - // Invisible. - return (GlyphAllocation::default(), h_pos as i32); - }; + if glyph_id == GlyphId::NOTDEF { + // invisible + return (GlyphAllocation::default(), h_pos.round() as i32); + } - // CJK scripts contain a lot of characters and could hog the glyph atlas if we stored 4 subpixel offsets per - // glyph. - let (h_pos_round, bin) = if is_cjk(chr) { + let (h_pos_round, bin) = if is_cjk { + // CJK scripts contain a lot of characters and could hog the glyph atlas + // if we stored 4 subpixel offsets per glyph. (h_pos.round() as i32, SubpixelBin::Zero) } else { SubpixelBin::new(h_pos) }; - let entry = match self - .glyph_alloc_cache - .entry(GlyphCacheKey::new(glyph_id, metrics, bin)) - { - std::collections::hash_map::Entry::Occupied(glyph_alloc) => { - let mut glyph_alloc = *glyph_alloc.get(); - glyph_alloc.advance_width_px = advance_width_px; // Hack to get `\t` and thin space to work, since they use the same glyph id as ` ` (space). - return (glyph_alloc, h_pos_round); - } - std::collections::hash_map::Entry::Vacant(entry) => entry, - }; + let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin); - let allocation = self - .font - .allocate_glyph_uncached(atlas, metrics, &glyph_info, bin, (&metrics.location).into()) - .unwrap_or_default(); + let alloc = *self.glyph_alloc_cache.entry(cache_key).or_insert_with(|| { + self.font + .allocate_glyph_uncached(atlas, metrics, glyph_id, bin, (&metrics.location).into()) + .unwrap_or_default() + }); - entry.insert(allocation); - (allocation, h_pos_round) + (alloc, h_pos_round) } } +/// Positioning info for a single glyph, ready for atlas allocation. +#[derive(Clone, Copy, Debug)] +pub(crate) struct ShapedGlyph { + pub glyph_id: GlyphId, + + /// Horizontal position of the glyph origin, in physical pixels. + pub h_pos: f32, + + /// CJK glyphs skip subpixel positioning to save atlas space. + pub is_cjk: bool, +} + // TODO(emilk): rename? /// Wrapper over multiple [`FontFace`] (e.g. a primary + fallbacks for emojis) pub struct Font<'a> { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 5099e0085..135ddb6d5 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -765,6 +765,9 @@ pub struct FontsImpl { fonts_by_id: nohash_hasher::IntMap, fonts_by_name: ahash::HashMap, family_cache: ahash::HashMap, + + /// Recycled `harfrust` shaping buffer to avoid per-layout allocations. + shape_buffer: Option, } impl FontsImpl { @@ -798,6 +801,7 @@ impl FontsImpl { fonts_by_id, fonts_by_name, family_cache: Default::default(), + shape_buffer: Some(harfrust::UnicodeBuffer::new()), } } @@ -805,6 +809,16 @@ impl FontsImpl { self.atlas.options() } + /// Take the recycled shaping buffer (or create a new one if already taken). + pub fn take_shape_buffer(&mut self) -> harfrust::UnicodeBuffer { + self.shape_buffer.take().unwrap_or_default() + } + + /// Return a shaping buffer for reuse. + pub fn return_shape_buffer(&mut self, buffer: harfrust::UnicodeBuffer) { + self.shape_buffer = Some(buffer); + } + /// Get the right font implementation from [`FontFamily`]. pub fn font(&mut self, family: &FontFamily) -> Font<'_> { let cached_family = self.family_cache.entry(family.clone()).or_insert_with(|| { diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 0233c1c58..0f3089292 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -8,15 +8,35 @@ use crate::{ Color32, Mesh, Stroke, Vertex, stroke::PathStroke, text::{ - font::{StyledMetrics, is_cjk, is_cjk_break_allowed}, + TAB_SIZE, + font::{StyledMetrics, UvRect, is_cjk, is_cjk_break_allowed}, fonts::FontFaceKey, }, }; -use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals}; +use super::{ + FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals, + VariationCoords, + font::{Font, FontFace, ShapedGlyph}, +}; // ---------------------------------------------------------------------------- +/// Returns `true` if the character is a Unicode combining mark (categories Mn, Mc, Me). +/// +/// These characters modify the preceding base character and should not be +/// rendered as standalone replacement glyphs when the shaper can't handle them. +#[inline] +fn is_combining_mark(c: char) -> bool { + use unicode_general_category::{GeneralCategory, get_general_category}; + matches!( + get_general_category(c), + GeneralCategory::NonspacingMark + | GeneralCategory::SpacingMark + | GeneralCategory::EnclosingMark + ) +} + /// Represents GUI scale and convenience methods for rounding to pixels. #[derive(Clone, Copy)] struct PointScale { @@ -98,15 +118,21 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc) // For most of this we ignore the y coordinate: let mut paragraphs = vec![Paragraph::from_section_index(0)]; - for (section_index, section) in job.sections.iter().enumerate() { - layout_section( - fonts, - pixels_per_point, - &job, - section_index as u32, - section, - &mut paragraphs, - ); + { + let mut shape_buffer = fonts.take_shape_buffer(); + for (section_index, section) in job.sections.iter().enumerate() { + let mut font = fonts.font(§ion.format.font_id.family); + shape_buffer = layout_section( + &mut font, + shape_buffer, + pixels_per_point, + &job, + section_index as u32, + section, + &mut paragraphs, + ); + } + fonts.return_shape_buffer(shape_buffer); } let point_scale = PointScale::new(pixels_per_point); @@ -144,21 +170,198 @@ pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc) galley_from_rows(point_scale, job, rows, elided, intrinsic_size) } +/// Shared context for emitting shaped glyphs into a [`Paragraph`]. +struct ShapingContext { + pixels_per_point: f32, + font_size: f32, + line_height: f32, + extra_letter_spacing: f32, + section_index: u32, + font_metrics: StyledMetrics, + is_first_glyph_in_section: bool, + prev_cluster: Option, +} + +impl ShapingContext { + fn glyph( + &self, + chr: char, + physical_x: i32, + advance_width_px: f32, + face_metrics: &StyledMetrics, + uv_rect: UvRect, + ) -> Glyph { + Glyph { + chr, + pos: pos2(physical_x as f32 / self.pixels_per_point, f32::NAN), + advance_width: advance_width_px / self.pixels_per_point, + line_height: self.line_height, + font_face_height: face_metrics.row_height, + font_face_ascent: face_metrics.ascent, + font_height: self.font_metrics.row_height, + font_ascent: self.font_metrics.ascent, + uv_rect, + section_index: self.section_index, + first_vertex: 0, + } + } +} + +/// Produced by [`segment_into_runs`] for text shaping. +#[derive(Debug)] +struct TextRun { + /// Which font face should shape this run. + font_key: FontFaceKey, + + /// Byte range within the section text. + byte_range: std::ops::Range, +} + +/// Emit shaped glyphs from a [`harfrust::GlyphBuffer`] into a [`Paragraph`]. +fn layout_shaped_run( + font: &mut Font<'_>, + run: &TextRun, + run_text: &str, + glyph_buffer: &harfrust::GlyphBuffer, + face_metrics: &StyledMetrics, + ctx: &mut ShapingContext, + paragraph: &mut Paragraph, +) { + let px_scale = face_metrics.px_scale_factor; + + // Reset cluster tracking — cluster values are byte offsets within run_text, + // so they are not comparable across runs. + ctx.prev_cluster = None; + + for (info, pos) in glyph_buffer + .glyph_infos() + .iter() + .zip(glyph_buffer.glyph_positions()) + { + let glyph_id = skrifa::GlyphId::new(info.glyph_id); + let cluster = info.cluster; + let mut advance_width_px = pos.x_advance as f32 * px_scale; + let x_offset_px = pos.x_offset as f32 * px_scale; + let y_offset_px = -(pos.y_offset as f32 * px_scale); // harfrust Y+ up → screen Y+ down + + let chr = run_text + .get(cluster as usize..) + .and_then(|s| s.chars().next()) + .unwrap_or('\u{FFFD}'); // Unicode Replacement Character + + // Tab is a layout concept, not a glyph — the shaper doesn't know about tab stops. + // Override the advance width to TAB_SIZE Ɨ space width. + if chr == '\t' { + let (_, space_info) = font.glyph_info(' '); + let space_width_px = space_info.advance_width_unscaled.0 * px_scale; + advance_width_px = TAB_SIZE as f32 * space_width_px; + } + + // Apply extra_letter_spacing only at cluster boundaries, + // never between glyphs within the same cluster (e.g. base + mark). + let is_new_cluster = ctx.prev_cluster.is_none_or(|pc| pc != cluster); + if !ctx.is_first_glyph_in_section && is_new_cluster { + paragraph.cursor_x_px += ctx.extra_letter_spacing * ctx.pixels_per_point; + } + if is_new_cluster { + ctx.is_first_glyph_in_section = false; + } + ctx.prev_cluster = Some(cluster); + + let glyph = if glyph_id == skrifa::GlyphId::NOTDEF { + // The shaper couldn't map this character. Drop combining marks + // (Unicode category M) and duplicate NOTDEF glyphs within the same + // cluster — only the first base character gets a replacement glyph. + if is_combining_mark(chr) || !is_new_cluster { + continue; + } + + // Use the fallback font face (not run.font_key which returned NOTDEF). + let (fallback_key, glyph_info) = font.glyph_info(chr); + let fallback_metrics = font + .fonts_by_id + .get(&fallback_key) + .map(|ff| { + ff.styled_metrics(ctx.pixels_per_point, ctx.font_size, &Default::default()) + }) + .unwrap_or_default(); + let advance_width_px = + glyph_info.advance_width_unscaled.0 * fallback_metrics.px_scale_factor; + let (glyph_alloc, physical_x) = + if let Some(ff) = font.fonts_by_id.get_mut(&fallback_key) { + ff.allocate_glyph( + font.atlas, + &fallback_metrics, + &ShapedGlyph { + glyph_id: glyph_info.id.unwrap_or(skrifa::GlyphId::NOTDEF), + h_pos: paragraph.cursor_x_px, + is_cjk: is_cjk(chr), + }, + ) + } else { + Default::default() + }; + + paragraph.cursor_x_px += advance_width_px; + + ctx.glyph( + chr, + physical_x, + advance_width_px, + &fallback_metrics, + glyph_alloc.uv_rect, + ) + } else { + let (mut glyph_alloc, physical_x) = + if let Some(ff) = font.fonts_by_id.get_mut(&run.font_key) { + ff.allocate_glyph( + font.atlas, + face_metrics, + &ShapedGlyph { + glyph_id, + h_pos: paragraph.cursor_x_px + x_offset_px, + is_cjk: is_cjk(chr), + }, + ) + } else { + Default::default() + }; + + // Apply shaper y_offset — this varies per glyph instance so it + // is not part of the cached ShapedGlyph / GlyphAllocation. + glyph_alloc.uv_rect.offset.y += y_offset_px / ctx.pixels_per_point; + + paragraph.cursor_x_px += advance_width_px; + + ctx.glyph( + chr, + physical_x, + advance_width_px, + face_metrics, + glyph_alloc.uv_rect, + ) + }; + paragraph.glyphs.push(glyph); + } +} + // Ignores the Y coordinate. +#[must_use] fn layout_section( - fonts: &mut FontsImpl, + font: &mut Font<'_>, + mut shape_buffer: harfrust::UnicodeBuffer, pixels_per_point: f32, job: &LayoutJob, section_index: u32, section: &LayoutSection, out_paragraphs: &mut Vec, -) { +) -> harfrust::UnicodeBuffer { let LayoutSection { leading_space, byte_range, format, } = section; - let mut font = fonts.font(&format.font_id.family); + let font_size = format.font_id.size; let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords); let line_height = section @@ -169,76 +372,100 @@ fn layout_section( let mut paragraph = out_paragraphs.last_mut().unwrap(); if paragraph.glyphs.is_empty() { - paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs? + paragraph.empty_paragraph_height = line_height; } - paragraph.cursor_x_px += leading_space * pixels_per_point; - let mut last_glyph_id = None; + let section_text = &job.text[byte_range.clone()]; + let mut ctx = ShapingContext { + pixels_per_point, + font_size, + line_height, + extra_letter_spacing, + section_index, + font_metrics, + is_first_glyph_in_section: paragraph.glyphs.is_empty(), + prev_cluster: None, + }; + let mut runs = Vec::new(); - // Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes. - let mut current_font = FontFaceKey::INVALID; - let mut current_font_face_metrics = StyledMetrics::default(); - - for chr in job.text[byte_range.clone()].chars() { - if job.break_on_newline && chr == '\n' { + // Process each paragraph segment (split on newlines — the shaper can't handle them). + for (seg_idx, segment) in SplitOrWhole::new(section_text, job.break_on_newline).enumerate() { + if 0 < seg_idx { out_paragraphs.push(Paragraph::from_section_index(section_index)); paragraph = out_paragraphs.last_mut().unwrap(); - paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs? - } else { - let (font_id, glyph_info) = font.glyph_info(chr); - let mut font_face = font.fonts_by_id.get_mut(&font_id); - if current_font != font_id { - current_font = font_id; - current_font_face_metrics = font_face - .as_ref() - .map(|font_face| { - font_face.styled_metrics(pixels_per_point, font_size, &format.coords) - }) - .unwrap_or_default(); - } + paragraph.empty_paragraph_height = line_height; + ctx.is_first_glyph_in_section = true; + } - if let (Some(font_face), Some(last_glyph_id), Some(glyph_id)) = - (&font_face, last_glyph_id, glyph_info.id) - { - paragraph.cursor_x_px += font_face.pair_kerning_pixels( - ¤t_font_face_metrics, - last_glyph_id, - glyph_id, - ); + if segment.is_empty() { + continue; + } - // Only apply extra_letter_spacing to glyphs after the first one: - paragraph.cursor_x_px += extra_letter_spacing * pixels_per_point; - } + segment_into_runs(font, segment, &mut runs); - let (glyph_alloc, physical_x) = if let Some(font_face) = font_face.as_mut() { - font_face.allocate_glyph( - font.atlas, - ¤t_font_face_metrics, - glyph_info, - chr, - paragraph.cursor_x_px, - ) - } else { - Default::default() + let num_runs = runs.len(); + for (run_idx, run) in runs.iter().enumerate() { + let run_text = &segment[run.byte_range.clone()]; + let Some(font_face) = font.fonts_by_id.get(&run.font_key) else { + continue; }; - paragraph.glyphs.push(Glyph { - chr, - pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN), - advance_width: glyph_alloc.advance_width_px / pixels_per_point, - line_height, - font_face_height: current_font_face_metrics.row_height, - font_face_ascent: current_font_face_metrics.ascent, - font_height: font_metrics.row_height, - font_ascent: font_metrics.ascent, - uv_rect: glyph_alloc.uv_rect, - section_index, - first_vertex: 0, // filled in later - }); + let face_metrics = + font_face.styled_metrics(pixels_per_point, font_size, &format.coords); - paragraph.cursor_x_px += glyph_alloc.advance_width_px; - last_glyph_id = Some(glyph_alloc.id); + // Set buffer flags for paragraph boundary context. + let mut flags = harfrust::BufferFlags::empty(); + if run_idx == 0 { + flags |= harfrust::BufferFlags::BEGINNING_OF_TEXT; + } + if run_idx + 1 == num_runs { + flags |= harfrust::BufferFlags::END_OF_TEXT; + } + + let glyph_buffer = shape_text(font_face, run_text, &format.coords, shape_buffer, flags); + + layout_shaped_run( + font, + run, + run_text, + &glyph_buffer, + &face_metrics, + &mut ctx, + paragraph, + ); + + shape_buffer = glyph_buffer.clear(); + } + } + + shape_buffer +} + +/// Iterator that either splits on `'\n'` or yields the whole string once. +/// Avoids `Box` and `Vec<&str>` allocation. +enum SplitOrWhole<'a> { + Split(std::str::Split<'a, char>), + Whole(std::iter::Once<&'a str>), +} + +impl<'a> SplitOrWhole<'a> { + fn new(text: &'a str, split: bool) -> Self { + if split { + Self::Split(text.split('\n')) + } else { + Self::Whole(std::iter::once(text)) + } + } +} + +impl<'a> Iterator for SplitOrWhole<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + match self { + Self::Split(iter) => iter.next(), + Self::Whole(iter) => iter.next(), } } } @@ -479,33 +706,14 @@ fn replace_last_glyph_with_overflow_character( .unwrap_or_default(); let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() { - // Kern the overflow character properly - let pair_kerning = font_face - .as_mut() - .map(|font_face| { - if let (Some(prev_glyph_id), Some(overflow_glyph_id)) = ( - font_face.glyph_info(prev_glyph.chr).and_then(|g| g.id), - font_face.glyph_info(overflow_character).and_then(|g| g.id), - ) { - font_face.pair_kerning(&font_face_metrics, prev_glyph_id, overflow_glyph_id) - } else { - 0.0 - } - }) - .unwrap_or_default(); - - prev_glyph.max_x() + extra_letter_spacing + pair_kerning + prev_glyph.max_x() + extra_letter_spacing } else { 0.0 // TODO(emilk): heed paragraph leading_space 😬 }; - let replacement_glyph_width = font_face - .as_mut() - .and_then(|f| f.glyph_info(overflow_character)) - .map(|i| { - i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point - }) - .unwrap_or_default(); + let advance_width_px = + glyph_info.advance_width_unscaled.0 * font_face_metrics.px_scale_factor; + let replacement_glyph_width = advance_width_px / pixels_per_point; // Check if we're within width budget: if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width() @@ -519,9 +727,11 @@ fn replace_last_glyph_with_overflow_character( f.allocate_glyph( font.atlas, &font_face_metrics, - glyph_info, - overflow_character, - overflow_glyph_x * pixels_per_point, + &ShapedGlyph { + glyph_id: glyph_info.id.unwrap_or(skrifa::GlyphId::NOTDEF), + h_pos: overflow_glyph_x * pixels_per_point, + is_cjk: is_cjk(overflow_character), + }, ) }) .unwrap_or_default(); @@ -536,7 +746,7 @@ fn replace_last_glyph_with_overflow_character( row.glyphs.push(Glyph { chr: overflow_character, pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN), - advance_width: replacement_glyph_alloc.advance_width_px / pixels_per_point, + advance_width: advance_width_px / pixels_per_point, line_height, font_face_height: font_face_metrics.row_height, font_face_ascent: font_face_metrics.ascent, @@ -1060,6 +1270,90 @@ impl RowBreakCandidates { // ---------------------------------------------------------------------------- +/// Segment text into runs where each run uses a single font face. +/// +/// Grapheme clusters are never split across runs: if a combining mark +/// falls back to a different font than its base character, it stays +/// with the base character's font (the shaper will handle it). +/// +/// NOTE: Segmentation is by font face, not by Unicode script. A run may +/// mix scripts (e.g. Latin + Cyrillic) when they share the same font. +/// This is acceptable for scripts with similar shaping rules, but would +/// need script-aware splitting once RTL/bidi support is added. +/// +/// Results are appended to `out` (which is cleared first) to allow +/// the caller to reuse the allocation across calls. +fn segment_into_runs(font: &mut Font<'_>, text: &str, out: &mut Vec) { + use unicode_segmentation::UnicodeSegmentation as _; + + out.clear(); + + for (byte_offset, grapheme_str) in text.grapheme_indices(true) { + let byte_end = byte_offset + grapheme_str.len(); + + let base_char = grapheme_str.chars().next().unwrap_or(' '); + let (font_key, _) = font.glyph_info(base_char); + + if let Some(last_run) = out.last_mut() + && last_run.font_key == font_key + { + last_run.byte_range.end = byte_end; + continue; + } + out.push(TextRun { + font_key, + byte_range: byte_offset..byte_end, + }); + } +} + +/// Shape a text run and return the raw [`harfrust::GlyphBuffer`]. +/// +/// The caller should iterate `glyph_infos()` / `glyph_positions()` (both +/// `Copy` slices) and convert font units to pixels using `metrics.px_scale_factor`. +/// After iteration, recycle the buffer via `glyph_buffer.clear()`. +fn shape_text( + font_face: &FontFace, + text: &str, + coords: &VariationCoords, + mut buffer: harfrust::UnicodeBuffer, + flags: harfrust::BufferFlags, +) -> harfrust::GlyphBuffer { + let font_ref = font_face.skrifa_font_ref(); + let tweak = font_face.tweak(); + + // Build shaper with variable font instance if variation coordinates are set. + let variations: Vec = tweak + .coords + .as_ref() + .iter() + .chain(coords.as_ref().iter()) + .map(|&(tag, value)| harfrust::Variation { tag, value }) + .collect(); + + let instance = if variations.is_empty() { + None + } else { + Some(harfrust::ShaperInstance::from_variations( + font_ref, variations, + )) + }; + + let shaper = font_face + .shaper_data() + .shaper(font_ref) + .instance(instance.as_ref()) + .build(); + + buffer.set_flags(flags); + buffer.push_str(text); + buffer.guess_segment_properties(); + + shaper.shape(buffer, &[]) +} + +// ---------------------------------------------------------------------------- + #[cfg(test)] mod tests { @@ -1277,4 +1571,177 @@ mod tests { "Unexpected intrinsic size" ); } + + #[test] + fn test_combining_diacritics() { + // É”Ģƒ = U+0254 (LATIN SMALL LETTER OPEN O) + U+0303 (COMBINING TILDE) + // With text shaping, the combining tilde should NOT produce a separate + // advance — it should be positioned above ɔ via GPOS anchors. + // Note: the default fonts don't contain U+0254, so the replacement glyph + // is used. The key test is that the combining mark does NOT add extra width. + let pixels_per_point = 1.0; + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + + let job_combined = LayoutJob::simple( + "ɔ\u{0303}".to_owned(), + FontId::proportional(14.0), + Color32::WHITE, + f32::INFINITY, + ); + let galley_combined = layout(&mut fonts, pixels_per_point, job_combined.into()); + + let job_base = LayoutJob::simple( + "ɔ".to_owned(), + FontId::proportional(14.0), + Color32::WHITE, + f32::INFINITY, + ); + let galley_base = layout(&mut fonts, pixels_per_point, job_base.into()); + + let width_combined = galley_combined.size().x; + let width_base = galley_base.size().x; + + assert!( + (width_combined - width_base).abs() < 2.0, + "Combining diacritic should not add significant width. \ + Base width: {width_base}, Combined width: {width_combined}" + ); + + let glyphs = &galley_combined.rows[0].row.glyphs; + assert!(!glyphs.is_empty(), "Expected at least 1 glyph for É”Ģƒ"); + } + + #[test] + fn test_shaping_basic_latin() { + // Basic test: shaped Latin text should produce the same number of glyphs as characters. + let pixels_per_point = 1.0; + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + + let job = LayoutJob::simple( + "Hello".to_owned(), + FontId::proportional(14.0), + Color32::WHITE, + f32::INFINITY, + ); + let galley = layout(&mut fonts, pixels_per_point, job.into()); + + assert_eq!(galley.rows.len(), 1); + assert_eq!(galley.rows[0].row.glyphs.len(), 5); + assert!(galley.size().x > 0.0); + } + + #[test] + fn test_shaping_empty_string() { + let pixels_per_point = 1.0; + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + + let job = LayoutJob::simple( + String::new(), + FontId::proportional(14.0), + Color32::WHITE, + f32::INFINITY, + ); + let galley = layout(&mut fonts, pixels_per_point, job.into()); + + assert_eq!(galley.rows.len(), 1); + assert_eq!(galley.rows[0].row.glyphs.len(), 0); + } + + #[test] + fn test_shaping_multiple_newlines() { + let pixels_per_point = 1.0; + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + + let job = LayoutJob::simple( + "A\n\nB".to_owned(), + FontId::proportional(14.0), + Color32::WHITE, + f32::INFINITY, + ); + let galley = layout(&mut fonts, pixels_per_point, job.into()); + + assert_eq!(galley.rows.len(), 3, "Expected 3 rows for 'A\\n\\nB'"); + assert_eq!(galley.rows[0].row.glyphs.len(), 1); // "A" + assert_eq!(galley.rows[1].row.glyphs.len(), 0); // empty line + assert_eq!(galley.rows[2].row.glyphs.len(), 1); // "B" + } + + #[test] + fn test_shaping_mixed_font_fallback() { + // Text with both Latin and emoji should work without panicking, + // even though they use different font faces. + let pixels_per_point = 1.0; + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + + let job = LayoutJob::simple( + "Hi šŸŽ‰ bye".to_owned(), + FontId::proportional(14.0), + Color32::WHITE, + f32::INFINITY, + ); + let galley = layout(&mut fonts, pixels_per_point, job.into()); + + assert_eq!(galley.rows.len(), 1); + // "Hi " (3) + "šŸŽ‰" (1) + " bye" (4) = at least 8 glyphs + assert!( + galley.rows[0].row.glyphs.len() >= 8, + "Expected >= 8 glyphs, got {}", + galley.rows[0].row.glyphs.len() + ); + } + + #[test] + fn test_gpos_kerning() { + // GPOS kerning: pairs like "AV", "VA", "AT" should be tighter than + // the sum of individual character widths. Without text shaping, egui + // only uses the legacy `kern` table, so these pairs had diff ā‰ˆ 0. + // With harfrust, GPOS kerning applies proper negative adjustments. + let pixels_per_point = 1.0; + let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default()); + let font_id = FontId::proportional(14.0); + + for pair in ["AV", "VA", "AT"] { + let (pair_w, _, _) = measure_text(&mut fonts, pair, &font_id, pixels_per_point); + let chars: Vec = pair.chars().collect(); + let (w1, _, _) = measure_text( + &mut fonts, + &chars[0].to_string(), + &font_id, + pixels_per_point, + ); + let (w2, _, _) = measure_text( + &mut fonts, + &chars[1].to_string(), + &font_id, + pixels_per_point, + ); + let sum = w1 + w2; + let kern_adjustment = sum - pair_w; + + assert!( + kern_adjustment > 0.5, + "GPOS kerning for '{pair}': expected pair to be noticeably tighter \ + than sum of individuals. pair_width={pair_w:.2}, sum={sum:.2}, \ + kern_adjustment={kern_adjustment:.2} (should be > 0.5)", + ); + } + } + + fn measure_text( + fonts: &mut FontsImpl, + text: &str, + font_id: &FontId, + pixels_per_point: f32, + ) -> (f32, usize, Vec<(char, f32)>) { + let job = LayoutJob::simple( + text.to_owned(), + font_id.clone(), + Color32::WHITE, + f32::INFINITY, + ); + let galley = layout(fonts, pixels_per_point, job.into()); + let glyphs = &galley.rows[0].row.glyphs; + let details: Vec<_> = glyphs.iter().map(|g| (g.chr, g.advance_width)).collect(); + (galley.size().x, glyphs.len(), details) + } } diff --git a/tests/egui_tests/tests/snapshots/atom_letter_spacing.png b/tests/egui_tests/tests/snapshots/atom_letter_spacing.png index 89fba254e..8a022896b 100644 --- a/tests/egui_tests/tests/snapshots/atom_letter_spacing.png +++ b/tests/egui_tests/tests/snapshots/atom_letter_spacing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4aaf541ed0245777c802d31f01edb0cc4e53ebd2f4444e094336c180b98091d3 -size 2221 +oid sha256:14a8e05b81da82b086fe1ba006a39951a8bca3ff7a2b05c5385a425383b30961 +size 2207 diff --git a/tests/egui_tests/tests/snapshots/button_shortcut.png b/tests/egui_tests/tests/snapshots/button_shortcut.png index de7d64b4d..6ee227fca 100644 --- a/tests/egui_tests/tests/snapshots/button_shortcut.png +++ b/tests/egui_tests/tests/snapshots/button_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbf68b6934dae0868bc9cf0891baf5acf110284d297cfa348e756237fca64a28 -size 1564 +oid sha256:1cdfd5e248b3a1f2053f038a4e45c68f085ddbc085a6e1719b1c4c43b18f8d6c +size 1566 diff --git a/tests/egui_tests/tests/snapshots/grow_all.png b/tests/egui_tests/tests/snapshots/grow_all.png index 89b96aba7..9771b6ef1 100644 --- a/tests/egui_tests/tests/snapshots/grow_all.png +++ b/tests/egui_tests/tests/snapshots/grow_all.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83c3e19004462b793a5929f60f8b81a795c57529bfc74c6e87890aa4b9b8d939 -size 13930 +oid sha256:d74498e867f1ede9fa5e8769afeb24d7cfdd464db7accf0e4770aa94c7860bbc +size 13929 diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png index e6ac8e446..277c93280 100644 --- a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png +++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef21b42f90401f6b85685e1cc37d07970b38d2b40394f53bbde5bd4f0d54fb95 -size 5340 +oid sha256:1bf4b21569bb28659808ee668be82ac89275dfbbbefba3237560aa2bcbc986b2 +size 5339 diff --git a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png index a533a8401..60aede644 100644 --- a/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png +++ b/tests/egui_tests/tests/snapshots/horizontal_wrapped_multiline_row_height_reference.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5025f4cb528ae5edc387149f1d14523ab4b93058f0862e775a1c2276a3e77af6 -size 5377 +oid sha256:3b2a7bfced8ffd7d0002e52f98a01585c9667bc9821bc392e00fb890fb317696 +size 5376 diff --git a/tests/egui_tests/tests/snapshots/hovering_should_preserve_text_format.png b/tests/egui_tests/tests/snapshots/hovering_should_preserve_text_format.png index 672418f84..845dfb4ad 100644 --- a/tests/egui_tests/tests/snapshots/hovering_should_preserve_text_format.png +++ b/tests/egui_tests/tests/snapshots/hovering_should_preserve_text_format.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a5669c2c354c6ea42d8eaeb2eb39b65130a87807cbba8382dcc24d59790e794 -size 12181 +oid sha256:78daa3415e3012c0834819de8338dc380e262c5199f0e477b8796b6d11fe606f +size 12212 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_image.png b/tests/egui_tests/tests/snapshots/layout/atoms_image.png index 765e63f05..9bde044b4 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_image.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24c85a987b0b80961b656f386f529b7538ddee59a030d02a0946d0f714ce7004 -size 368329 +oid sha256:c6f941a45026d7842292366b70cfa3824fe16a9f78fd745b6fcd44c48e046a36 +size 369410 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png index 3e37969f7..6d77c9127 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_minimal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97b26c9abaf655fa5ef0625b8bc61042291a8ea18ecc89ea16abd3be6368c006 -size 367314 +oid sha256:a9ec7559bb6f57f446fd32f1fc342907ad4b888688195a5c147a2faaeaf5cd7a +size 369296 diff --git a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png index 54a8a3e1c..350194332 100644 --- a/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png +++ b/tests/egui_tests/tests/snapshots/layout/atoms_multi_grow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47b09261afe84892cdb169cb99ae59c49f671e68b3e99fc170e304de9b2bf526 -size 290633 +oid sha256:f0402dcf47b05586a9792d4583a7b7c76072b2f147a57b94bc7f3ba2b3471b26 +size 291012 diff --git a/tests/egui_tests/tests/snapshots/layout/button.png b/tests/egui_tests/tests/snapshots/layout/button.png index 635858aba..464daa5de 100644 --- a/tests/egui_tests/tests/snapshots/layout/button.png +++ b/tests/egui_tests/tests/snapshots/layout/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cbc6f95073cbbb26729d287e5fe073c76e8bddee7eef95b431a873522234297 -size 313244 +oid sha256:1bafb447c177f2bd94980c1d146a2826ecf62070fb6ee18e63e6a925e2741210 +size 314478 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image.png b/tests/egui_tests/tests/snapshots/layout/button_image.png index 6c63fb759..ce2844f8f 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f14f770785d01b1673d1c8ca780bfff72e51992794dc7233cf5ec4ea99cb3e9 -size 350648 +oid sha256:eadf74cc7f7e10be804df0e7df65c232ecaed7d77c2c89197548ab7206706843 +size 351669 diff --git a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png index 9c74cd8be..3a8d09f95 100644 --- a/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/layout/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:231ceab75a602eedcd11f4f4ed34f38fb9d072f5cb54e135a7e02d33d257f86b -size 433973 +oid sha256:60739a18972a5e866610ae2c8f5af719a1bbaeffcdd8c6319e783c37e28d3107 +size 435108 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox.png b/tests/egui_tests/tests/snapshots/layout/checkbox.png index 766aedaca..1f958fd57 100644 --- a/tests/egui_tests/tests/snapshots/layout/checkbox.png +++ b/tests/egui_tests/tests/snapshots/layout/checkbox.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f17fe1f7b2cccaa8991559218a7f13f61e459dc8443cf0fe2d24df7e9bd2eea -size 388959 +oid sha256:46d28fc14a4fb8e4a5fb962b17f6a1ad9b8c952453df62d49f846fda9945b409 +size 389363 diff --git a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png index 8f79ec659..3a2676aba 100644 --- a/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/layout/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:552a4d4933768ea1ee2323e7946f74f9ddd7e2f7b7c6d9f94bb92c8e7dd230a4 -size 416630 +oid sha256:2fb6db99b4dfa1b0c6f97799ffd72c264c02eefd803eee5737075231760c80bd +size 417925 diff --git a/tests/egui_tests/tests/snapshots/layout/drag_value.png b/tests/egui_tests/tests/snapshots/layout/drag_value.png index bfe289a61..105cd3453 100644 --- a/tests/egui_tests/tests/snapshots/layout/drag_value.png +++ b/tests/egui_tests/tests/snapshots/layout/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:339772a7974a2136b222697af2dd6e0202295d78e0720645204feb3c291481af -size 263181 +oid sha256:f54f444c25fbd1def8d0f11d03e1fd8bde6745a2593d0029e346cbaafa584ed3 +size 264007 diff --git a/tests/egui_tests/tests/snapshots/layout/radio.png b/tests/egui_tests/tests/snapshots/layout/radio.png index 2fbd917a8..043e6b1a7 100644 --- a/tests/egui_tests/tests/snapshots/layout/radio.png +++ b/tests/egui_tests/tests/snapshots/layout/radio.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:275c5358d3cfcbae7dfbeae4eac6606e2f394023837da492adc85934a972203e -size 325936 +oid sha256:5be1a82841c5d9916ad0c69eb08ca782797e42ee6f085a50ce2f5bcaa1ec917d +size 328013 diff --git a/tests/egui_tests/tests/snapshots/layout/radio_checked.png b/tests/egui_tests/tests/snapshots/layout/radio_checked.png index e95932c0d..6ef9b7b05 100644 --- a/tests/egui_tests/tests/snapshots/layout/radio_checked.png +++ b/tests/egui_tests/tests/snapshots/layout/radio_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bde6a904873ec2ffd7a194b820f3d76db5cacb3c266f3cb99f1c77ca2bd69fb -size 346473 +oid sha256:0cd7790301453097e1109a2f6600a986a12e0b02406471f12d8c1984395870e4 +size 347530 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value.png b/tests/egui_tests/tests/snapshots/layout/selectable_value.png index fd2daeeb0..b66e2e68a 100644 --- a/tests/egui_tests/tests/snapshots/layout/selectable_value.png +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0b87d78fce32144f1c694beb637461cb70b9127346c90d0276a877db0700291 -size 387935 +oid sha256:fd2d4946dcff38c5fbb1c3af64faa9686f93bd163f54eef0aef9c92084527671 +size 388797 diff --git a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png index 8ce768dae..a52c4de9a 100644 --- a/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/layout/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d4dd1a05771c25af933398d4f118e5e21a31b2e4db66161cf054fb1d7ebe24 -size 400911 +oid sha256:97c2f56dad8d6aa45385901f9b23a13c49c21fc5aa811ab50dcf7c8451b177dd +size 401596 diff --git a/tests/egui_tests/tests/snapshots/layout/slider.png b/tests/egui_tests/tests/snapshots/layout/slider.png index 83da462b7..306d2a97f 100644 --- a/tests/egui_tests/tests/snapshots/layout/slider.png +++ b/tests/egui_tests/tests/snapshots/layout/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4071301c08f980ee26d914e4a4724b3f46f1113c62495483d9b0df980d8cbcd -size 274770 +oid sha256:619139eb166d383cc9b35653c8c4a97c55cd21b171a24e253d923558cc0d35e1 +size 275512 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit.png b/tests/egui_tests/tests/snapshots/layout/text_edit.png index d5d853f5e..5936c68a7 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29363b37f1260f9f39edf9ba873f4c33c0d8a8b6670f6fc178459019539ae7e3 -size 220588 +oid sha256:260b2f77cfa51fbc5a948725d1219cfee8723362f228eb768255ee3f271faa39 +size 221505 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png index d87f37561..251ecc9ea 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94186c0b9331fd0d13284126f4f5e92e66014105fb6533422516d4fbe765e4c7 -size 372041 +oid sha256:35342ae9a83bcd19ba169c762f21f1667d9fcc781e6370b939e1781ff5a4e477 +size 372996 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png index e65f04b1c..8adc7676b 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_no_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aed677ddda9544258ddc58ed602655f6a62ab2d1d8342accd025593bbcb25e2f -size 506926 +oid sha256:01795b279efc5c680d77817f7d0e6e9f18dc3969ec964d8d3419d0175b0f4d87 +size 512262 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png index 578e4d9db..900845aff 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ff058ef716689c309ae9806aaf08fb64eca545ef8f92ce89e1f8e9b7b7733bc -size 330200 +oid sha256:263f38d4053119d3b5104575ceaba749d29582f6f021b7b9089054ecba4b22a0 +size 330923 diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png index bdcab38f2..439e89094 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf00e99dbfdf7497688955feb8c417fab0a366588d92182eccee775abade5179 -size 361876 +oid sha256:a40004fe56075f31162e16c7c59c00d7e1b8132bbea603b3c54c4dec0875b1bb +size 364491 diff --git a/tests/egui_tests/tests/snapshots/max_width.png b/tests/egui_tests/tests/snapshots/max_width.png index be50f81db..cc1d4f913 100644 --- a/tests/egui_tests/tests/snapshots/max_width.png +++ b/tests/egui_tests/tests/snapshots/max_width.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df3c1ba38afa30d22106d21a54621c28a0de2b98f77f4d7e398f09089286ef3e -size 8367 +oid sha256:67a7d4da3fd41cae45abc5c0dbc0257de64c8d136291b92a6885ab4d9589ff6e +size 8388 diff --git a/tests/egui_tests/tests/snapshots/max_width_and_grow.png b/tests/egui_tests/tests/snapshots/max_width_and_grow.png index d49489c41..abb0ceb28 100644 --- a/tests/egui_tests/tests/snapshots/max_width_and_grow.png +++ b/tests/egui_tests/tests/snapshots/max_width_and_grow.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5919c35a3d736e0c432b3a94d6ab2a2f936f71852b94f2f95475fa6ab5281ad -size 8369 +oid sha256:e15a6273a919fe06f7f096f0f7f4ef23eec739841cae58c9b27bd948ea025fef +size 8390 diff --git a/tests/egui_tests/tests/snapshots/rotated_ellipse.png b/tests/egui_tests/tests/snapshots/rotated_ellipse.png index e32f7864c..55a51d681 100644 --- a/tests/egui_tests/tests/snapshots/rotated_ellipse.png +++ b/tests/egui_tests/tests/snapshots/rotated_ellipse.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8f222733524b21969834a9ccc15aa9b0a4deb1d41e1086c80750f7cdd9711c8 -size 17324 +oid sha256:7591c9001a1b4b8cf1f19d7b3efa3bb1f43f7a695c1818b602e3c1f19b179a69 +size 17299 diff --git a/tests/egui_tests/tests/snapshots/rotated_rect.png b/tests/egui_tests/tests/snapshots/rotated_rect.png index 52255aa7f..d09ee268f 100644 --- a/tests/egui_tests/tests/snapshots/rotated_rect.png +++ b/tests/egui_tests/tests/snapshots/rotated_rect.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb4f1d10aa664e04da4b2e38c52cb6516a4c43a98884c9223e15266ea28ccd3d -size 14191 +oid sha256:816105d33ea4233c62a577377a1bf447bf5022ce4df553480924aee97fc374c1 +size 14153 diff --git a/tests/egui_tests/tests/snapshots/shrink_first_text.png b/tests/egui_tests/tests/snapshots/shrink_first_text.png index a623d8b3b..7b9ef4003 100644 --- a/tests/egui_tests/tests/snapshots/shrink_first_text.png +++ b/tests/egui_tests/tests/snapshots/shrink_first_text.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73b1cc01da110554dd41f4e5134f5d6d34b7e2079d5ac776f40980d616481ffc -size 11448 +oid sha256:224d55c987c8528bb6f9d051893572fbbb36e82bb5acec8b9614784d25093af1 +size 11383 diff --git a/tests/egui_tests/tests/snapshots/shrink_last_text.png b/tests/egui_tests/tests/snapshots/shrink_last_text.png index cda1a2add..f1fa64de7 100644 --- a/tests/egui_tests/tests/snapshots/shrink_last_text.png +++ b/tests/egui_tests/tests/snapshots/shrink_last_text.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00e129a40ea9815472ab9d823a1801fbdd268bd58745cad1c1c3dd91309c61fc -size 12010 +oid sha256:cee1c5d11e895f997b95606bf2f7c789dbff11e3e95327ab6e958586c334f5b0 +size 11932 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long.png b/tests/egui_tests/tests/snapshots/sides/default_long.png index ae862c32d..695e679fc 100644 --- a/tests/egui_tests/tests/snapshots/sides/default_long.png +++ b/tests/egui_tests/tests/snapshots/sides/default_long.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c970aab8c09558b806c81f57fc1d695992cb9f6e735a3fb2be75997c106a141 -size 8214 +oid sha256:a1ebcf48076404bf375d9cb66fed752fab48e83adcaa3a08372340f5e68caf2f +size 8111 diff --git a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png index 534b55d92..308a273d6 100644 --- a/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/default_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 -size 8833 +oid sha256:b844510cda2e4ba73adf0fb711299d268eedbd05e8a84ea04ce73451093a3b63 +size 8786 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png index ebf7424c3..451859fa0 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:46bca727290bb0fc5a9a28137385e7ee4821390d1594704ce5e0ea089f28dacf -size 7079 +oid sha256:140fe4f323fbcd752d1f091c3095674e5392c9de72bfc43ee7032546aca5ce8a +size 7035 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png index 534b55d92..308a273d6 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_left_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 -size 8833 +oid sha256:b844510cda2e4ba73adf0fb711299d268eedbd05e8a84ea04ce73451093a3b63 +size 8786 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png index d1cfeb533..2fa858ad9 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:841f69878a4b9331f8ab4730d212384a82a9de14b9fba0d6964cd3010900132a -size 6939 +oid sha256:059ea9c691e07377307aca905d2ad70797362138228c6bd173e80951d2170ff0 +size 6833 diff --git a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png index 534b55d92..308a273d6 100644 --- a/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/shrink_right_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 -size 8833 +oid sha256:b844510cda2e4ba73adf0fb711299d268eedbd05e8a84ea04ce73451093a3b63 +size 8786 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png index be67eaf7a..757d74fb5 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:602bc370e3929995c9b17415b513b412e0e12433f2c2b9120c58ea63c747ed79 -size 9184 +oid sha256:b35bec0c6bd97d9941fc4ffd1072869a8875eef0080f6348b036e885c37ceeb1 +size 9073 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png index 534b55d92..308a273d6 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_left_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 -size 8833 +oid sha256:b844510cda2e4ba73adf0fb711299d268eedbd05e8a84ea04ce73451093a3b63 +size 8786 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png index cb31d61e1..0e6711016 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9165daef8acd038a1527192ded0b7cd5d03f235be737308ade467df33b6c8a0 -size 9192 +oid sha256:6e478664951aa3561e74573a0e28a977c00e43db8730b086299fd33c5b4c52da +size 9099 diff --git a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png index 534b55d92..308a273d6 100644 --- a/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png +++ b/tests/egui_tests/tests/snapshots/sides/wrap_right_long_fit_contents.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d7c49df327cdea8cc7d6a0b7278a831574a38e8998dba0733fcae2fd44256a9 -size 8833 +oid sha256:b844510cda2e4ba73adf0fb711299d268eedbd05e8a84ea04ce73451093a3b63 +size 8786 diff --git a/tests/egui_tests/tests/snapshots/size_max_size.png b/tests/egui_tests/tests/snapshots/size_max_size.png index 499259fd4..d157d2053 100644 --- a/tests/egui_tests/tests/snapshots/size_max_size.png +++ b/tests/egui_tests/tests/snapshots/size_max_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11a987f7376f8a3174958a8c21bece8bfb7ec284077940d87038271717d2c397 -size 8655 +oid sha256:64287f55195452d41eb94729ab39ea7667abeec7cfd53c4c7e92235dbcd03bf9 +size 8675 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png index 58b0b13f2..7f85cf977 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_0_empty.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf62a248bcec1054cbd97251e6fc429972ef2318c24b9a56698d7c80115aa57e -size 2262 +oid sha256:570216d6ffa3cc278705582d95b96200bc3ef1608b8f2983a6ed3f8b2ee7276c +size 2280 diff --git a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png index c1920bcf1..66ad3f4a4 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png +++ b/tests/egui_tests/tests/snapshots/text_edit_delay_1_h_invisible.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef70c95f7e171984f992e1b9366b4a0fe11a4871746cb8cfaa8ee263e59de702 -size 2272 +oid sha256:e74e32fb12decf36c8685274226c29c4e748b2bf276bf6b47ba19ac9a3def66a +size 2290 diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png index 29546a036..bda8964c8 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_halign.png +++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:502607c803b884e4e1640d39c97b03b0a40df93c2da328f889168e386f837f36 -size 13261 +oid sha256:f567547c446ffa75f968e0ffc505560f3b3d4171319fbe59be27dde4e553e287 +size 13273 diff --git a/tests/egui_tests/tests/snapshots/visuals/button.png b/tests/egui_tests/tests/snapshots/visuals/button.png index e6978c70f..4978b5a3b 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button.png +++ b/tests/egui_tests/tests/snapshots/visuals/button.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ed487544a84f9f128af550030bc7fe8a960bc70897b38f7c858440a42b6ce44 -size 11197 +oid sha256:2eb21d7b99171d249127377698225f0b2e103733864f6809785dfba169e133bf +size 11213 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image.png b/tests/egui_tests/tests/snapshots/visuals/button_image.png index 6cb7241bf..9d3669597 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d53f67fb3a3717f7bc5ce99b93bc21d1d6580899dfe8e1371ff22bb416af0786 -size 12114 +oid sha256:3322b616a292f685f6051d2bf9be0df28ca7472e8fc3c982cd094ca44dc47ff8 +size 12117 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png index b278f6c25..41d88992d 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e298d89e6fb434e5010d96661fca40bf119118b6b31fdd9fc13201bcd74c8ffd -size 15149 +oid sha256:2021446525a7b1090aaa6b3f12fdd9295c67cde00de61c7f0d36838b6dfc2593 +size 15148 diff --git a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png index 9a1e15c20..3bb097aeb 100644 --- a/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/button_image_shortcut_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0581d601f1e536298cb52bfc8a167aa37aebdf065fc910973a752c9c159223d -size 14733 +oid sha256:e341f6d3fb3d9d09bd573584f68e4ada4e9af0dcb27cf1bc164797f62865c398 +size 14734 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox.png b/tests/egui_tests/tests/snapshots/visuals/checkbox.png index 29f33406f..e45ea9fb3 100644 --- a/tests/egui_tests/tests/snapshots/visuals/checkbox.png +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:929d06a271e98f0c0a2a72fc63648693b22885f80beda0a7baa5858ceb6f952b -size 13507 +oid sha256:9fd43ec4633a59798d2077d9fd3ad8292d5da66678a2d75d33c957a062d7848a +size 13218 diff --git a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png index 145020884..31f84dbeb 100644 --- a/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/checkbox_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:880881761251b70d2c841b8aef3895e95a5eea499d2ff554d3f68b888fb167f9 -size 14571 +oid sha256:3e2252dbe6b26cc6b484d09c57a47f4ecd0d75df88f2279a7dd95ca43b597e9d +size 14282 diff --git a/tests/egui_tests/tests/snapshots/visuals/drag_value.png b/tests/egui_tests/tests/snapshots/visuals/drag_value.png index 7cd2d2ce8..b327692cb 100644 --- a/tests/egui_tests/tests/snapshots/visuals/drag_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/drag_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60ad2d88535977244ac0fa153700489b454a582af2829dc2f41a531943a21d7a -size 9079 +oid sha256:75e5610bd64f6beda7abb005344eff1f5ee23220272077d3b6ee5345baa616aa +size 9095 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio.png b/tests/egui_tests/tests/snapshots/visuals/radio.png index b35eb2d51..d8d6a052a 100644 --- a/tests/egui_tests/tests/snapshots/visuals/radio.png +++ b/tests/egui_tests/tests/snapshots/visuals/radio.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4502cc58a4085d1e0f9945d0bd1d25adeefe71094ce94a210c57f113727f3a5a -size 11806 +oid sha256:bceb25ddc48567a79389d8333eeffa3db4f083d47488a05772602d6145faf2ea +size 11830 diff --git a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png index 7bdae8cf1..c746c028e 100644 --- a/tests/egui_tests/tests/snapshots/visuals/radio_checked.png +++ b/tests/egui_tests/tests/snapshots/visuals/radio_checked.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdedec6788b1a5026603322db9dd9f5fa398813d8aa2c56bc60acad390110501 -size 12499 +oid sha256:30e17cfb5c21e3fd6f850e022f301787bd04d1e6453e3bd30ff695de1f61b8c1 +size 12524 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png index 2f3192a74..bb2b2a32b 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:23d1cddf87ea10d6735403ea0b2a16811d4f92246415633d393c991c3bfab2a1 -size 13716 +oid sha256:b42c8d774d1ecb50f5bde1aa358a37cbb669b59d934786fb1cc4c6f0c3400701 +size 13711 diff --git a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png index 66f3df875..87163dd91 100644 --- a/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png +++ b/tests/egui_tests/tests/snapshots/visuals/selectable_value_selected.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8ac8bbdf9331dbe4244aa2964adf9f49ab8981b899aee9f3200b2799cdf7bc0 -size 13731 +oid sha256:7037f694fef1211f2ea9d8e7e60836d8eb97c9242c7b396efeb8390eaf0852f1 +size 13730 diff --git a/tests/egui_tests/tests/snapshots/visuals/slider.png b/tests/egui_tests/tests/snapshots/visuals/slider.png index 8a1b9cf30..8b758e108 100644 --- a/tests/egui_tests/tests/snapshots/visuals/slider.png +++ b/tests/egui_tests/tests/snapshots/visuals/slider.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff67666113e08d60f1d2310a3a5fca30316953cdabfcc4b259ee816eb498f209 -size 9900 +oid sha256:2d09706e0dbbbf6d07e39470285dc8bcd1b3c6d09bedd91cd20d86c96dba1131 +size 9911 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit.png b/tests/egui_tests/tests/snapshots/visuals/text_edit.png index 4719c8ce9..2cd2c737a 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e1fb3fb0a00a447906aa205c27aa496dcb3d79e98aadf6092811a0514efb5a0 -size 8127 +oid sha256:bf723b58802e9cf5aefb0a06689ed2bfaa45afff9f0fc48d37e742244da097e6 +size 8149 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png index 8a5999742..666364d68 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:077e7de9fdaaa222ee75f6ad620967fb1e29da37f60407d584be7141e9d0badd -size 10143 +oid sha256:e900a4004a1fc808bf74bef7b1ece7c98e16582c40155e222c04f759d229fc70 +size 10125 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png index f02b65693..fff21ac13 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_no_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6cf861a5c1682add50f9bdee4672e5fcaf882329566097faecab5312ac509b7 -size 21419 +oid sha256:2119fd3aa9aefe8198e7b79968f7fafc4aa93294ba4c2577dc55929302067996 +size 21827 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png index 19c231b45..0fb7a4708 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_placeholder_clip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b022e27d7275764df45039abd26f80d69af40fb18bec98cca85565850df859ae -size 8838 +oid sha256:02a333af5e58c53a89d436c4540579d3ea0c1e99c1cad4c019db1da34ff9de10 +size 8840 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png index d27f6f8c4..c61f99ed0 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:337dcbf0b3a344c6cadaf9500376a627739e19e9c47b5da23786c98c612ef4dc -size 10028 +oid sha256:e11dbb1d48a3eadecb5c0e36917785fa1f107e4e283ff2f76831482fe7cd2042 +size 10051 From ab4bca65eafea37b5a41959417bee264e53e6135 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 6 Apr 2026 18:09:54 +0200 Subject: [PATCH 60/89] Make the width of the thin space configurable (#8070) Adds `FontTweak::thin_space_width` and `FontTweak::tab_size` --- crates/egui/src/lib.rs | 4 +-- crates/egui/src/style.rs | 17 +++++++++++ .../egui/src/widgets/text_edit/text_buffer.rs | 8 ++--- .../snapshots/image_kerning/image_dark_x1.png | 4 +-- .../snapshots/image_kerning/image_dark_x2.png | 4 +-- .../image_kerning/image_light_x1.png | 4 +-- .../image_kerning/image_light_x2.png | 4 +-- crates/epaint/src/text/font.rs | 30 ++++++++----------- crates/epaint/src/text/fonts.rs | 15 ++++++++++ crates/epaint/src/text/mod.rs | 3 -- crates/epaint/src/text/text_layout.rs | 17 +++++++++-- 11 files changed, 73 insertions(+), 37 deletions(-) diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index cd098eaea..d706fe46d 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -458,8 +458,8 @@ pub use epaint::{ pub mod text { pub use crate::text_selection::CCursorRange; pub use epaint::text::{ - FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TAB_SIZE, - TextFormat, TextWrapping, cursor::CCursor, + FontData, FontDefinitions, FontFamily, Fonts, Galley, LayoutJob, LayoutSection, TextFormat, + TextWrapping, cursor::CCursor, }; } diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index f7b889506..b84fe727d 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2915,6 +2915,8 @@ impl Widget for &mut FontTweak { y_offset, hinting_override, coords, + thin_space_width, + tab_size, } = self; ui.label("Scale"); @@ -2987,6 +2989,21 @@ impl Widget for &mut FontTweak { } ui.end_row(); + ui.label("thin_space_width"); + ui.horizontal(|ui| { + ui.add( + DragValue::new(thin_space_width) + .range(0.0..=1.0) + .speed(0.01), + ); + ui.label("1\u{2009}234\u{2009}567\u{2009}890"); + }); + ui.end_row(); + + ui.label("tab_size"); + ui.add(DragValue::new(tab_size).range(0.0..=16.0).speed(0.1)); + ui.end_row(); + if ui.button("Reset").clicked() { *self = Default::default(); } diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index a67dc1b38..dbc2db26e 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,9 +1,9 @@ use std::{borrow::Cow, ops::Range}; -use epaint::{ - Galley, - text::{TAB_SIZE, cursor::CCursor}, -}; +use epaint::{Galley, text::cursor::CCursor}; + +/// One `\t` character is this many spaces wide (for indentation purposes). +const TAB_SIZE: usize = 4; use crate::{ text::CCursorRange, diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png index 218a4e258..d65dcb44f 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2ef07080ca2aa10e128c646479b8b322a092d63f15b35c52ed59015e7c2a0f60 -size 15434 +oid sha256:6d64cc7d014cf063689a4dd8a6cdd87eb944f9637890baf32f59a258342400bf +size 15400 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png index 8da2d19a9..147a692f2 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_dark_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4edb61c6619d5e44892fa29da0aa0a624306ae637dbbaa057e3fa47c14dc06bd -size 35988 +oid sha256:9e9f800546cc98bbd92f31072aceef10b5b8b9bbef0db4c8f4dbae652aefec61 +size 35918 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png index 1085f9969..ad6dd5637 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f8da0c2e37497968864a91fa6bef7c545c37791f4f9b788ea9a2f43dd4ac16b1 -size 16116 +oid sha256:e3f6e1cc6a069ac96ff9e039ce85462453ee943b4cb46080550bc1c0749ad658 +size 16085 diff --git a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png index d89daa1c0..71b8a1cee 100644 --- a/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png +++ b/crates/egui_demo_lib/tests/snapshots/image_kerning/image_light_x2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2a603fcb2eb97943d6be3239b91cacee093adcb55a6dd14af93a72fc8b3a61fa -size 39270 +oid sha256:5901b5a8201b85b51118193eef300749a4569eedc62043cd4e03f76e73a12f51 +size 39222 diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 48659fbe9..311b17a05 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -432,8 +432,7 @@ impl FontFace { && let Some(space) = self.glyph_info(' ') { let glyph_info = GlyphInfo { - advance_width_unscaled: (crate::text::TAB_SIZE as f32 - * space.advance_width_unscaled.0) + advance_width_unscaled: (self.tweak.tab_size * space.advance_width_unscaled.0) .into(), ..space }; @@ -441,21 +440,18 @@ impl FontFace { return Some(glyph_info); } - if c == '\u{2009}' { - // Thin space, often used as thousands deliminator: 1 234 567 890 - // https://www.compart.com/en/unicode/U+2009 - // https://en.wikipedia.org/wiki/Thin_space - - if let Some(space) = self.glyph_info(' ') { - let em = self.font.borrow_dependent().metrics.units_per_em as f32; - let advance_width = f32::min(em / 6.0, space.advance_width_unscaled.0 * 0.5); // TODO(emilk): make configurable - let glyph_info = GlyphInfo { - advance_width_unscaled: advance_width.into(), - ..space - }; - self.glyph_info_cache.insert(c, glyph_info); - return Some(glyph_info); - } + if (c == '\u{2009}' || c == '\u{202F}') + && let Some(space) = self.glyph_info(' ') + { + // Thin space (U+2009) and narrow no-break space (U+202F), + // often used as thousands separator: 1 234 567 890 + let advance_width = self.tweak.thin_space_width * space.advance_width_unscaled.0; + let glyph_info = GlyphInfo { + advance_width_unscaled: advance_width.into(), + ..space + }; + self.glyph_info_cache.insert(c, glyph_info); + return Some(glyph_info); } if invisible_char(c) { diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 135ddb6d5..b6c8f3504 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -193,6 +193,19 @@ pub struct FontTweak { /// Override the font's default variation coordinates. pub coords: VariationCoords, + + /// Width of a thin space (`\u{2009}`) and narrow no-break space (`\u{202F}`), + /// as a fraction of the normal space width. + /// + /// Thin space is often used as a thousands separator: `1 234 567`. + /// + /// Default: `0.5` (half a normal space). + pub thin_space_width: f32, + + /// Width of a tab character (`\t`), measured in number of space widths. + /// + /// Default: `4.0`. + pub tab_size: f32, } impl Default for FontTweak { @@ -203,6 +216,8 @@ impl Default for FontTweak { y_offset: 0.0, hinting_override: None, coords: VariationCoords::default(), + thin_space_width: 0.5, + tab_size: 4.0, } } } diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index b40ba45b8..7d37c0db6 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -6,9 +6,6 @@ mod fonts; mod text_layout; mod text_layout_types; -/// One `\t` character is this many spaces wide. -pub const TAB_SIZE: usize = 4; - pub use { fonts::{ FontData, FontDefinitions, FontFamily, FontId, FontInsert, FontPriority, FontTweak, Fonts, diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 0f3089292..75cbb5be0 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -8,7 +8,6 @@ use crate::{ Color32, Mesh, Stroke, Vertex, stroke::PathStroke, text::{ - TAB_SIZE, font::{StyledMetrics, UvRect, is_cjk, is_cjk_break_allowed}, fonts::FontFaceKey, }, @@ -250,11 +249,23 @@ fn layout_shaped_run( .unwrap_or('\u{FFFD}'); // Unicode Replacement Character // Tab is a layout concept, not a glyph — the shaper doesn't know about tab stops. - // Override the advance width to TAB_SIZE Ɨ space width. + // Override the advance width using the font's configured tab size. if chr == '\t' { + let tweak = font.fonts_by_id.get(&run.font_key).map(|ff| ff.tweak()); + let tab_size = tweak.map_or(4.0, |t| t.tab_size); let (_, space_info) = font.glyph_info(' '); let space_width_px = space_info.advance_width_unscaled.0 * px_scale; - advance_width_px = TAB_SIZE as f32 * space_width_px; + advance_width_px = tab_size * space_width_px; + } + + // Thin space (U+2009) and narrow no-break space (U+202F): + // override the shaper's advance width with the configured fraction of a space. + if chr == '\u{2009}' || chr == '\u{202F}' { + let tweak = font.fonts_by_id.get(&run.font_key).map(|ff| ff.tweak()); + let thin_space_width = tweak.map_or(0.5, |t| t.thin_space_width); + let (_, space_info) = font.glyph_info(' '); + let space_width_px = space_info.advance_width_unscaled.0 * px_scale; + advance_width_px = thin_space_width * space_width_px; } // Apply extra_letter_spacing only at cluster boundaries, From 3abba21f2db984294f912768f496e3b175f2a984 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 7 Apr 2026 10:38:37 +0200 Subject: [PATCH 61/89] Add `subpixel_binning` to `TextOptions` and `FontTweak` (#8072) This lets you turn off subpixel horizontal binning of glyphs. The option is a trade-off between even kerning and sharp text. * Closes https://github.com/emilk/egui/issues/8034 --- crates/egui/src/style.rs | 33 +++++++++++++++++++-------------- crates/epaint/src/text/font.rs | 12 ++++++++---- crates/epaint/src/text/fonts.rs | 12 +++++++++--- crates/epaint/src/text/mod.rs | 15 +++++++++++++++ 4 files changed, 51 insertions(+), 21 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index b84fe727d..c88ee45fe 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -2344,11 +2344,13 @@ impl Visuals { max_texture_side: _, alpha_from_coverage, font_hinting, + subpixel_binning, } = text_options; text_alpha_from_coverage_ui(ui, alpha_from_coverage); - ui.checkbox(font_hinting, "Enable font hinting"); + ui.checkbox(font_hinting, "Font hinting (sharper text)"); + ui.checkbox(subpixel_binning, "Sub-pixel binning (more even kerning)"); }); ui.collapsing("Text cursor", |ui| { @@ -2913,10 +2915,11 @@ impl Widget for &mut FontTweak { scale, y_offset_factor, y_offset, - hinting_override, + hinting, coords, thin_space_width, tab_size, + subpixel_binning, } = self; ui.label("Scale"); @@ -2932,18 +2935,20 @@ impl Widget for &mut FontTweak { ui.add(DragValue::new(y_offset).speed(-0.02)); ui.end_row(); - ui.label("hinting_override"); - ComboBox::from_id_salt("hinting_override") - .selected_text(match hinting_override { - None => "None", - Some(true) => "Enable", - Some(false) => "Disable", - }) - .show_ui(ui, |ui| { - ui.selectable_value(hinting_override, None, "None"); - ui.selectable_value(hinting_override, Some(true), "Enable"); - ui.selectable_value(hinting_override, Some(false), "Disable"); - }); + ui.label("hinting"); + ui.horizontal(|ui| { + ui.radio_value(hinting, Some(true), "on"); + ui.radio_value(hinting, Some(false), "off"); + ui.radio_value(hinting, None, "default"); + }); + ui.end_row(); + + ui.label("subpixel_binning"); + ui.horizontal(|ui| { + ui.radio_value(subpixel_binning, Some(true), "on"); + ui.radio_value(subpixel_binning, Some(false), "off"); + ui.radio_value(subpixel_binning, None, "default"); + }); ui.end_row(); ui.label("coords"); diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 311b17a05..2d5edf1cf 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -316,6 +316,7 @@ pub struct FontFace { name: String, font: FontCell, tweak: FontTweak, + subpixel_binning: bool, /// Cached `harfrust` shaper data (parsed GSUB/GPOS tables). /// `ShaperData` is `Copy` — lives outside the `self_cell`. @@ -352,7 +353,7 @@ impl FontFace { skrifa::instance::LocationRef::default(), ); - let hinting_enabled = tweak.hinting_override.unwrap_or(options.font_hinting); + let hinting_enabled = tweak.hinting.unwrap_or(options.font_hinting); let hinting_instance = hinting_enabled .then(|| { // It doesn't really matter what we put here for options. Since the size is `unscaled()`, we will @@ -379,10 +380,13 @@ impl FontFace { let shaper_data = harfrust::ShaperData::new(&font.borrow_dependent().skrifa); + let subpixel_binning = tweak.subpixel_binning.unwrap_or(options.subpixel_binning); + Ok(Self { name, font, tweak, + subpixel_binning, shaper_data, glyph_info_cache: Default::default(), glyph_alloc_cache: Default::default(), @@ -551,12 +555,12 @@ impl FontFace { return (GlyphAllocation::default(), h_pos.round() as i32); } - let (h_pos_round, bin) = if is_cjk { + let (h_pos_round, bin) = if self.subpixel_binning && !is_cjk { + SubpixelBin::new(h_pos) + } else { // CJK scripts contain a lot of characters and could hog the glyph atlas // if we stored 4 subpixel offsets per glyph. (h_pos.round() as i32, SubpixelBin::Zero) - } else { - SubpixelBin::new(h_pos) }; let cache_key = GlyphCacheKey::new(glyph_id, metrics, bin); diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index b6c8f3504..f1a6e63b4 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -188,8 +188,13 @@ pub struct FontTweak { /// Override the global font hinting setting for this specific font. /// - /// `None` means use the global setting. - pub hinting_override: Option, + /// `None` means use the global setting in [`TextOptions::font_hinting`]. + pub hinting: Option, + + /// Override the global sub-pixel binning setting for this specific font. + /// + /// `None` means use the global setting in [`TextOptions::subpixel_binning`]. + pub subpixel_binning: Option, /// Override the font's default variation coordinates. pub coords: VariationCoords, @@ -214,7 +219,8 @@ impl Default for FontTweak { scale: 1.0, y_offset_factor: 0.0, y_offset: 0.0, - hinting_override: None, + hinting: None, + subpixel_binning: None, coords: VariationCoords::default(), thin_space_width: 0.5, tab_size: 4.0, diff --git a/crates/epaint/src/text/mod.rs b/crates/epaint/src/text/mod.rs index 7d37c0db6..6d2d783c2 100644 --- a/crates/epaint/src/text/mod.rs +++ b/crates/epaint/src/text/mod.rs @@ -34,6 +34,20 @@ pub struct TextOptions { /// /// Default is `true`. pub font_hinting: bool, + + /// Enable sub-pixel binning for glyphs. + /// + /// Sub-pixel binning renders each glyph at up to four fractional horizontal offsets, + /// giving more even kerning at the cost of more atlas space. + /// + /// It also lead to text looking more blurry. + /// + /// This is always disabled for CJK characters (which have too many unique glyphs). + /// + /// Can be overridden per font with [`FontTweak::subpixel_binning`]. + /// + /// Default: `true`. + pub subpixel_binning: bool, } impl Default for TextOptions { @@ -42,6 +56,7 @@ impl Default for TextOptions { max_texture_side: 2048, // Small but portable alpha_from_coverage: crate::AlphaFromCoverage::default(), font_hinting: true, + subpixel_binning: true, } } } From 188ffacf415f95152b904c40ac48fa9980b05bf4 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:38:57 +0900 Subject: [PATCH 62/89] Log `localStorage` write failures in `local_storage_set` (#8062) Log `localStorage` write failures in `local_storage_set` **Description** This PR improves `local_storage_set()` logging in `eframe/src/web/storage.rs`. It logs: - write failures with key and browser error - unavailable local storage This helps diagnose web persistence issues such as `QuotaExceededError` when `localStorage.setItem()` fails. --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/web/storage.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/eframe/src/web/storage.rs b/crates/eframe/src/web/storage.rs index 170798dc6..baa3ad83b 100644 --- a/crates/eframe/src/web/storage.rs +++ b/crates/eframe/src/web/storage.rs @@ -9,7 +9,19 @@ pub fn local_storage_get(key: &str) -> Option { /// Write data to local storage. pub fn local_storage_set(key: &str, value: &str) { - local_storage().map(|storage| storage.set_item(key, value)); + match local_storage() { + Some(storage) => { + if let Err(err) = storage.set_item(key, value) { + log::warn!( + "local_storage_set failed: key={key}, err={}", + crate::web::string_from_js_value(&err) + ); + } + } + None => { + log::warn!("local_storage unavailable"); + } + } } #[cfg(feature = "persistence")] From a511282e94c364609a2ceb89c4399af67112d53e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 7 Apr 2026 12:42:13 +0200 Subject: [PATCH 63/89] Update to wgpu 29.0.1 (#8073) This fixes an important bug on OpenGL: * https://github.com/emilk/egui/issues/8012 --- Cargo.lock | 24 ++++++++++++------------ Cargo.toml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58817670a..cd91fea8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2722,9 +2722,9 @@ dependencies = [ [[package]] name = "naga" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b4372fed0bd362d646d01b6926df0e837859ccc522fed720c395e0460f29c8" +checksum = "aa2630921705b9b01dcdd0b6864b9562ca3c1951eecd0f0c4f5f04f61e412647" dependencies = [ "arrayvec", "bit-set 0.9.1", @@ -5218,9 +5218,9 @@ checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "wgpu" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9f386699b1fb8b8a05bfe82169b24d151f05702d2905a0bf93bc454fcc825" +checksum = "72c239a9a747bbd379590985bac952c2e53cb19873f7072b3370c6a6a8e06837" dependencies = [ "arrayvec", "bitflags 2.9.4", @@ -5248,9 +5248,9 @@ dependencies = [ [[package]] name = "wgpu-core" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c34181b0acb8f98168f78f8e57ec66f57df5522b39143dbe5f2f45d7ca927c" +checksum = "1e80ac6cf1895df6342f87d975162108f9d98772a0d74bc404ab7304ac29469e" dependencies = [ "arrayvec", "bit-set 0.9.1", @@ -5318,9 +5318,9 @@ dependencies = [ [[package]] name = "wgpu-hal" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058b6047337cf323a4f092486443a9337f3d81325347e5d77deed7e563aeaedc" +checksum = "89a47aef47636562f3937285af4c44b4b5b404b46577471411cc5313a921da7e" dependencies = [ "android_system_properties", "arrayvec", @@ -5371,9 +5371,9 @@ dependencies = [ [[package]] name = "wgpu-naga-bridge" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b8e1e505095f24cb4a578f04b1421d456257dca7fac114d9d9dd3d978c34b8" +checksum = "7b4684f4410da0cf95a4cb63bb5edaac022461dedb6adf0b64d0d9b5f6890d51" dependencies = [ "naga", "wgpu-types", @@ -5381,9 +5381,9 @@ dependencies = [ [[package]] name = "wgpu-types" -version = "29.0.0" +version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15ece45db77dd5451f11c0ce898334317ce8502d304a20454b531fdc0652fae" +checksum = "ec2675540fb1a5cfa5ef122d3d5f390e2c75711a0b946410f2d6ac3a0f77d1f6" dependencies = [ "bitflags 2.9.4", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 2f5d5cf50..0e592637c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,7 +146,7 @@ wayland-cursor = { version = "0.31.11", default-features = false } web-sys = "0.3.77" web-time = "1.1.0" # Timekeeping for native and web webbrowser = "1.0.5" -wgpu = { version = "29.0.0", default-features = false, features = ["std"] } +wgpu = { version = "29.0.1", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.13", default-features = false } From 74b9970a9f65b1b2d0c04176de79c2922ee66bea Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 7 Apr 2026 12:59:46 +0200 Subject: [PATCH 64/89] Fix wrong color of last glyph of selected text (#8075) * Closes https://github.com/emilk/egui/issues/8059 --- crates/egui/src/text_selection/visuals.rs | 2 +- crates/egui_demo_lib/tests/snapshots/text_selection_0.png | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index e41d7a436..e114ddb55 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -61,7 +61,7 @@ pub fn paint_text_selection( let last_glyph_index = if ri == max.row { max.column } else { - row.glyphs.len() - 1 + row.glyphs.len() }; let first_vertex_index = row diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png index a6d590fdf..a7aac34bf 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16e993bb27d32162fcf573cc4d977af775e6e0b94373fc5e8e1a9890453e508c -size 5278 +oid sha256:ce1e16fa09588ec5351d031408657137f42ff40eb7cf6dd00b8c65a4f51ec680 +size 5332 From 1fdc5c07751253c3738b3a2a495ed97ae3516c2c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 7 Apr 2026 13:29:57 +0200 Subject: [PATCH 65/89] Fix text selection of centered and right-aligned text (#8076) * Fixes https://github.com/emilk/egui/issues/8049 * Bug introduced in https://github.com/emilk/egui/pull/7831 --- crates/egui_demo_lib/src/demo/text_layout.rs | 32 ++++++++++++++++--- crates/egui_demo_lib/tests/misc.rs | 20 +++++++----- .../tests/snapshots/demos/Text Layout.png | 4 +-- .../tests/snapshots/text_selection_0.png | 4 +-- .../tests/snapshots/text_selection_1.png | 4 +-- crates/epaint/src/text/text_layout_types.rs | 4 +-- 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/crates/egui_demo_lib/src/demo/text_layout.rs b/crates/egui_demo_lib/src/demo/text_layout.rs index ef84fb326..656d5a6d2 100644 --- a/crates/egui_demo_lib/src/demo/text_layout.rs +++ b/crates/egui_demo_lib/src/demo/text_layout.rs @@ -7,17 +7,21 @@ pub struct TextLayoutDemo { overflow_character: Option, extra_letter_spacing: f32, line_height_pixels: u32, + halign: egui::Align, + justify: bool, lorem_ipsum: bool, } impl Default for TextLayoutDemo { fn default() -> Self { Self { - max_rows: 6, - break_anywhere: true, + max_rows: 1000, + break_anywhere: false, overflow_character: Some('…'), extra_letter_spacing: 0.0, line_height_pixels: 0, + halign: egui::Align::LEFT, + justify: false, lorem_ipsum: true, } } @@ -48,6 +52,8 @@ impl crate::View for TextLayoutDemo { overflow_character, extra_letter_spacing, line_height_pixels, + halign, + justify, lorem_ipsum, } = self; @@ -109,6 +115,18 @@ impl crate::View for TextLayoutDemo { }); ui.end_row(); + ui.label("Horizontal align:"); + ui.horizontal(|ui| { + ui.selectable_value(halign, egui::Align::LEFT, "Left"); + ui.selectable_value(halign, egui::Align::Center, "Center"); + ui.selectable_value(halign, egui::Align::RIGHT, "Right"); + }); + ui.end_row(); + + ui.label("Justify:"); + ui.checkbox(justify, "Fill row width"); + ui.end_row(); + ui.label("Text:"); ui.horizontal(|ui| { ui.selectable_value(lorem_ipsum, true, "Lorem Ipsum"); @@ -145,8 +163,14 @@ impl crate::View for TextLayoutDemo { ..Default::default() }; - // NOTE: `Label` overrides some of the wrapping settings, e.g. wrap width - ui.label(job); + // NOTE: `Label` overrides some of the wrapping settings, + // e.g. wrap width, halign, and justify. + ui.with_layout( + egui::Layout::top_down(*halign).with_cross_justify(*justify), + |ui| { + ui.label(job); + }, + ); }); } } diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index 6d66abfb1..427710629 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -62,21 +62,25 @@ fn test_italics() { fn test_text_selection() { let mut results = egui_kittest::SnapshotResults::new(); - for (test_idx, drag_start_x) in [0.2_f32, 0.9].into_iter().enumerate() { - let mut harness = Harness::builder().build_ui(|ui| { - let visuals = ui.visuals_mut(); - visuals.selection.bg_fill = Color32::LIGHT_GREEN; - visuals.selection.stroke.color = Color32::RED; + for (test_idx, drag_start_x) in [0.2_f32, 0.95].into_iter().enumerate() { + let mut harness = Harness::builder() + .with_pixels_per_point(1.0) // TODO(emilk): why does this test fail with 2.0? + .build_ui(|ui| { + let visuals = ui.visuals_mut(); + visuals.selection.bg_fill = Color32::LIGHT_GREEN; + visuals.selection.stroke.color = Color32::RED; - ui.label("Some varied ☺ text :)\nAnd it has a second line!"); - }); + ui.vertical_centered(|ui| { + ui.label("Some varied ☺ text :)\nAnd it has a second line!\nAnd a third!"); + }); + }); harness.run(); harness.fit_contents(); // Drag to select text: let label = harness.get_by_role(Role::Label); harness.drag_at(label.rect().lerp_inside([drag_start_x, 0.25])); - harness.drop_at(label.rect().lerp_inside([0.6, 0.75])); + harness.drop_at(label.rect().lerp_inside([0.5, 0.75])); harness.run(); harness.snapshot(format!("text_selection_{test_idx}")); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png index 61065db1d..0bd5b9ceb 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/Text Layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c47faa75a002577220a92efceab5c0ccee1ddaa17c2d2f3d6d1429dbf8fe718 -size 60887 +oid sha256:661570ed4bdf24d52ab049e7d3cf22ef4d50542ee5486d133e0a618a6146da42 +size 98582 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png index a7aac34bf..dae75cfc3 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_0.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce1e16fa09588ec5351d031408657137f42ff40eb7cf6dd00b8c65a4f51ec680 -size 5332 +oid sha256:d7d8578b55ed8fdaf2062cb53d7320555e490289ff9efb30f39ebb697d2e252e +size 6529 diff --git a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png index 3506845cb..4be490adc 100644 --- a/crates/egui_demo_lib/tests/snapshots/text_selection_1.png +++ b/crates/egui_demo_lib/tests/snapshots/text_selection_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79676693e9f1804c7e0a32ffa9bec3bde281446ffc184b55a1a3fcf34074ac34 -size 5304 +oid sha256:5e09ec77bd473ad6695557b78d4967ade09b12100f04bc66d0e29779dfb0d551 +size 6495 diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 22fb03c57..481e3ebbf 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1047,7 +1047,7 @@ impl Galley { return self.end_pos(); }; - let x = row.x_offset(layout_cursor.column) + row.pos.x - self.rect.left(); + let x = row.x_offset(layout_cursor.column) + row.pos.x; Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y())) } @@ -1092,7 +1092,7 @@ impl Galley { if is_pos_within_row || y_dist < best_y_dist { best_y_dist = y_dist; // char_at is `Row` not `PlacedRow` relative which means we have to subtract the pos. - let column = row.char_at(pos.x - row.pos.x + self.rect.left()); + let column = row.char_at(pos.x - row.pos.x); let prefer_next_row = column < row.char_count_excluding_newline(); cursor = CCursor { index: ccursor_index + column, From b117a1ac19954199bb60230ee758b89bd91ab1d8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 8 Apr 2026 09:52:28 +0200 Subject: [PATCH 66/89] Fix `Context::is_pointer_over_egui` and `Context::egui_wants_pointer_input` (#8081) * Closes https://github.com/emilk/egui/issues/8041 These functions were broken when using the new `run_ui`. --- crates/egui/src/context.rs | 52 ++++++++++++++++-------- crates/egui/src/pass_state.rs | 27 ++++++++++++ tests/test_inline_glow_paint/Cargo.toml | 3 +- tests/test_inline_glow_paint/src/main.rs | 19 +++++++++ 4 files changed, 83 insertions(+), 18 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 824a5318c..809f5dac5 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -793,7 +793,7 @@ impl Context { let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); #[expect(deprecated)] self.run(new_input, |ctx| { - let mut top_ui = Ui::new( + let mut root_ui = Ui::new( ctx.clone(), Id::new((ctx.viewport_id(), "__top_ui")), UiBuilder::new() @@ -802,14 +802,15 @@ impl Context { ); { - plugins.on_begin_pass(&mut top_ui); - run_ui(&mut top_ui); - plugins.on_end_pass(&mut top_ui); + plugins.on_begin_pass(&mut root_ui); + run_ui(&mut root_ui); + plugins.on_end_pass(&mut root_ui); } - // Inform ctx about what we actually used, so we can shrink the native window to fit. - // TODO(emilk): make better use of this somehow - ctx.pass_state_mut(|state| state.allocate_central_panel(top_ui.min_rect())); + ctx.pass_state_mut(|state| { + state.root_ui_available_rect = Some(root_ui.available_rect_before_wrap()); + state.root_ui_min_rect = Some(root_ui.min_rect()); + }); }) } @@ -2853,13 +2854,21 @@ impl Context { /// How much space is still available after panels have been added. #[deprecated = "Use content_rect (or viewport_rect) instead"] pub fn available_rect(&self) -> Rect { + #[expect(deprecated)] // legacy self.pass_state(|s| s.available_rect()).round_ui() } /// How much space is used by windows and the top-level [`Ui`]. pub fn globally_used_rect(&self) -> Rect { self.write(|ctx| { - let mut used = ctx.viewport().this_pass.used_by_panels; + let viewport = ctx.viewport(); + let root_ui_min_rect = + (viewport.this_pass.root_ui_min_rect).or(viewport.prev_pass.root_ui_min_rect); + + let mut used = root_ui_min_rect.unwrap_or_else(|| { + #[expect(deprecated)] // legacy + ctx.viewport().this_pass.used_by_panels + }); for (_id, window) in ctx.memory.areas().visible_windows() { used |= window.rect(); } @@ -2886,18 +2895,27 @@ impl Context { /// Is the pointer (mouse/touch) over any egui area? pub fn is_pointer_over_egui(&self) -> bool { let pointer_pos = self.input(|i| i.pointer.interact_pos()); - if let Some(pointer_pos) = pointer_pos { - if let Some(layer) = self.layer_id_at(pointer_pos) { - if layer.order == Order::Background { - !self.pass_state(|state| state.unused_rect.contains(pointer_pos)) - } else { - true - } + let Some(pointer_pos) = pointer_pos else { + return false; + }; + let Some(layer) = self.layer_id_at(pointer_pos) else { + return false; + }; + if layer.order == Order::Background { + let root_ui_available_rect = self + .pass_state(|state| state.root_ui_available_rect) + .or_else(|| self.prev_pass_state(|state| state.root_ui_available_rect)); + + if let Some(root_ui_available_rect) = root_ui_available_rect { + // Modern `run_ui` code + !root_ui_available_rect.contains(pointer_pos) } else { - false + // Legacy code + #[expect(deprecated)] + !self.pass_state(|state| state.unused_rect.contains(pointer_pos)) } } else { - false + true } } diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index 56564541d..b6e655705 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] // TODO(emilk): Remove legacy panels + use ahash::HashMap; use crate::{Align, Id, IdMap, LayerId, Rangef, Rect, Vec2, WidgetRects, id::IdSet, style}; @@ -199,15 +201,28 @@ pub struct PassState { pub tooltips: TooltipPassState, + /// What the root UI had available at the end of the previous pass. + /// + /// Only set if [`crate::Context::run_ui`] has been called. + pub root_ui_available_rect: Option, + + /// What the root UI had used at the end of the previous pass. + /// + /// Only set if [`crate::Context::run_ui`] has been called. + pub root_ui_min_rect: Option, + /// Starts off as the `screen_rect`, shrinks as panels are added. /// The [`crate::CentralPanel`] does not change this. + #[deprecated = "Only used by legacy Context-Panels"] pub available_rect: Rect, /// Starts off as the `screen_rect`, shrinks as panels are added. /// The [`crate::CentralPanel`] retracts from this. + #[deprecated = "Only used by legacy Context-Panels"] pub unused_rect: Rect, /// How much space is used by panels. + #[deprecated = "Only used by legacy Context-Panels"] pub used_by_panels: Rect, /// The current scroll area should scroll to this range (horizontal, vertical). @@ -240,6 +255,8 @@ impl Default for PassState { widgets: Default::default(), layers: Default::default(), tooltips: Default::default(), + root_ui_available_rect: None, + root_ui_min_rect: None, available_rect: Rect::NAN, unused_rect: Rect::NAN, used_by_panels: Rect::NAN, @@ -262,6 +279,8 @@ impl PassState { widgets, tooltips, layers, + root_ui_available_rect, + root_ui_min_rect, available_rect, unused_rect, used_by_panels, @@ -278,6 +297,8 @@ impl PassState { widgets.clear(); tooltips.clear(); layers.clear(); + *root_ui_available_rect = None; + *root_ui_min_rect = None; *available_rect = content_rect; *unused_rect = content_rect; *used_by_panels = Rect::NOTHING; @@ -295,6 +316,7 @@ impl PassState { } /// How much space is still available after panels has been added. + #[deprecated = "Only used by legacy Context-Panels"] pub(crate) fn available_rect(&self) -> Rect { debug_assert!( self.available_rect.is_finite(), @@ -304,6 +326,7 @@ impl PassState { } /// Shrink `available_rect`. + #[deprecated = "Only used by legacy Context-Panels"] pub(crate) fn allocate_left_panel(&mut self, panel_rect: Rect) { debug_assert!( panel_rect.min.distance(self.available_rect.min) < 0.1, @@ -315,6 +338,7 @@ impl PassState { } /// Shrink `available_rect`. + #[deprecated = "Only used by legacy Context-Panels"] pub(crate) fn allocate_right_panel(&mut self, panel_rect: Rect) { debug_assert!( panel_rect.max.distance(self.available_rect.max) < 0.1, @@ -326,6 +350,7 @@ impl PassState { } /// Shrink `available_rect`. + #[deprecated = "Only used by legacy Context-Panels"] pub(crate) fn allocate_top_panel(&mut self, panel_rect: Rect) { debug_assert!( panel_rect.min.distance(self.available_rect.min) < 0.1, @@ -337,6 +362,7 @@ impl PassState { } /// Shrink `available_rect`. + #[deprecated = "Only used by legacy Context-Panels"] pub(crate) fn allocate_bottom_panel(&mut self, panel_rect: Rect) { debug_assert!( panel_rect.max.distance(self.available_rect.max) < 0.1, @@ -347,6 +373,7 @@ impl PassState { self.used_by_panels |= panel_rect; } + #[deprecated = "Only used by legacy Context-Panels"] pub(crate) fn allocate_central_panel(&mut self, panel_rect: Rect) { // Note: we do not shrink `available_rect`, because // we allow windows to cover the CentralPanel. diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 090ccae4c..6aa9428e4 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -13,7 +13,8 @@ workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { workspace = true, features = [ +eframe = { workspace = true, default_features = false, features = [ + "glow", "default", "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } diff --git a/tests/test_inline_glow_paint/src/main.rs b/tests/test_inline_glow_paint/src/main.rs index 576934bf3..fd4abc35f 100644 --- a/tests/test_inline_glow_paint/src/main.rs +++ b/tests/test_inline_glow_paint/src/main.rs @@ -30,6 +30,10 @@ struct MyTestApp {} impl eframe::App for MyTestApp { fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) { + egui::Panel::top("top").show_inside(ui, |ui| { + ui.label("This is a test of painting directly with glow."); + }); + use glow::HasContext as _; let gl = frame.gl().unwrap(); @@ -43,6 +47,21 @@ impl eframe::App for MyTestApp { egui::Window::new("Floating Window").show(ui.ctx(), |ui| { ui.label("The background should be purple."); + ui.label(format!( + "is_pointer_over_egui: {}", + ui.is_pointer_over_egui() + )); + ui.label(format!( + "egui_wants_pointer_input: {}", + ui.egui_wants_pointer_input() + )); + ui.label(format!( + "egui_is_using_pointer: {}", + ui.egui_is_using_pointer() + )); + if let Some(pos) = ui.pointer_latest_pos() { + ui.label(format!("layer_id_at: {:?}", ui.layer_id_at(pos))); + } }); } } From 86a7f47738b70e0b5c39950ca0962ee79e128033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Wed, 8 Apr 2026 16:04:15 +0800 Subject: [PATCH 67/89] Delegate handling of IME interruptions to integrations to fix virtual keyboard flickering on web (#8078) * Closes N/A * Partially replaces #7983 * Related: #8045 * [x] I have followed the instructions in the PR template ## Details In #7983, I modified `Memory::request_focus` to interrupt any ongoing IME composition. This fixed a bug where clicking inside an already focused `TextEdit` failed to cancel the active composition, resulting in duplicated text: https://github.com/emilk/egui/pull/8045#issuecomment-4193310616 To avoid introducing API changes in that PR, I ensured the IME state was reset by forcing `PlatformOutput::ime` to `None` for at least one frame. While this works well on desktop platforms, it causes virtual keyboard flickering on the web: https://github.com/emilk/egui/pull/8045#issuecomment-4193035008 In this PR, I delegate the responsibility for handling IME composition interruptions to integrations, allowing each integration to decide how to interrupt compositions in a flexible manner. ### The new field `should_interrupt_composition` on `IMEOutput`. Instead of introducing a new `OutputCommand` variant, this PR adds a new field `should_interrupt_composition` to `IMEOutput`. Interrupting an active composition is only meaningful when IME remains allowed. If IME should be disabled altogether, `PlatformOutput::ime` can simply be set to `None`. Given this, IMO, it is more appropriate to attach the interrupt signal to `IMEOutput` (i.e., the type of `PlatformOutput::ime`). --- crates/eframe/src/web/text_agent.rs | 15 ++++- crates/egui-winit/src/lib.rs | 11 +++- crates/egui/src/context.rs | 6 ++ crates/egui/src/data/output.rs | 3 + crates/egui/src/memory/mod.rs | 59 ++++++-------------- crates/egui/src/widgets/text_edit/builder.rs | 1 + 6 files changed, 49 insertions(+), 46 deletions(-) diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index e3d4f8860..20b904244 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -56,8 +56,13 @@ impl TextAgent { let input = input.clone(); move |event: web_sys::InputEvent, runner: &mut AppRunner| { let text = input.value(); - // Fix android virtual keyboard Gboard - // This removes the virtual keyboard's suggestion. + // Workaround for an Android Gboard issue: after typing a word, + // the user has to delete invisible characters (whose count + // matches the length of the current suggestion) before actual + // characters are deleted, unless the focus has been reset. + // + // this issue appears to have been fixed in Gboard sometime + // between versions 14.7.09 and 17.0.12. if !event.is_composing() { input.blur().ok(); input.focus().ok(); @@ -132,6 +137,12 @@ impl TextAgent { let Some(ime) = ime else { return Ok(()) }; + if ime.should_interrupt_composition { + // no-op for now: currently, the text agent is sizeless, so any + // click shifts focus to the canvas, which naturally interrupts the + // composition. + } + let mut canvas_rect = super::canvas_content_rect(canvas); // Fix for safari with virtual keyboard flapping position if is_mobile_safari() { diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 99a9894f3..3526f92be 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1049,7 +1049,8 @@ impl State { self.set_cursor_icon(window, cursor_icon); let allow_ime = ime.is_some(); - if self.allow_ime != allow_ime { + let is_toggling_ime = self.allow_ime != allow_ime; + if is_toggling_ime { self.allow_ime = allow_ime; #[cfg(target_os = "windows")] if !self.allow_ime { @@ -1066,6 +1067,14 @@ impl State { } if let Some(ime) = ime { + if !is_toggling_ime && ime.should_interrupt_composition { + // TODO(umajho): use a more proper way to interrupt composition + // if `winit` provides one in the future. + + window.set_ime_allowed(false); + window.set_ime_allowed(true); + } + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); let ime_rect_px = pixels_per_point * ime.rect; if self.ime_rect_px != Some(ime_rect_px) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 809f5dac5..433446648 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -2612,6 +2612,12 @@ impl ContextImpl { let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); + if self.memory.should_interrupt_ime() + && let Some(ime) = &mut platform_output.ime + { + ime.should_interrupt_composition = true; + } + { profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index ea3ff4eec..77a0eee63 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -79,6 +79,9 @@ pub struct IMEOutput { /// /// This is a very thin rectangle. pub cursor_rect: crate::Rect, + + /// Whether any ongoing IME composition should be interrupted. + pub should_interrupt_composition: bool, } /// Commands that the egui integration should execute at the end of a frame. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 34e0fe319..8d109c254 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -117,21 +117,10 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] popups: ViewportIdMap, - /// When the last IME interruption was made. + /// Whether to inform the backend to interrupt any ongoing IME composition + /// this pass. #[cfg_attr(feature = "persistence", serde(skip))] - ime_interruption_time: ImeInterruptionTime, -} - -#[derive(Clone, Copy, Debug, Default)] -enum ImeInterruptionTime { - #[default] - None, - - /// The IME was interrupted in the current frame. - ThisFrame, - - /// The IME was interrupted in the previous frame. - LastFrame, + requested_interrupt_ime: bool, } impl Default for Memory { @@ -149,7 +138,7 @@ impl Default for Memory { popups: Default::default(), everything_is_visible: Default::default(), add_fonts: Default::default(), - ime_interruption_time: Default::default(), + requested_interrupt_ime: Default::default(), }; slf.interactions.entry(slf.viewport_id).or_default(); slf.areas.entry(slf.viewport_id).or_default(); @@ -778,15 +767,7 @@ impl Memory { self.areas.entry(self.viewport_id).or_default(); - match self.ime_interruption_time { - ImeInterruptionTime::ThisFrame => { - self.ime_interruption_time = ImeInterruptionTime::LastFrame; - } - ImeInterruptionTime::LastFrame => { - self.ime_interruption_time = ImeInterruptionTime::None; - } - ImeInterruptionTime::None => {} - } + self.requested_interrupt_ime = false; // self.interactions is handled elsewhere @@ -1028,30 +1009,22 @@ impl Memory { /// /// A widget should only consume IME events if this returns `true`. At most /// one widget can own IME events for each frame. + #[inline(always)] pub fn owns_ime_events(&self, id: Id) -> bool { - let Some(focus) = self.focus() else { - return false; - }; - // We check across two frames because the widget that called - // `interrupt_ime` may run after other widgets that call this method - // within the same frame. - if matches!( - self.ime_interruption_time, - ImeInterruptionTime::ThisFrame | ImeInterruptionTime::LastFrame - ) { - return false; - } - focus.focused() == Some(id) + // Note: Even if the IME is being interrupted in the current frame, we + // should not return `false` here, since we still need + // `PlatformOutput::ime` to be set in such cases. + + self.has_focus(id) } /// Interrupt the current IME composition, if any. - /// - /// This causes [`Self::owns_ime_events`] to return `false` for all widgets - /// for the remainder of this frame and the next frame, giving time - /// for the IME to be dismissed (by making `platform_output.ime` be `None` - /// for at least one frame). pub fn interrupt_ime(&mut self) { - self.ime_interruption_time = ImeInterruptionTime::ThisFrame; + self.requested_interrupt_ime = true; + } + + pub(crate) fn should_interrupt_ime(&self) -> bool { + self.requested_interrupt_ime } } diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 9905a2a55..8ba188443 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -871,6 +871,7 @@ impl TextEdit<'_> { o.ime = Some(crate::output::IMEOutput { rect: to_global * inner_rect, cursor_rect: to_global * primary_cursor_rect, + should_interrupt_composition: false, }); }); } From 41b64fc6f35e811beebeec83ce75408ae4505f30 Mon Sep 17 00:00:00 2001 From: Dimitris Papaioannou Date: Sun, 12 Apr 2026 17:06:12 +0300 Subject: [PATCH 68/89] Call pre_present_notify before presenting (#8089) --- crates/eframe/src/native/wgpu_integration.rs | 2 ++ crates/egui-wgpu/src/winit.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 6d300d513..de691153f 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -722,6 +722,7 @@ impl WgpuWinitRunning<'_> { &clipped_primitives, &textures_delta, screenshot_commands, + window, ); for action in viewport.actions_requested.drain(..) { @@ -1111,6 +1112,7 @@ fn render_immediate_viewport( &clipped_primitives, &textures_delta, vec![], + window, ); egui_winit.handle_platform_output(window, platform_output); diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 3f6adfc27..d64644330 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -411,6 +411,7 @@ impl Painter { /// and the captures captured screenshot if it was requested. /// /// If `capture_data` isn't empty, a screenshot will be captured. + #[expect(clippy::too_many_arguments)] pub fn paint_and_update_textures( &mut self, viewport_id: ViewportId, @@ -419,6 +420,7 @@ impl Painter { clipped_primitives: &[epaint::ClippedPrimitive], textures_delta: &epaint::textures::TexturesDelta, capture_data: Vec, + window: &winit::window::Window, ) -> f32 { profiling::function_scope!(); @@ -654,6 +656,8 @@ impl Painter { ); } + window.pre_present_notify(); + { profiling::scope!("present"); // wgpu doesn't document where vsync can happen. Maybe here? From ba9e0eb6672f21d8cf7b7c4ef7938daece6f30cb Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 13 Apr 2026 11:57:34 +0200 Subject: [PATCH 69/89] Add taplo-fmt CI step (#8095) --- .github/workflows/taplo.yml | 25 +++++++++++++++++++++++++ Cargo.toml | 8 ++++++-- crates/egui_glow/Cargo.toml | 9 +++++++-- crates/egui_kittest/Cargo.toml | 2 +- crates/epaint/Cargo.toml | 9 ++++++++- 5 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/taplo.yml diff --git a/.github/workflows/taplo.yml b/.github/workflows/taplo.yml new file mode 100644 index 000000000..11a7b978a --- /dev/null +++ b/.github/workflows/taplo.yml @@ -0,0 +1,25 @@ +# Checks that all TOML files are formatted with taplo. +name: Taplo + +on: + push: + branches: + - "main" + pull_request: + types: [opened, synchronize] + +jobs: + taplo: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Taplo + uses: taiki-e/install-action@v2.48.7 + with: + tool: taplo-cli@0.9.3 + + - name: Check TOML formatting + run: | + taplo fmt --check diff --git a/Cargo.toml b/Cargo.toml index 0e592637c..4559152a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,12 +134,16 @@ syntect = { version = "5.3.0", default-features = false } tempfile = "3.23.0" thiserror = "2.0.17" tokio = "1.49" -toml = {version = "1.0.0", default-features = false } +toml = { version = "1.0.0", default-features = false } type-map = "0.5.1" unicode_names2 = { version = "2.0.0", default-features = false } unicode-general-category = "1.1.0" unicode-segmentation = "1.12.0" -vello_cpu = { version = "0.0.7", default-features = false, features = ["std", "u8_pipeline", "f32_pipeline"] } +vello_cpu = { version = "0.0.7", default-features = false, features = [ + "std", + "u8_pipeline", + "f32_pipeline", +] } wasm-bindgen = "0.2.108" # Keep wasm-bindgen version in sync in: setup_web.sh, Cargo.toml, Cargo.lock, rust.yml. Don't update this spuriously, because of https://github.com/rerun-io/rerun/issues/8766 wasm-bindgen-futures = "0.4.58" wayland-cursor = { version = "0.31.11", default-features = false } diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index f714dd8e0..c5ff714b3 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -11,7 +11,13 @@ readme = "README.md" repository = "https://github.com/emilk/egui/tree/main/crates/egui_glow" categories = ["gui", "game-development"] keywords = ["glow", "egui", "gui", "gamedev"] -include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml", "src/shader/*.glsl"] +include = [ + "../../LICENSE-APACHE", + "../../LICENSE-MIT", + "**/*.rs", + "Cargo.toml", + "src/shader/*.glsl", +] [lints] workspace = true @@ -65,7 +71,6 @@ winit = { workspace = true, optional = true, default-features = false, features web-sys = { workspace = true, features = ["console"] } wasm-bindgen.workspace = true - [dev-dependencies] glutin = { workspace = true, default-features = true } # examples/pure_glow glutin-winit = { workspace = true, default-features = true } diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index 10938cd37..2d26e6bd8 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -38,7 +38,7 @@ egui.workspace = true eframe = { workspace = true, optional = true } kittest.workspace = true serde.workspace = true -toml = {workspace = true, features = ["parse", "serde"] } +toml = { workspace = true, features = ["parse", "serde"] } # wgpu dependencies egui-wgpu = { workspace = true, optional = true } diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 705f1f6a9..f16903730 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -48,7 +48,14 @@ mint = ["emath/mint"] rayon = ["dep:rayon"] ## Allow serialization using [`serde`](https://docs.rs/serde). -serde = ["dep:serde", "ahash/serde", "emath/serde", "ecolor/serde", "font-types/serde", "smallvec/serde"] +serde = [ + "dep:serde", + "ahash/serde", + "ecolor/serde", + "emath/serde", + "font-types/serde", + "smallvec/serde", +] ## Change Vertex layout to be compatible with unity unity = [] From 170b46a0c860c429ddf5774fd6ebd07a46f5f234 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 13 Apr 2026 11:57:41 +0200 Subject: [PATCH 70/89] Update `rand` (#8096) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd91fea8e..6efed68d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1337,7 +1337,7 @@ dependencies = [ "image", "jiff", "mimalloc", - "rand 0.9.2", + "rand 0.9.3", "serde", "unicode_names2", ] @@ -3657,9 +3657,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", From db87c712a1c02e6f1577c682b4f90fb926482364 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 13 Apr 2026 17:48:43 +0200 Subject: [PATCH 71/89] Replace `cargo machete` with `cargo shear` (#8094) We've had good experiences with `cargo shear` at Rerun --- .github/workflows/cargo_machete.yml | 19 -------------- .github/workflows/cargo_shear.yml | 25 +++++++++++++++++++ Cargo.lock | 2 -- crates/egui-winit/Cargo.toml | 3 +++ crates/egui_demo_app/Cargo.toml | 4 +-- crates/egui_demo_lib/Cargo.toml | 3 +++ crates/egui_glow/Cargo.toml | 4 --- examples/custom_style/Cargo.toml | 4 +-- examples/hello_world_par/Cargo.toml | 2 +- examples/images/Cargo.toml | 2 +- examples/puffin_profiler/Cargo.toml | 2 +- tests/egui_tests/Cargo.toml | 3 +++ tests/test_egui_extras_compilation/Cargo.toml | 2 +- 13 files changed, 42 insertions(+), 33 deletions(-) delete mode 100644 .github/workflows/cargo_machete.yml create mode 100644 .github/workflows/cargo_shear.yml diff --git a/.github/workflows/cargo_machete.yml b/.github/workflows/cargo_machete.yml deleted file mode 100644 index 1dc162e56..000000000 --- a/.github/workflows/cargo_machete.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Cargo Machete - -on: [push, pull_request] - -jobs: - cargo-machete: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: 1.92 - - name: Machete install - ## The official cargo-machete action - uses: bnjbvr/cargo-machete@v0.9.1 - - name: Checkout - uses: actions/checkout@v4 - - name: Machete Check - run: cargo machete diff --git a/.github/workflows/cargo_shear.yml b/.github/workflows/cargo_shear.yml new file mode 100644 index 000000000..734abacb5 --- /dev/null +++ b/.github/workflows/cargo_shear.yml @@ -0,0 +1,25 @@ +# Looks for unused crates. +name: Cargo Shear + +on: + push: + branches: + - "main" + pull_request: + types: [opened, synchronize] + +jobs: + cargo-shear: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Cargo Shear + uses: taiki-e/install-action@v2.48.7 + with: + tool: cargo-shear@1.11.2 + + - name: Run Cargo Shear + run: | + cargo shear diff --git a/Cargo.lock b/Cargo.lock index 6efed68d0..caac14876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1375,8 +1375,6 @@ dependencies = [ "log", "memoffset", "profiling", - "wasm-bindgen", - "web-sys", "winit", ] diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index dd4aa8f9d..7fea2341b 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -20,6 +20,9 @@ workspace = true all-features = true rustdoc-args = ["--generate-link-to-definition"] +[package.metadata.cargo-shear] +ignored = ["wayland-cursor"] # TODO(emilk): remove when we update winit + [features] default = ["clipboard", "links", "wayland", "winit/default", "x11"] diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 5bb093ced..3b4de53e6 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -8,8 +8,8 @@ rust-version.workspace = true publish = false default-run = "egui_demo_app" -[package.metadata.cargo-machete] -ignored = ["profiling"] +[package.metadata.cargo-shear] +ignored = ["image", "profiling", "wasm-bindgen-futures"] [lints] workspace = true diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index dc57fb092..d2586fa11 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -20,6 +20,9 @@ workspace = true all-features = true rustdoc-args = ["--generate-link-to-definition"] +[package.metadata.cargo-shear] +ignored = ["image"] # We need the png feature + [lib] diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index c5ff714b3..d5a9f3716 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -66,10 +66,6 @@ document-features = { workspace = true, optional = true } # Native: winit = { workspace = true, optional = true, default-features = false, features = ["rwh_06"] } -# Web: -[target.'cfg(target_arch = "wasm32")'.dependencies] -web-sys = { workspace = true, features = ["console"] } -wasm-bindgen.workspace = true [dev-dependencies] glutin = { workspace = true, default-features = true } # examples/pure_glow diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index 83174df40..3352aa474 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -10,8 +10,8 @@ publish = false workspace = true -[package.metadata.cargo-machete] -ignored = ["image"] # We need the .png feature +[package.metadata.cargo-shear] +ignored = ["image"] # We need the png feature [dependencies] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index ef719b742..04a3c0e25 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -11,7 +11,7 @@ publish = false workspace = true -[package.metadata.cargo-machete] +[package.metadata.cargo-shear] ignored = ["winit"] # Just enable some features of it; see below diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index 7c3b77b7c..adac2a540 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -11,7 +11,7 @@ publish = false workspace = true -[package.metadata.cargo-machete] +[package.metadata.cargo-shear] ignored = ["image"] # We only use the dependency to add more features to it diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index 49e6598db..ed8ce3aa0 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" rust-version = "1.92" publish = false -[package.metadata.cargo-machete] +[package.metadata.cargo-shear] ignored = ["profiling"] [lints] diff --git a/tests/egui_tests/Cargo.toml b/tests/egui_tests/Cargo.toml index 8c09042b1..44a7b9c8f 100644 --- a/tests/egui_tests/Cargo.toml +++ b/tests/egui_tests/Cargo.toml @@ -5,6 +5,9 @@ license.workspace = true rust-version.workspace = true version.workspace = true +[package.metadata.cargo-shear] +ignored = ["image"] # We need the png feature + [dev-dependencies] egui = { workspace = true, default-features = true } egui_kittest = { workspace = true, features = ["snapshot", "wgpu"] } diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index 3598362a9..3e1d166f7 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -9,7 +9,7 @@ publish = false [lints] workspace = true -[package.metadata.cargo-machete] +[package.metadata.cargo-shear] ignored = ["eframe", "egui_extras"] # We don't use them, just check that things compile [dependencies] From 770090a6fff5500b22d73da1ee2d6da609d0e719 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 14 Apr 2026 10:52:28 +0200 Subject: [PATCH 72/89] Fix `egui_demo_app` on Linux (#8100) * Closes https://github.com/emilk/egui/issues/8098 --- crates/egui_demo_app/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 3b4de53e6..f9a153268 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -23,7 +23,7 @@ crate-type = ["cdylib", "rlib"] [features] -default = ["wgpu", "persistence"] +default = ["wgpu", "persistence", "wayland", "x11"] web_app = ["http", "persistence"] From 152b97b434b6a2ff11b5d77b50a659ce1945e185 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 14 Apr 2026 11:35:52 +0200 Subject: [PATCH 73/89] Warn if using a software rasterizer (#8101) * Related to https://github.com/emilk/egui/issues/8093 --- crates/egui-wgpu/src/lib.rs | 41 +++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 180b305be..5ab91557b 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -138,29 +138,12 @@ async fn request_adapter( } })?; - if cfg!(target_arch = "wasm32") { - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) + if 1 < available_adapters.len() { + log::info!( + "There are {} available wgpu adapters: {}", + available_adapters.len(), + describe_adapters(available_adapters) ); - } else { - // native: - if available_adapters.len() == 1 { - log::debug!( - "Picked the only available wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } else { - log::info!( - "There were {} available wgpu adapters: {}", - available_adapters.len(), - describe_adapters(available_adapters) - ); - log::debug!( - "Picked wgpu adapter: {}", - adapter_info_summary(&adapter.get_info()) - ); - } } Ok(adapter) @@ -236,6 +219,8 @@ impl RenderState { }) => (adapter, device, queue), }; + log_adapter_info(&adapter.get_info()); + let surface_formats = { profiling::scope!("get_capabilities"); compatible_surface.map_or_else( @@ -406,6 +391,18 @@ pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option String { let wgpu::AdapterInfo { From 5c96f4f080b9124e9a1bbb3c53c51b1ac272eeab Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 14 Apr 2026 13:00:35 +0200 Subject: [PATCH 74/89] Document glow-only fields in `NativeOptions` (#8104) --- crates/eframe/src/epi.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index b9a178a1d..bc19951bf 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -317,6 +317,8 @@ pub struct NativeOptions { /// Turn on vertical syncing, limiting the FPS to the display refresh rate. /// /// The default is `true`. + /// + /// Only affects the `glow` backend. pub vsync: bool, /// Set the level of the multisampling anti-aliasing (MSAA). @@ -343,6 +345,8 @@ pub struct NativeOptions { /// Specify whether or not hardware acceleration is preferred, required, or not. /// /// Default: [`HardwareAcceleration::Preferred`]. + /// + /// Only affects the `glow` backend. pub hardware_acceleration: HardwareAcceleration, /// What rendering backend to use. From 902906f9893a07f893ddc876c943febb8cd4da8d Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Tue, 14 Apr 2026 13:13:59 +0200 Subject: [PATCH 75/89] Fix centered & right aligned `TextEdit` (#8082) A couple improvements to centered and right-aligned text edits: - Fix text selection in centered and right aligned text edits (ironically, this broke in #8076) - Fix cursor movement in centered and right aligned text edits (horizontal cursor position will be retained on vertical movement) - Multiline text edit exceeding available width if there are atoms - Added atoms & alignment options to text edit demo - Improve how vertical_align and horizontal_align are applied - Textedit atom is grow now, removing the need for the extra seperate grow atom - This allows us to apply the `align` on the text edit atom instead of the whole AtomLayout - Fixes https://github.com/emilk/egui/pull/8022 - Fixes https://github.com/emilk/egui/issues/7999 --- crates/egui/src/widgets/text_edit/builder.rs | 36 +++++++++------- crates/egui_demo_lib/src/demo/text_edit.rs | 42 ++++++++++++++++++- .../tests/snapshots/demos/TextEdit.png | 4 +- crates/epaint/src/text/text_layout_types.rs | 6 ++- .../layout/text_edit_prefix_suffix.png | 4 +- .../tests/snapshots/text_edit_halign.png | 4 +- .../visuals/text_edit_prefix_suffix.png | 4 +- 7 files changed, 74 insertions(+), 26 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 8ba188443..416cd8c9b 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -4,8 +4,8 @@ use emath::{Rect, TSTransform}; use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor}; use crate::{ - Align, Align2, Atom, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, - Event, EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key, + Align, Align2, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, CursorIcon, Event, + EventFilter, FontSelection, Frame, Id, ImeEvent, IntoAtoms, IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint, os::OperatingSystem, @@ -480,11 +480,12 @@ impl TextEdit<'_> { let font_id_clone = font_id.clone(); let mut default_layouter = move |ui: &Ui, text: &dyn TextBuffer, wrap_width: f32| { let text = mask_if_password(password, text.as_str()); - let layout_job = if multiline { + let mut layout_job = if multiline { LayoutJob::simple(text, font_id_clone.clone(), text_color, wrap_width) } else { LayoutJob::simple_singleline(text, font_id_clone.clone(), text_color) }; + layout_job.halign = align.x(); ui.fonts_mut(|f| f.layout_job(layout_job)) }; @@ -591,6 +592,7 @@ impl TextEdit<'_> { if !shrunk && matches!(atom.kind, AtomKind::Text(_)) { // elide the hint_text if needed atom = atom.atom_shrink(true); + atom = atom.atom_grow(true); shrunk = true; } @@ -619,6 +621,11 @@ impl TextEdit<'_> { get_galley = Some(galley); } else { + // We need to shrink when clip_text, so that we don't exceed the available size + // and thus clip. We also need to shrink in multi line text edits, so text can + // wrap appropriately. + let should_shrink = clip_text || multiline; + // We need a closure here, so we can calculate the galley based on the available // width (after adding suffix and prefix), for correct wrapping in multi line text // edits @@ -645,16 +652,13 @@ impl TextEdit<'_> { sized: SizedAtomKind::Empty { size: Some(size) }, } }) + .atom_grow(true) + .atom_align(self.align) .atom_id(inner_rect_id) - .atom_shrink(clip_text), + .atom_shrink(should_shrink), ); } - // Ensure the suffix is always right-aligned - if !suffix.is_empty() { - atoms.push_right(Atom::grow()); - } - // TODO(servo/rust-smallvec#146): Use extend_right instead of the loop once we have // smallvec 2.0. Using `extend_right` here won't compile, due to lifetime issues. for atom in suffix { @@ -679,7 +683,7 @@ impl TextEdit<'_> { .max_width(allocate_width) .sense(sense) .frame(frame) - .align2(Align2::LEFT_TOP) + .align2(align) .wrap_mode(wrap_mode) .allocate(ui); @@ -740,16 +744,18 @@ impl TextEdit<'_> { // TODO(emilk): drag selected text to either move or clone (ctrl on windows, alt on mac) - let cursor_at_pointer = - galley.cursor_from_pos(pointer_pos - inner_rect.min + state.text_offset); + let cursor_at_pointer = galley.cursor_from_pos( + pointer_pos - inner_rect.min + state.text_offset + vec2(galley.rect.left(), 0.0), + ); if ui.visuals().text_cursor.preview && response.hovered() && ui.input(|i| i.pointer.is_moving()) { // text cursor preview: - let cursor_rect = TSTransform::from_translation(inner_rect.min.to_vec2()) - * cursor_rect(&galley, &cursor_at_pointer, row_height); + let cursor_rect = TSTransform::from_translation( + inner_rect.min.to_vec2() - vec2(galley.rect.left(), 0.0), + ) * cursor_rect(&galley, &cursor_at_pointer, row_height); text_selection::visuals::paint_cursor_end(&painter, ui.visuals(), cursor_rect); } @@ -835,7 +841,7 @@ impl TextEdit<'_> { if has_focus && let Some(cursor_range) = state.cursor.range(&galley) { let primary_cursor_rect = cursor_rect(&galley, &cursor_range.primary, row_height) - .translate(galley_pos.to_vec2()); + .translate(galley_pos.to_vec2() - vec2(galley.rect.left(), 0.0)); if response.changed() || selection_changed { // Scroll to keep primary cursor in view: diff --git a/crates/egui_demo_lib/src/demo/text_edit.rs b/crates/egui_demo_lib/src/demo/text_edit.rs index 3ec53a523..8fd42da11 100644 --- a/crates/egui_demo_lib/src/demo/text_edit.rs +++ b/crates/egui_demo_lib/src/demo/text_edit.rs @@ -1,15 +1,21 @@ +use egui::{Align, Align2, AtomExt as _}; + /// Showcase [`egui::TextEdit`]. #[derive(PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] pub struct TextEditDemo { pub text: String, + halign: egui::Align, + valign: egui::Align, } impl Default for TextEditDemo { fn default() -> Self { Self { text: "Edit this text".to_owned(), + halign: egui::Align::LEFT, + valign: egui::Align::TOP, } } } @@ -37,7 +43,11 @@ impl crate::View for TextEditDemo { ui.add(crate::egui_github_link_file!()); }); - let Self { text } = self; + let Self { + text, + halign, + valign, + } = self; ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; @@ -46,10 +56,40 @@ impl crate::View for TextEditDemo { ui.label("."); }); + ui.horizontal(|ui| { + ui.label("Horizontal align:"); + ui.selectable_value(halign, egui::Align::LEFT, "Left"); + ui.selectable_value(halign, egui::Align::Center, "Center"); + ui.selectable_value(halign, egui::Align::RIGHT, "Right"); + }); + ui.horizontal(|ui| { + ui.label("Vertical align:"); + ui.selectable_value(valign, egui::Align::TOP, "Top"); + ui.selectable_value(valign, egui::Align::Center, "Center"); + ui.selectable_value(valign, egui::Align::BOTTOM, "Bottom"); + }); + + let clear_id = egui::Id::new("clear_button"); + let clear_size = egui::Vec2::splat(ui.spacing().interact_size.y); + let output = egui::TextEdit::multiline(text) .hint_text("Type something!") + // Atoms are centered by default, so we need to pass the right align here: + .prefix("šŸ”Ž".atom_align(Align2([Align::LEFT, *valign]))) + .suffix( + egui::Atom::custom(clear_id, clear_size) + .atom_align(Align2([Align::RIGHT, *valign])), + ) + .horizontal_align(*halign) + .vertical_align(*valign) .show(ui); + if let Some(rect) = output.response.rect(clear_id) + && ui.place(rect, egui::Button::new("āŒ")).clicked() + { + text.clear(); + } + ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("Selected text: "); diff --git a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png index 81b49688b..839a15faa 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/TextEdit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c09529c3a1c26c8f28c00fc15cc5f495842862276870c24b5ee0713954f97fc -size 21916 +oid sha256:94c4af5715992f4dbb5bbec6ce67eec1e2f66cfc078a3e704ec386bdb482cac4 +size 30064 diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 481e3ebbf..fc987890d 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1244,7 +1244,8 @@ impl Galley { let new_layout_cursor = { // keep same X coord - let column = self.rows[new_row].char_at(h_pos); + // char_at is Row-relative, so subtract the row's position + let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x); LayoutCursor { row: new_row, column, @@ -1266,7 +1267,8 @@ impl Galley { let new_layout_cursor = { // keep same X coord - let column = self.rows[new_row].char_at(h_pos); + // char_at is Row-relative, so subtract the row's position + let column = self.rows[new_row].char_at(h_pos - self.rows[new_row].pos.x); LayoutCursor { row: new_row, column, diff --git a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png index 439e89094..642a8c5fd 100644 --- a/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png +++ b/tests/egui_tests/tests/snapshots/layout/text_edit_prefix_suffix.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a40004fe56075f31162e16c7c59c00d7e1b8132bbea603b3c54c4dec0875b1bb -size 364491 +oid sha256:a9f298f8ea6692e7ccbddbe182a91824ce262913d50e9a7df104a6c63d39d8a0 +size 372564 diff --git a/tests/egui_tests/tests/snapshots/text_edit_halign.png b/tests/egui_tests/tests/snapshots/text_edit_halign.png index bda8964c8..2f81509e6 100644 --- a/tests/egui_tests/tests/snapshots/text_edit_halign.png +++ b/tests/egui_tests/tests/snapshots/text_edit_halign.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f567547c446ffa75f968e0ffc505560f3b3d4171319fbe59be27dde4e553e287 -size 13273 +oid sha256:48114ad6d116fb9288ce9fe3b173017bda317f69753e7ac03a090b8d02d6cb4d +size 13258 diff --git a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png index c61f99ed0..e00868b1f 100644 --- a/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png +++ b/tests/egui_tests/tests/snapshots/visuals/text_edit_prefix_suffix.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e11dbb1d48a3eadecb5c0e36917785fa1f107e4e283ff2f76831482fe7cd2042 -size 10051 +oid sha256:a37ed30425967301ffa7dda3fdc8f316dfd7f4665c731b17778e3e5942783e81 +size 10534 From 3607aae91d7bd5b3895cfa4a7e2056c41e0a5bfe Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 14 Apr 2026 13:14:16 +0200 Subject: [PATCH 76/89] Configure wgpu to be low-latency by default (#8103) This changes the default value of `WgpuConfiguration::desired_maximum_frame_latency` to `Some(1)`. For low-Hz displays, this results in significantly lower input latency. * Closes https://github.com/emilk/egui/issues/5037 ? * Related to https://github.com/emilk/egui/issues/7761 --- crates/egui-wgpu/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 5ab91557b..75155899d 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -328,7 +328,12 @@ impl Default for WgpuConfiguration { fn default() -> Self { Self { present_mode: wgpu::PresentMode::AutoVsync, - desired_maximum_frame_latency: None, + desired_maximum_frame_latency: if cfg!(target_os = "ios") { + None // The default is good on iOS, while `Some(1)` cuts FPS in half + } else { + Some(1) // Low-latency by default. + }, + // No display handle available at this point — callers should replace this with // `WgpuSetup::from_display_handle(...)` before creating the instance if one is available. wgpu_setup: WgpuSetup::without_display_handle(), From fe5533e4501848430dfa77af723321b321af11d3 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 14 Apr 2026 20:49:54 +0900 Subject: [PATCH 77/89] Optimize text selection performance for large documents (#7917) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Perf: Optimize text selection and navigation performance for large documents** #### **Summary** This PR significantly improves the performance of text selection (double-clicking) and cursor navigation within `TextEdit` and `Label` widgets, particularly when handling large documents (e.g., 1MB+ or logs). It eliminates several $O(N^2)$ bottlenecks and unnecessary memory allocations in `text_cursor_state.rs`. #### **Problems Identified** 1. **$O(N^2)$ Word Boundary Scanning:** In `next_word_boundary_char_index`, `char_index_from_byte_index` was called repeatedly inside a loop. This caused the entire document to be scanned from the beginning for every word found, leading to quadratic time complexity. 2. **Heavy String Allocations:** `ccursor_previous_word` used `collect::()` and `rev()` to search backwards, causing a full copy and memory allocation of the text (or line) every time the user moved the cursor or double-clicked. 3. **Inefficient Line Start Finding:** `find_line_start` performed global character counts (`text.chars().count()`) and global skips, which is very slow for large files. 4. **Global Search Scope:** `select_word_at` was performing word boundary searches across the entire document even for simple double-click actions. #### **Key Changes & Optimizations** 1. **Line-Scoped Selection:** Updated `select_word_at` to first identify the current line and then perform word boundary searches within that local scope. This reduces the search space from millions of characters to hundreds. 2. **Linear Time ($O(N)$) Boundary Search:** Refactored `next_word_boundary_char_index` to use a running cumulative character counter. This ensures the text is scanned only once. 3. **Zero-Allocation Backwards Search:** Optimized `ccursor_previous_word` to use `next_back()` on the `DoubleEndedIterator` provided by `unicode-segmentation`. This removes all temporary `String` allocations. 4. **Byte-Based Line Search:** Optimized `find_line_start` to use byte-based reverse scanning (`rfind('\n')`), which is significantly faster than counting characters from the start of the document. #### **Performance Impact** In my tests with large text files (over 10,000 lines / 1MB+): - **Before:** Double-clicking a word caused a UI freeze for 2–5 seconds. - **After:** Word selection and navigation are near-instantaneous (0–1ms), providing a smooth "native-like" experience even in WASM environments. --- .../src/text_selection/text_cursor_state.rs | 136 +++++++++++------- 1 file changed, 85 insertions(+), 51 deletions(-) diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index 12e0656d8..ee7f6e512 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -106,38 +106,26 @@ impl TextCursorState { } fn select_word_at(text: &str, ccursor: CCursor) -> CCursorRange { - if ccursor.index == 0 { - CCursorRange::two(ccursor, ccursor_next_word(text, ccursor)) - } else { - let it = text.chars(); - let mut it = it.skip(ccursor.index - 1); - if let Some(char_before_cursor) = it.next() { - if let Some(char_after_cursor) = it.next() { - if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) { - let min = ccursor_previous_word(text, ccursor + 1); - let max = ccursor_next_word(text, min); - CCursorRange::two(min, max) - } else if is_word_char(char_before_cursor) { - let min = ccursor_previous_word(text, ccursor); - let max = ccursor_next_word(text, min); - CCursorRange::two(min, max) - } else if is_word_char(char_after_cursor) { - let max = ccursor_next_word(text, ccursor); - CCursorRange::two(ccursor, max) - } else { - let min = ccursor_previous_word(text, ccursor); - let max = ccursor_next_word(text, ccursor); - CCursorRange::two(min, max) - } - } else { - let min = ccursor_previous_word(text, ccursor); - CCursorRange::two(min, ccursor) - } - } else { - let max = ccursor_next_word(text, ccursor); - CCursorRange::two(ccursor, max) - } + if text.is_empty() { + return CCursorRange::one(ccursor); } + + let line_start = find_line_start(text, ccursor); + let line_end = ccursor_next_line(text, line_start); + + let line_range = line_start.index..line_end.index; + let current_line_text = slice_char_range(text, line_range.clone()); + + let relative_idx = ccursor.index - line_start.index; + let relative_ccursor = CCursor::new(relative_idx); + + let min = ccursor_previous_word(current_line_text, relative_ccursor); + let max = ccursor_next_word(current_line_text, relative_ccursor); + + CCursorRange::two( + CCursor::new(line_start.index + min.index), + CCursor::new(line_start.index + max.index), + ) } fn select_line_at(text: &str, ccursor: CCursor) -> CCursorRange { @@ -209,16 +197,20 @@ fn ccursor_previous_line(text: &str, ccursor: CCursor) -> CCursor { } fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize { - for (word_byte_index, word) in text.split_word_bound_indices() { - let word_ci = char_index_from_byte_index(text, word_byte_index); + let mut current_char_idx = 0; + + for (_word_byte_index, word) in text.split_word_bound_indices() { + let word_ci = current_char_idx; // We consider `.` a word boundary. // At least that's how Mac works when navigating something like `www.example.com`. - for (dot_ci_offset, chr) in word.chars().enumerate() { - let dot_ci = word_ci + dot_ci_offset; + let mut word_char_count = 0; + for chr in word.chars() { + let dot_ci = word_ci + word_char_count; if chr == '.' && cursor_ci < dot_ci { return dot_ci; } + word_char_count += 1; } // Splitting considers contiguous whitespace as one word, such words must be skipped, @@ -228,9 +220,11 @@ fn next_word_boundary_char_index(text: &str, cursor_ci: usize) -> usize { if cursor_ci < word_ci && !all_word_chars(word) { return word_ci; } + + current_char_idx += word_char_count; } - char_index_from_byte_index(text, text.len()) + current_char_idx } fn all_word_chars(text: &str) -> bool { @@ -265,22 +259,14 @@ fn is_linebreak(c: char) -> bool { /// Accepts and returns character offset (NOT byte offset!). pub fn find_line_start(text: &str, current_index: CCursor) -> CCursor { - // We know that new lines, '\n', are a single byte char, but we have to - // work with char offsets because before the new line there may be any - // number of multi byte chars. - // We need to know the char index to be able to correctly set the cursor - // later. - let chars_count = text.chars().count(); + let byte_idx = byte_index_from_char_index(text, current_index.index); + let text_before = &text[..byte_idx]; - let position = text - .chars() - .rev() - .skip(chars_count - current_index.index) - .position(|x| x == '\n'); - - match position { - Some(pos) => CCursor::new(current_index.index - pos), - None => CCursor::new(0), + if let Some(last_newline_byte) = text_before.rfind('\n') { + let char_idx = char_index_from_byte_index(text, last_newline_byte + 1); + CCursor::new(char_idx) + } else { + CCursor::new(0) } } @@ -367,3 +353,51 @@ mod test { assert_eq!(next_word_boundary_char_index(text, 20), 21); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_previous_word_graphemes() { + let cases = [ + ("", 0, 0), + ("hello", 0, 0), + ("hello", "hello".chars().count(), 0), + ("hello world", 6, 0), + ("hello world", 8, 6), + ("hello world", "hello world".chars().count(), 6), + ("hello world ", "hello world ".chars().count(), 6), + ("hello world", "hello world".chars().count(), 8), + (" ", " ".chars().count(), 0), + ("hello, world", "hello, world".chars().count(), 7), + ("www.example.com", "www.example.com".chars().count(), 12), + ("ģ•ˆė…•! 😊 ģ„øģƒ", 8, 6), + ("ā¤ļøšŸ‘ skvělĆ” knihovna šŸ‘ā¤ļø", 18, 11), + ( + "a e\u{301} b", + "a e\u{301} b".chars().count(), + "a e\u{301} ".chars().count(), + ), + ( + "hi šŸ™‚ world", + "hi šŸ™‚ world".chars().count(), + "hi šŸ™‚ ".chars().count(), + ), + ( + "hi šŸ‘Øā€šŸ‘©ā€šŸ‘§ā€šŸ‘¦ world", + "hi šŸ‘Øā€šŸ‘©ā€šŸ‘§ā€šŸ‘¦ world".chars().count(), + "hi šŸ‘Øā€šŸ‘©ā€šŸ‘§ā€šŸ‘¦ ".chars().count(), + ), + ]; + + for (text, cursor, expected) in cases { + let result = ccursor_previous_word(text, CCursor::new(cursor)); + assert_eq!( + result.index, expected, + "text={text:?}, cursor={cursor}, got={}, expected={expected}", + result.index + ); + } + } +} From 1cd89b5edcbaa3ab6080e9d00e760492a0d30be1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 14 Apr 2026 20:19:36 +0200 Subject: [PATCH 78/89] Remove everything that was marked `#[deprecated]` (#8105) Simplify. Streamline. Spring cleaning. --- crates/eframe/src/epi.rs | 24 +- crates/eframe/src/lib.rs | 65 +- crates/eframe/src/native/epi_integration.rs | 8 +- crates/eframe/src/web/app_runner.rs | 3 - crates/egui/src/containers/area.rs | 4 +- .../egui/src/containers/collapsing_header.rs | 9 - crates/egui/src/containers/combo_box.rs | 6 - crates/egui/src/containers/frame.rs | 15 - crates/egui/src/containers/menu.rs | 5 +- crates/egui/src/containers/mod.rs | 2 - crates/egui/src/containers/old_popup.rs | 212 ----- crates/egui/src/containers/panel.rs | 226 +---- crates/egui/src/containers/popup.rs | 8 +- crates/egui/src/containers/resize.rs | 7 - crates/egui/src/containers/scroll_area.rs | 33 - crates/egui/src/containers/tooltip.rs | 18 - crates/egui/src/containers/window.rs | 6 +- crates/egui/src/context.rs | 230 +----- crates/egui/src/data/input.rs | 6 +- crates/egui/src/data/output.rs | 2 +- crates/egui/src/hit_test.rs | 2 +- crates/egui/src/input_state/mod.rs | 10 +- crates/egui/src/layers.rs | 8 +- crates/egui/src/lib.rs | 12 +- crates/egui/src/memory/mod.rs | 63 +- crates/egui/src/menu.rs | 781 ------------------ crates/egui/src/painter.rs | 41 - crates/egui/src/pass_state.rs | 93 +-- crates/egui/src/style.rs | 27 - .../egui/src/text_selection/cursor_range.rs | 6 - crates/egui/src/ui.rs | 276 +------ crates/egui/src/util/mod.rs | 4 - crates/egui/src/viewport.rs | 2 +- crates/egui/src/widget_text.rs | 4 +- crates/egui/src/widgets/button.rs | 6 - crates/egui/src/widgets/drag_value.rs | 16 - crates/egui/src/widgets/image.rs | 12 - crates/egui/src/widgets/image_button.rs | 164 ---- crates/egui/src/widgets/mod.rs | 28 +- crates/egui/src/widgets/progress_bar.rs | 6 - crates/egui/src/widgets/selected_label.rs | 13 - crates/egui/src/widgets/slider.rs | 12 +- crates/egui_extras/src/datepicker/button.rs | 8 - crates/egui_extras/src/table.rs | 11 +- crates/egui_kittest/src/app_kind.rs | 22 +- crates/egui_kittest/src/builder.rs | 63 -- crates/egui_kittest/src/lib.rs | 68 +- crates/egui_kittest/src/node.rs | 27 - crates/epaint/src/lib.rs | 6 - crates/epaint/src/margin_f32.rs | 15 - crates/epaint/src/shapes/shape.rs | 6 - crates/epaint/src/tessellator.rs | 10 - 52 files changed, 78 insertions(+), 2633 deletions(-) delete mode 100644 crates/egui/src/containers/old_popup.rs delete mode 100644 crates/egui/src/menu.rs delete mode 100644 crates/egui/src/widgets/image_button.rs delete mode 100644 crates/egui/src/widgets/selected_label.rs diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index bc19951bf..a9ce726fe 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -2,7 +2,7 @@ //! //! `epi` provides interfaces for window management and serialization. //! -//! Start by looking at the [`App`] trait, and implement [`App::update`]. +//! Start by looking at the [`App`] trait, and implement [`App::ui`]. #![warn(missing_docs)] // Let's keep `epi` well-documented. @@ -161,22 +161,6 @@ pub trait App { /// (A "viewport" in egui means an native OS window). fn ui(&mut self, ui: &mut egui::Ui, frame: &mut Frame); - /// Called each time the UI needs repainting, which may be many times per second. - /// - /// Put your widgets into a [`egui::Panel`], [`egui::CentralPanel`], [`egui::Window`] or [`egui::Area`]. - /// - /// The [`egui::Context`] can be cloned and saved if you like. - /// - /// To force a repaint, call [`egui::Context::request_repaint`] at any time (e.g. from another thread). - /// - /// This is called for the root viewport ([`egui::ViewportId::ROOT`]). - /// Use [`egui::Context::show_viewport_deferred`] to spawn additional viewports (windows). - /// (A "viewport" in egui means an native OS window). - #[deprecated = "Use Self::ui instead"] - fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) { - _ = (ctx, frame); - } - /// Get a handle to the app. /// /// Can be used from web to interact or other external context. @@ -256,7 +240,7 @@ pub trait App { true } - /// A hook for manipulating or filtering raw input before it is processed by [`Self::update`]. + /// A hook for manipulating or filtering raw input before it is processed by [`Self::ui`]. /// /// This function provides a way to modify or filter input events before they are processed by egui. /// @@ -780,7 +764,7 @@ impl Frame { /// * Read the pixel buffer from the previous frame (`glow::Context::read_pixels`). /// * Render things behind the egui windows. /// - /// Note that all egui painting is deferred to after the call to [`App::update`] + /// Note that all egui painting is deferred to after the call to [`App::ui`] /// ([`egui`] only collects [`egui::Shape`]s and then eframe paints them all in one go later on). /// /// To get a [`glow`] context you need to compile with the `glow` feature flag, @@ -886,7 +870,7 @@ pub struct IntegrationInfo { /// Seconds of cpu usage (in seconds) on the previous frame. /// - /// This includes [`App::update`] as well as rendering (except for vsync waiting). + /// This includes [`App::ui`] as well as rendering (except for vsync waiting). /// /// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling). /// diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index c644289e0..9f7a4f3ef 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -6,7 +6,7 @@ //! To get started, see the [examples](https://github.com/emilk/egui/tree/main/examples). //! To learn how to set up `eframe` for web and native, go to and follow the instructions there! //! -//! In short, you implement [`App`] (especially [`App::update`]) and then +//! In short, you implement [`App`] (especially [`App::ui`]) and then //! call [`crate::run_native`] from your `main.rs`, and/or use `eframe::WebRunner` from your `lib.rs`. //! //! ## Compiling for web @@ -19,7 +19,7 @@ //! //! ## Simplified usage //! If your app is only for native, and you don't need advanced features like state persistence, -//! then you can use the simpler function [`run_simple_native`]. +//! then you can use the simpler function [`run_ui_native`]. //! //! ## Usage, native: //! ``` no_run @@ -446,67 +446,6 @@ pub fn run_ui_native( ) } -/// The simplest way to get started when writing a native app. -/// -/// This does NOT support persistence of custom user data. For that you need to use [`run_native`]. -/// However, it DOES support persistence of egui data (window positions and sizes, how far the user has scrolled in a -/// [`ScrollArea`](egui::ScrollArea), etc.) if the persistence feature is enabled. -/// -/// # Example -/// ``` no_run -/// fn main() -> eframe::Result { -/// // Our application state: -/// let mut name = "Arthur".to_owned(); -/// let mut age = 42; -/// -/// let options = eframe::NativeOptions::default(); -/// eframe::run_simple_native("My egui App", options, move |ctx, _frame| { -/// egui::CentralPanel::default().show(ctx, |ui| { -/// ui.heading("My egui Application"); -/// ui.horizontal(|ui| { -/// let name_label = ui.label("Your name: "); -/// ui.text_edit_singleline(&mut name) -/// .labelled_by(name_label.id); -/// }); -/// ui.add(egui::Slider::new(&mut age, 0..=120).text("age")); -/// if ui.button("Increment").clicked() { -/// age += 1; -/// } -/// ui.label(format!("Hello '{name}', age {age}")); -/// }); -/// }) -/// } -/// ``` -/// -/// # Errors -/// This function can fail if we fail to set up a graphics context. -#[deprecated = "Use run_ui_native instead"] -#[cfg(not(target_arch = "wasm32"))] -#[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] -pub fn run_simple_native( - app_name: &str, - native_options: NativeOptions, - update_fun: impl FnMut(&egui::Context, &mut Frame) + 'static, -) -> Result { - struct SimpleApp { - update_fun: U, - } - - impl App for SimpleApp { - fn ui(&mut self, _ui: &mut egui::Ui, _frame: &mut Frame) {} - - fn update(&mut self, ctx: &egui::Context, frame: &mut Frame) { - (self.update_fun)(ctx, frame); - } - } - - run_native( - app_name, - native_options, - Box::new(|_cc| Ok(Box::new(SimpleApp { update_fun }))), - ) -} - // ---------------------------------------------------------------------------- /// The different problems that can occur when trying to run `eframe`. diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 5b22eb08c..e558b0a2b 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -259,7 +259,7 @@ impl EpiIntegration { /// Run user code - this can create immediate viewports, so hold no locks over this! /// - /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`]. + /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`]. pub fn update( &mut self, app: &mut dyn epi::App, @@ -287,12 +287,6 @@ impl EpiIntegration { } if is_visible { - { - profiling::scope!("App::update"); - #[expect(deprecated)] - app.update(ui.ctx(), &mut self.frame); - } - { profiling::scope!("App::ui"); app.ui(ui, &mut self.frame); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 7161a665e..c15e78d68 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -284,9 +284,6 @@ impl AppRunner { self.app.logic(ui.ctx(), &mut self.frame); if is_visible { - #[expect(deprecated)] - self.app.update(ui.ctx(), &mut self.frame); - self.app.ui(ui, &mut self.frame); } }); diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index d44c0ae41..09488058d 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -280,7 +280,7 @@ impl Area { self } - /// Constrains this area to [`Context::screen_rect`]? + /// Constrains this area to [`Context::content_rect`]? /// /// Default: `true`. #[inline] @@ -291,7 +291,7 @@ impl Area { /// Constrain the movement of the window to the given rectangle. /// - /// For instance: `.constrain_to(ctx.screen_rect())`. + /// For instance: `.constrain_to(ctx.content_rect())`. #[inline] pub fn constrain_to(mut self, constrain_rect: Rect) -> Self { self.constrain = true; diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index aca8ab138..de3581288 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -451,15 +451,6 @@ impl CollapsingHeader { self } - /// Explicitly set the source of the [`Id`] of this widget, instead of using title label. - /// This is useful if the title label is dynamic or not unique. - #[deprecated = "Renamed id_salt"] - #[inline] - pub fn id_source(mut self, id_salt: impl Hash) -> Self { - self.id_salt = Id::new(id_salt); - self - } - /// If you set this to `false`, the [`CollapsingHeader`] will be grayed out and un-clickable. /// /// This is a convenience for [`Ui::disable`]. diff --git a/crates/egui/src/containers/combo_box.rs b/crates/egui/src/containers/combo_box.rs index c4097f803..adbda583c 100644 --- a/crates/egui/src/containers/combo_box.rs +++ b/crates/egui/src/containers/combo_box.rs @@ -94,12 +94,6 @@ impl ComboBox { } } - /// Without label. - #[deprecated = "Renamed from_id_salt"] - pub fn from_id_source(id_salt: impl std::hash::Hash) -> Self { - Self::from_id_salt(id_salt) - } - /// Set the outer width of the button and menu. /// /// Default is [`Spacing::combo_width`]. diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index d556f827e..5bbce626d 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -174,11 +174,6 @@ impl Frame { Self::NONE } - #[deprecated = "Use `Frame::NONE` or `Frame::new()` instead."] - pub const fn none() -> Self { - Self::NONE - } - /// For when you want to group a few widgets together within a frame. pub fn group(style: &Style) -> Self { Self::new() @@ -283,16 +278,6 @@ impl Frame { self } - /// The rounding of the _outer_ corner of the [`Self::stroke`] - /// (or, if there is no stroke, the outer corner of [`Self::fill`]). - /// - /// In other words, this is the corner radius of the _widget rect_. - #[inline] - #[deprecated = "Renamed to `corner_radius`"] - pub fn rounding(self, corner_radius: impl Into) -> Self { - self.corner_radius(corner_radius) - } - /// Margin outside the painted frame. /// /// Similar to what is called `margin` in CSS. diff --git a/crates/egui/src/containers/menu.rs b/crates/egui/src/containers/menu.rs index cfdaac827..d4c95b298 100644 --- a/crates/egui/src/containers/menu.rs +++ b/crates/egui/src/containers/menu.rs @@ -197,7 +197,7 @@ impl MenuState { /// Horizontal menu bar where you can add [`MenuButton`]s. /// -/// The menu bar goes well in a [`crate::TopBottomPanel::top`], +/// The menu bar goes well in a [`crate::Panel::top`], /// but can also be placed in a [`crate::Window`]. /// In the latter case you may want to wrap it in [`Frame`]. /// @@ -219,9 +219,6 @@ pub struct MenuBar { style: StyleModifier, } -#[deprecated = "Renamed to `egui::MenuBar`"] -pub type Bar = MenuBar; - impl Default for MenuBar { fn default() -> Self { Self { diff --git a/crates/egui/src/containers/mod.rs b/crates/egui/src/containers/mod.rs index a8f3306e9..d828ce0f8 100644 --- a/crates/egui/src/containers/mod.rs +++ b/crates/egui/src/containers/mod.rs @@ -9,7 +9,6 @@ mod combo_box; pub mod frame; pub mod menu; pub mod modal; -pub mod old_popup; pub mod panel; mod popup; pub(crate) mod resize; @@ -26,7 +25,6 @@ pub use { combo_box::*, frame::Frame, modal::{Modal, ModalResponse}, - old_popup::*, panel::*, popup::*, resize::Resize, diff --git a/crates/egui/src/containers/old_popup.rs b/crates/egui/src/containers/old_popup.rs deleted file mode 100644 index f8e7cc900..000000000 --- a/crates/egui/src/containers/old_popup.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Old and deprecated API for popups. Use [`Popup`] instead. -#![expect(deprecated)] - -use crate::containers::tooltip::Tooltip; -use crate::{ - Align, Context, Id, LayerId, Layout, Popup, PopupAnchor, PopupCloseBehavior, Pos2, Rect, - Response, Ui, Widget as _, WidgetText, -}; -use emath::RectAlign; -// ---------------------------------------------------------------------------- - -/// Show a tooltip at the current pointer position (if any). -/// -/// Most of the time it is easier to use [`Response::on_hover_ui`]. -/// -/// See also [`show_tooltip_text`]. -/// -/// Returns `None` if the tooltip could not be placed. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// # #[expect(deprecated)] -/// if ui.ui_contains_pointer() { -/// egui::show_tooltip(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { -/// ui.label("Helpful text"); -/// }); -/// } -/// # }); -/// ``` -#[deprecated = "Use `egui::Tooltip` instead"] -pub fn show_tooltip( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - show_tooltip_at_pointer(ctx, parent_layer, widget_id, add_contents) -} - -/// Show a tooltip at the current pointer position (if any). -/// -/// Most of the time it is easier to use [`Response::on_hover_ui`]. -/// -/// See also [`show_tooltip_text`]. -/// -/// Returns `None` if the tooltip could not be placed. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// if ui.ui_contains_pointer() { -/// egui::show_tooltip_at_pointer(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), |ui| { -/// ui.label("Helpful text"); -/// }); -/// } -/// # }); -/// ``` -#[deprecated = "Use `egui::Tooltip` instead"] -pub fn show_tooltip_at_pointer( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - Tooltip::always_open(ctx.clone(), parent_layer, widget_id, PopupAnchor::Pointer) - .gap(12.0) - .show(add_contents) - .map(|response| response.inner) -} - -/// Show a tooltip under the given area. -/// -/// If the tooltip does not fit under the area, it tries to place it above it instead. -#[deprecated = "Use `egui::Tooltip` instead"] -pub fn show_tooltip_for( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - widget_rect: &Rect, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - Tooltip::always_open(ctx.clone(), parent_layer, widget_id, *widget_rect) - .show(add_contents) - .map(|response| response.inner) -} - -/// Show a tooltip at the given position. -/// -/// Returns `None` if the tooltip could not be placed. -#[deprecated = "Use `egui::Tooltip` instead"] -pub fn show_tooltip_at( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - suggested_position: Pos2, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - Tooltip::always_open(ctx.clone(), parent_layer, widget_id, suggested_position) - .show(add_contents) - .map(|response| response.inner) -} - -/// Show some text at the current pointer position (if any). -/// -/// Most of the time it is easier to use [`Response::on_hover_text`]. -/// -/// See also [`show_tooltip`]. -/// -/// Returns `None` if the tooltip could not be placed. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// if ui.ui_contains_pointer() { -/// egui::show_tooltip_text(ui.ctx(), ui.layer_id(), egui::Id::new("my_tooltip"), "Helpful text"); -/// } -/// # }); -/// ``` -#[deprecated = "Use `egui::Tooltip` instead"] -pub fn show_tooltip_text( - ctx: &Context, - parent_layer: LayerId, - widget_id: Id, - text: impl Into, -) -> Option<()> { - show_tooltip(ctx, parent_layer, widget_id, |ui| { - crate::widgets::Label::new(text).ui(ui); - }) -} - -/// Was this tooltip visible last frame? -#[deprecated = "Use `Tooltip::was_tooltip_open_last_frame` instead"] -pub fn was_tooltip_open_last_frame(ctx: &Context, widget_id: Id) -> bool { - Tooltip::was_tooltip_open_last_frame(ctx, widget_id) -} - -/// Indicate whether a popup will be shown above or below the box. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum AboveOrBelow { - Above, - Below, -} - -/// Helper for [`popup_above_or_below_widget`]. -#[deprecated = "Use `egui::Popup` instead"] -pub fn popup_below_widget( - ui: &Ui, - popup_id: Id, - widget_response: &Response, - close_behavior: PopupCloseBehavior, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - popup_above_or_below_widget( - ui, - popup_id, - widget_response, - AboveOrBelow::Below, - close_behavior, - add_contents, - ) -} - -/// Shows a popup above or below another widget. -/// -/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. -/// -/// The opened popup will have a minimum width matching its parent. -/// -/// You must open the popup with [`crate::Memory::open_popup`] or [`crate::Memory::toggle_popup`]. -/// -/// Returns `None` if the popup is not open. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// let response = ui.button("Open popup"); -/// let popup_id = ui.make_persistent_id("my_unique_id"); -/// if response.clicked() { -/// ui.memory_mut(|mem| mem.toggle_popup(popup_id)); -/// } -/// let below = egui::AboveOrBelow::Below; -/// let close_on_click_outside = egui::PopupCloseBehavior::CloseOnClickOutside; -/// # #[expect(deprecated)] -/// egui::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| { -/// ui.set_min_width(200.0); // if you want to control the size -/// ui.label("Some more info, or things you can select:"); -/// ui.label("…"); -/// }); -/// # }); -/// ``` -#[deprecated = "Use `egui::Popup` instead"] -pub fn popup_above_or_below_widget( - _parent_ui: &Ui, - popup_id: Id, - widget_response: &Response, - above_or_below: AboveOrBelow, - close_behavior: PopupCloseBehavior, - add_contents: impl FnOnce(&mut Ui) -> R, -) -> Option { - let response = Popup::from_response(widget_response) - .layout(Layout::top_down_justified(Align::LEFT)) - .open_memory(None) - .close_behavior(close_behavior) - .id(popup_id) - .align(match above_or_below { - AboveOrBelow::Above => RectAlign::TOP_START, - AboveOrBelow::Below => RectAlign::BOTTOM_START, - }) - .width(widget_response.rect.width()) - .show(|ui| { - ui.set_min_width(ui.available_width()); - add_contents(ui) - })?; - Some(response.inner) -} diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 4a83ce8d1..d75a90bf4 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -18,9 +18,8 @@ use emath::{GuiRounding as _, Pos2}; use crate::{ - Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, NumExt as _, Rangef, - Rect, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, WidgetInfo, WidgetType, lerp, - vec2, + Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect, Sense, + Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, vec2, }; fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 { @@ -451,59 +450,6 @@ impl Panel { } } -// Deprecated -impl Panel { - #[deprecated = "Renamed default_size"] - pub fn default_width(self, default_size: f32) -> Self { - self.default_size(default_size) - } - - #[deprecated = "Renamed min_size"] - pub fn min_width(self, min_size: f32) -> Self { - self.min_size(min_size) - } - - #[deprecated = "Renamed max_size"] - pub fn max_width(self, max_size: f32) -> Self { - self.max_size(max_size) - } - - #[deprecated = "Renamed size_range"] - pub fn width_range(self, size_range: impl Into) -> Self { - self.size_range(size_range) - } - - #[deprecated = "Renamed exact_size"] - pub fn exact_width(self, size: f32) -> Self { - self.exact_size(size) - } - - #[deprecated = "Renamed default_size"] - pub fn default_height(self, default_size: f32) -> Self { - self.default_size(default_size) - } - - #[deprecated = "Renamed min_size"] - pub fn min_height(self, min_size: f32) -> Self { - self.min_size(min_size) - } - - #[deprecated = "Renamed max_size"] - pub fn max_height(self, max_size: f32) -> Self { - self.max_size(max_size) - } - - #[deprecated = "Renamed size_range"] - pub fn height_range(self, size_range: impl Into) -> Self { - self.size_range(size_range) - } - - #[deprecated = "Renamed exact_size"] - pub fn exact_height(self, size: f32) -> Self { - self.exact_size(size) - } -} - // Public showing methods impl Panel { /// Show the panel inside a [`Ui`]. @@ -515,41 +461,6 @@ impl Panel { self.show_inside_dyn(ui, Box::new(add_contents)) } - /// Show the panel at the top level. - #[deprecated = "Use show_inside() instead"] - pub fn show( - self, - ctx: &Context, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> InnerResponse { - self.show_dyn(ctx, Box::new(add_contents)) - } - - /// Show the panel if `is_expanded` is `true`, - /// otherwise don't show it, but with a nice animation between collapsed and expanded. - #[deprecated = "Use show_animated_inside() instead"] - pub fn show_animated( - self, - ctx: &Context, - is_expanded: bool, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> Option> { - #![expect(deprecated)] - - let how_expanded = animate_expansion(ctx, self.id.with("animation"), is_expanded); - - let animated_panel = self.get_animated_panel(ctx, is_expanded)?; - - if how_expanded < 1.0 { - // Show a fake panel in this in-between animation state: - animated_panel.show(ctx, |_ui| {}); - None - } else { - // Show the real panel: - Some(animated_panel.show(ctx, add_contents)) - } - } - /// Show the panel if `is_expanded` is `true`, /// otherwise don't show it, but with a nice animation between collapsed and expanded. pub fn show_animated_inside( @@ -577,34 +488,6 @@ impl Panel { } } - /// Show either a collapsed or a expanded panel, with a nice animation between. - #[deprecated = "Use show_animated_between_inside() instead"] - pub fn show_animated_between( - ctx: &Context, - is_expanded: bool, - collapsed_panel: Self, - expanded_panel: Self, - add_contents: impl FnOnce(&mut Ui, f32) -> R, - ) -> Option> { - #![expect(deprecated)] - - let how_expanded = animate_expansion(ctx, expanded_panel.id.with("animation"), is_expanded); - - // Get either the fake or the real panel to animate - let animated_between_panel = - Self::get_animated_between_panel(ctx, is_expanded, collapsed_panel, expanded_panel); - - if 0.0 == how_expanded { - Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded))) - } else if how_expanded < 1.0 { - // Show animation: - animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded)); - None - } else { - Some(animated_between_panel.show(ctx, |ui| add_contents(ui, how_expanded))) - } - } - /// Show either a collapsed or a expanded panel, with a nice animation between. pub fn show_animated_between_inside( ui: &mut Ui, @@ -756,59 +639,6 @@ impl Panel { inner_response } - /// Show the panel at the top level. - fn show_dyn<'c, R>( - self, - ctx: &Context, - add_contents: Box R + 'c>, - ) -> InnerResponse { - #![expect(deprecated)] - - let side = self.side; - let available_rect = ctx.available_rect(); - let mut panel_ui = Ui::new( - ctx.clone(), - self.id, - UiBuilder::new() - .layer_id(LayerId::background()) - .max_rect(available_rect), - ); - panel_ui.set_clip_rect(ctx.content_rect()); - panel_ui - .response() - .widget_info(|| WidgetInfo::new(WidgetType::Panel)); - - let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); - let rect = inner_response.response.rect; - - match side { - PanelSide::Vertical(side) => match side { - VerticalSide::Left => ctx.pass_state_mut(|state| { - state.allocate_left_panel(Rect::from_min_max(available_rect.min, rect.max)); - }), - VerticalSide::Right => ctx.pass_state_mut(|state| { - state.allocate_right_panel(Rect::from_min_max(rect.min, available_rect.max)); - }), - }, - PanelSide::Horizontal(side) => match side { - HorizontalSide::Top => { - ctx.pass_state_mut(|state| { - state.allocate_top_panel(Rect::from_min_max(available_rect.min, rect.max)); - }); - } - HorizontalSide::Bottom => { - ctx.pass_state_mut(|state| { - state.allocate_bottom_panel(Rect::from_min_max( - rect.min, - available_rect.max, - )); - }); - } - }, - } - inner_response - } - fn prepare_resizable_panel(&self, panel_sizer: &mut PanelSizer<'_>, ui: &Ui) { let resize_id = self.id.with("__resize"); let resize_response = ui.ctx().read_response(resize_id); @@ -1044,61 +874,9 @@ impl CentralPanel { response } - - /// Show the panel at the top level. - #[deprecated = "Use show_inside() instead"] - pub fn show( - self, - ctx: &Context, - add_contents: impl FnOnce(&mut Ui) -> R, - ) -> InnerResponse { - self.show_dyn(ctx, Box::new(add_contents)) - } - - /// Show the panel at the top level. - fn show_dyn<'c, R>( - self, - ctx: &Context, - add_contents: Box R + 'c>, - ) -> InnerResponse { - #![expect(deprecated)] - - let id = Id::new((ctx.viewport_id(), "central_panel")); - - let mut panel_ui = Ui::new( - ctx.clone(), - id, - UiBuilder::new() - .layer_id(LayerId::background()) - .max_rect(ctx.available_rect()), - ); - panel_ui.set_clip_rect(ctx.content_rect()); - - if false { - // TODO(emilk): @lucasmerlin shouldn't we enable this? - panel_ui - .response() - .widget_info(|| WidgetInfo::new(WidgetType::Panel)); - } - - let inner_response = self.show_inside_dyn(&mut panel_ui, add_contents); - - // Only inform ctx about what we actually used, so we can shrink the native window to fit. - ctx.pass_state_mut(|state| state.allocate_central_panel(inner_response.response.rect)); - - inner_response - } } fn clamp_to_range(x: f32, range: Rangef) -> f32 { let range = range.as_positive(); x.clamp(range.min, range.max) } - -// ---------------------------------------------------------------------------- - -#[deprecated = "Use Panel::left or Panel::right instead"] -pub type SidePanel = super::Panel; - -#[deprecated = "Use Panel::top or Panel::bottom instead"] -pub type TopBottomPanel = super::Panel; diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 0fb2a9f2a..cf78a6650 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] // This is a new, safe wrapper around the old `Memory::popup` API. - use std::iter::once; use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2}; @@ -87,7 +85,7 @@ pub enum PopupCloseBehavior { /// but in the popup's body CloseOnClickOutside, - /// Clicks will be ignored. Popup might be closed manually by calling [`crate::Memory::close_all_popups`] + /// Clicks will be ignored. Popup might be closed manually by calling [`Popup::close_all`] /// or by pressing the escape button IgnoreClicks, } @@ -666,10 +664,6 @@ impl Popup<'_> { } /// Open the given popup and close all others. - /// - /// If you are NOT using [`Popup::show`], you must - /// also call [`crate::Memory::keep_popup_open`] as long as - /// you're showing the popup. pub fn open_id(ctx: &Context, popup_id: Id) { ctx.memory_mut(|mem| mem.open_popup(popup_id)); } diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 7ff943b3f..8dcce6b20 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -69,13 +69,6 @@ impl Resize { self } - /// A source for the unique [`Id`], e.g. `.id_source("second_resize_area")` or `.id_source(loop_index)`. - #[inline] - #[deprecated = "Renamed id_salt"] - pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self { - self.id_salt(id_salt) - } - /// A source for the unique [`Id`], e.g. `.id_salt("second_resize_area")` or `.id_salt(loop_index)`. #[inline] pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self { diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index b99bcf5da..c0c29a9ab 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -423,13 +423,6 @@ impl ScrollArea { self } - /// A source for the unique [`Id`], e.g. `.id_source("second_scroll_area")` or `.id_source(loop_index)`. - #[inline] - #[deprecated = "Renamed id_salt"] - pub fn id_source(self, id_salt: impl std::hash::Hash) -> Self { - self.id_salt(id_salt) - } - /// A source for the unique [`Id`], e.g. `.id_salt("second_scroll_area")` or `.id_salt(loop_index)`. #[inline] pub fn id_salt(mut self, id_salt: impl std::hash::Hash) -> Self { @@ -530,32 +523,6 @@ impl ScrollArea { /// This can be used, for example, to optionally freeze scrolling while the user /// is typing text in a [`crate::TextEdit`] widget contained within the scroll area. /// - /// This controls both scrolling directions. - #[deprecated = "Use `ScrollArea::scroll_source()"] - #[inline] - pub fn enable_scrolling(mut self, enable: bool) -> Self { - self.scroll_source = if enable { - ScrollSource::ALL - } else { - ScrollSource::NONE - }; - self - } - - /// Can the user drag the scroll area to scroll? - /// - /// This is useful for touch screens. - /// - /// If `true`, the [`ScrollArea`] will sense drags. - /// - /// Default: `true`. - #[deprecated = "Use `ScrollArea::scroll_source()"] - #[inline] - pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { - self.scroll_source.drag = drag_to_scroll; - self - } - /// What sources does the [`ScrollArea`] use for scrolling the contents. #[inline] pub fn scroll_source(mut self, scroll_source: ScrollSource) -> Self { diff --git a/crates/egui/src/containers/tooltip.rs b/crates/egui/src/containers/tooltip.rs index 78c5a726b..22c319569 100644 --- a/crates/egui/src/containers/tooltip.rs +++ b/crates/egui/src/containers/tooltip.rs @@ -16,24 +16,6 @@ pub struct Tooltip<'a> { } impl Tooltip<'_> { - /// Show a tooltip that is always open. - #[deprecated = "Use `Tooltip::always_open` instead."] - pub fn new( - parent_widget: Id, - ctx: Context, - anchor: impl Into, - parent_layer: LayerId, - ) -> Self { - Self { - popup: Popup::new(parent_widget, ctx, anchor.into(), parent_layer) - .kind(PopupKind::Tooltip) - .gap(4.0) - .sense(Sense::hover()), - parent_layer, - parent_widget, - } - } - /// Show a tooltip that is always open. pub fn always_open( ctx: Context, diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 5ade37014..9f25d6131 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -262,7 +262,7 @@ impl<'open> Window<'open> { self } - /// Constrains this window to [`Context::screen_rect`]. + /// Constrains this window to [`Context::content_rect`]. /// /// To change the area to constrain to, use [`Self::constrain_to`]. /// @@ -275,7 +275,7 @@ impl<'open> Window<'open> { /// Constrain the movement of the window to the given rectangle. /// - /// For instance: `.constrain_to(ctx.screen_rect())`. + /// For instance: `.constrain_to(ctx.content_rect())`. #[inline] pub fn constrain_to(mut self, constrain_rect: Rect) -> Self { self.area = self.area.constrain_to(constrain_rect); @@ -427,7 +427,7 @@ impl<'open> Window<'open> { /// Enable/disable scrolling on the window by dragging with the pointer. `true` by default. /// - /// See [`ScrollArea::drag_to_scroll`] for more. + /// See [`ScrollArea::scroll_source`] for more. #[inline] pub fn drag_to_scroll(mut self, drag_to_scroll: bool) -> Self { self.scroll = self.scroll.scroll_source(ScrollSource { diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 433446648..d348517ef 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -300,7 +300,7 @@ impl RepaintCause { struct ViewportRepaintInfo { /// Monotonically increasing counter. /// - /// Incremented at the end of [`Context::run`]. + /// Incremented at the end of [`Context::run_ui`]. /// This can be smaller than [`Self::cumulative_pass_nr`], /// but never larger. cumulative_frame_nr: u64, @@ -463,7 +463,7 @@ impl ContextImpl { let content_rect = viewport.input.content_rect(); - viewport.this_pass.begin_pass(content_rect); + viewport.this_pass.begin_pass(); { let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); @@ -697,8 +697,8 @@ impl ContextImpl { /// // Game loop: /// loop { /// let raw_input = egui::RawInput::default(); -/// let full_output = ctx.run(raw_input, |ctx| { -/// egui::CentralPanel::default().show(&ctx, |ui| { +/// let full_output = ctx.run_ui(raw_input, |ui| { +/// egui::CentralPanel::default().show_inside(ui, |ui| { /// ui.label("Hello world!"); /// if ui.button("Click me").clicked() { /// // take some action here @@ -780,9 +780,6 @@ impl Context { /// }); /// // handle full_output /// ``` - /// - /// ## See also - /// * [`Self::run`] #[must_use] pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput { self.run_ui_dyn(new_input, &mut run_ui) @@ -791,14 +788,13 @@ impl Context { #[must_use] fn run_ui_dyn(&self, new_input: RawInput, run_ui: &mut dyn FnMut(&mut Ui)) -> FullOutput { let plugins = self.read(|ctx| ctx.plugins.ordered_plugins()); - #[expect(deprecated)] - self.run(new_input, |ctx| { + self.run_dyn(new_input, &mut |ctx| { let mut root_ui = Ui::new( ctx.clone(), Id::new((ctx.viewport_id(), "__top_ui")), UiBuilder::new() .layer_id(LayerId::background()) - .max_rect(ctx.available_rect()), + .max_rect(ctx.viewport_rect()), ); { @@ -814,38 +810,6 @@ impl Context { }) } - /// Run the ui code for one frame. - /// - /// At most [`Options::max_passes`] calls will be issued to `run_ui`, - /// and only on the rare occasion that [`Context::request_discard`] is called. - /// Usually, it `run_ui` will only be called once. - /// - /// Put your widgets into a [`crate::Panel`], [`crate::CentralPanel`], [`crate::Window`] or [`crate::Area`]. - /// - /// Instead of calling `run`, you can alternatively use [`Self::begin_pass`] and [`Context::end_pass`]. - /// - /// ``` - /// // One egui context that you keep reusing: - /// let mut ctx = egui::Context::default(); - /// - /// // Each frame: - /// let input = egui::RawInput::default(); - /// let full_output = ctx.run(input, |ctx| { - /// egui::CentralPanel::default().show(&ctx, |ui| { - /// ui.label("Hello egui!"); - /// }); - /// }); - /// // handle full_output - /// ``` - /// - /// ## See also - /// * [`Self::run_ui`] - #[must_use] - #[deprecated = "Call run_ui instead"] - pub fn run(&self, new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { - self.run_dyn(new_input, &mut run_ui) - } - #[must_use] fn run_dyn(&self, mut new_input: RawInput, run_ui: &mut dyn FnMut(&Self)) -> FullOutput { profiling::function_scope!(); @@ -915,10 +879,10 @@ impl Context { output } - /// An alternative to calling [`Self::run`]. + /// An alternative to calling [`Self::run_ui`]. /// - /// It is usually better to use [`Self::run`], because - /// `run` supports multi-pass layout using [`Self::request_discard`]. + /// It is usually better to use [`Self::run_ui`], because + /// `run_ui` supports multi-pass layout using [`Self::request_discard`]. /// /// ``` /// // One egui context that you keep reusing: @@ -928,9 +892,7 @@ impl Context { /// let input = egui::RawInput::default(); /// ctx.begin_pass(input); /// - /// egui::CentralPanel::default().show(&ctx, |ui| { - /// ui.label("Hello egui!"); - /// }); + /// // … add panels and windows here … /// /// let full_output = ctx.end_pass(); /// // handle full_output @@ -943,12 +905,6 @@ impl Context { self.write(|ctx| ctx.begin_pass(new_input)); } - - /// See [`Self::begin_pass`]. - #[deprecated = "Renamed begin_pass"] - pub fn begin_frame(&self, new_input: RawInput) { - self.begin_pass(new_input); - } } /// ## Borrows parts of [`Context`] @@ -1049,7 +1005,7 @@ impl Context { /// Read-only access to [`PassState`]. /// - /// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]). + /// This is only valid during the call to [`Self::run_ui`] (between [`Self::begin_pass`] and [`Self::end_pass`]). #[inline] pub(crate) fn pass_state(&self, reader: impl FnOnce(&PassState) -> R) -> R { self.write(move |ctx| reader(&ctx.viewport().this_pass)) @@ -1057,7 +1013,7 @@ impl Context { /// Read-write access to [`PassState`]. /// - /// This is only valid during the call to [`Self::run`] (between [`Self::begin_pass`] and [`Self::end_pass`]). + /// This is only valid during the call to [`Self::run_ui`] (between [`Self::begin_pass`] and [`Self::end_pass`]). #[inline] pub(crate) fn pass_state_mut(&self, writer: impl FnOnce(&mut PassState) -> R) -> R { self.write(move |ctx| writer(&mut ctx.viewport().this_pass)) @@ -1073,7 +1029,7 @@ impl Context { /// Read-only access to [`Fonts`]. /// - /// Not valid until first call to [`Context::run()`]. + /// Not valid until first call to [`Context::run_ui()`]. /// That's because since we don't know the proper `pixels_per_point` until then. #[inline] pub fn fonts(&self, reader: impl FnOnce(&FontsView<'_>) -> R) -> R { @@ -1090,7 +1046,7 @@ impl Context { /// Read-write access to [`Fonts`]. /// - /// Not valid until first call to [`Context::run()`]. + /// Not valid until first call to [`Context::run_ui()`]. /// That's because since we don't know the proper `pixels_per_point` until then. #[inline] pub fn fonts_mut(&self, reader: impl FnOnce(&mut FontsView<'_>) -> R) -> R { @@ -1666,7 +1622,7 @@ impl Context { /// The total number of completed frames. /// - /// Starts at zero, and is incremented once at the end of each call to [`Self::run`]. + /// Starts at zero, and is incremented once at the end of each call to [`Self::run_ui`]. /// /// This is always smaller or equal to [`Self::cumulative_pass_nr`]. pub fn cumulative_frame_nr(&self) -> u64 { @@ -1675,7 +1631,7 @@ impl Context { /// The total number of completed frames. /// - /// Starts at zero, and is incremented once at the end of each call to [`Self::run`]. + /// Starts at zero, and is incremented once at the end of each call to [`Self::run_ui`]. /// /// This is always smaller or equal to [`Self::cumulative_pass_nr_for`]. pub fn cumulative_frame_nr_for(&self, id: ViewportId) -> u64 { @@ -1695,7 +1651,7 @@ impl Context { /// The total number of completed passes (usually there is one pass per rendered frame). /// - /// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once). + /// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once). /// /// If you instead want to know which pass index this is within the current frame, /// use [`Self::current_pass_index`]. @@ -1705,7 +1661,7 @@ impl Context { /// The total number of completed passes (usually there is one pass per rendered frame). /// - /// Starts at zero, and is incremented for each completed pass inside of [`Self::run`] (usually once). + /// Starts at zero, and is incremented for each completed pass inside of [`Self::run_ui`] (usually once). pub fn cumulative_pass_nr_for(&self, id: ViewportId) -> u64 { self.read(|ctx| { ctx.viewports @@ -2080,7 +2036,7 @@ impl Context { self.options(|opt| opt.theme()) } - /// The [`Theme`] used to select between dark and light [`Self::style`] + /// The [`Theme`] used to select between dark and light [`Self::global_style`] /// as the active style used by all subsequent popups, menus, etc. /// /// Example: @@ -2097,12 +2053,6 @@ impl Context { self.options(|opt| Arc::clone(opt.style())) } - /// The currently active [`Style`] used by all subsequent popups, menus, etc. - #[deprecated = "Renamed to `global_style` to avoid confusion with `ui.style()`"] - pub fn style(&self) -> Arc