From d99b665ec0bf090e8f6fa431a7c90d4c0f2c60e9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 28 Jul 2026 07:24:18 -0700 Subject: [PATCH 01/49] Web: anchor the text agent to the canvas (#8297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Follow-up to #8296, part of * [x] I have followed the instructions in the PR template Stacked on #8296 (base branch), kept as a separate PR so it can be reverted independently. The hidden text-agent `` is now inserted as a *sibling of the canvas* (instead of appended to `document.body`) and positioned with `offsetLeft`/`offsetTop` instead of `getBoundingClientRect`. Since the input and the canvas share the same containing block, the input stays anchored to the canvas top-left corner no matter how the page is scrolled or how the canvas is embedded. Consequences: * Fixes the IME popup position when the host page is scrolled — `move_to` previously wrote *viewport* coordinates from `getBoundingClientRect` into document-absolute `left`/`top`. * Subsumes the mobile Safari virtual-keyboard workaround (it replaced the flapping `getBoundingClientRect` y with `offsetTop`, which is now used everywhere), so `is_mobile_safari()` is removed. * Removes the special-casing of document vs shadow DOM roots — sibling insertion works uniformly in both. * The input is `position: absolute`, so it does not participate in flex/grid layout of the canvas' parent and causes no layout shift. Caveat: host CSS selectors like `div > canvas:only-child` would no longer match. Verified with `cargo clippy -p eframe --target wasm32-unknown-unknown --all-features` and `cargo fmt --all`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 --- crates/eframe/src/web/text_agent.rs | 69 +++++++++++++---------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index 73461e9e4..d8d6b672e 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -4,7 +4,6 @@ use std::cell::Cell; use wasm_bindgen::prelude::*; -use web_sys::Document; use super::{AppRunner, WebRunner}; @@ -28,10 +27,9 @@ impl TextAgent { input.set_type("text"); input.set_attribute("autocapitalize", "off")?; - // Hide the element, and park it over the canvas + // Hide the element, and park it over the top-left corner of the canvas // so that focusing it can never scroll some other part // of the page into view. - let canvas_rect = super::canvas_content_rect(canvas); let style = input.style(); style.set_property("background-color", "transparent")?; style.set_property("border", "none")?; @@ -40,21 +38,22 @@ impl TextAgent { style.set_property("height", "1px")?; style.set_property("caret-color", "transparent")?; style.set_property("position", "absolute")?; - style.set_property("top", &format!("{}px", canvas_rect.min.y))?; - style.set_property("left", &format!("{}px", canvas_rect.min.x))?; + style.set_property("top", &format!("{}px", canvas.offset_top()))?; + style.set_property("left", &format!("{}px", canvas.offset_left()))?; // Prevent auto-zoom on mobile browsers (requires at least 16px). style.set_property("font-size", "16px")?; - let root = canvas.get_root_node(); - if root.has_type::() { - // root object is a document, append to its body - root.dyn_into::()? - .body() - .unwrap() - .append_child(&input)?; - } else { - // append input into root directly - root.append_child(&input)?; + // Insert the input as a sibling of the canvas, so that its + // `position: absolute` resolves against the same containing block + // as the canvas' `offset_top`/`offset_left`. + // This anchors the input to the canvas regardless of how the page + // is scrolled or how the canvas is embedded, and also works when + // the canvas is inside a shadow DOM. + if let Some(parent) = canvas.parent_node() { + parent.insert_before(&input, canvas.next_sibling().as_ref())?; + } else if let Some(body) = document.body() { + log::warn!("Canvas has no parent element - appending text agent to document body"); + body.append_child(&input)?; } // Focus the app on startup, without scrolling the page. @@ -187,29 +186,34 @@ impl TextAgent { // composition. } - let mut canvas_rect = super::canvas_content_rect(canvas); - // Fix for safari with virtual keyboard flapping position - if is_mobile_safari() { - canvas_rect.min.y = canvas.offset_top() as f32; - } - let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2()); - let style = self.input.style(); let native_ppp = super::native_pixels_per_point(); + // The input is a sibling of the canvas (see `attach`), so we position + // it relative to the same containing block using the canvas offset. + // Unlike `get_bounding_client_rect`, the offset is unaffected by page + // scrolling, and doesn't flap when the virtual keyboard is shown on + // mobile Safari. + // Clamp the input position within the canvas width to prevent unwanted horizontal scrolling. let logical_canvas_width = canvas.width() as f32 / native_ppp; - let visible_x = cursor_rect.center().x * zoom_factor; + let visible_x = ime.cursor_rect.center().x * zoom_factor; let clamped_x = visible_x.clamp(0.0, logical_canvas_width); // Clamp the input position within the canvas height to prevent unwanted vertical scrolling. let logical_canvas_height = canvas.height() as f32 / native_ppp; - let visible_y = cursor_rect.center().y * zoom_factor; + let visible_y = ime.cursor_rect.center().y * zoom_factor; let clamped_y = visible_y.clamp(0.0, logical_canvas_height); // This is where the IME input will point to: - style.set_property("left", &format!("{clamped_x}px"))?; - style.set_property("top", &format!("{clamped_y}px"))?; + style.set_property( + "left", + &format!("{}px", canvas.offset_left() as f32 + clamped_x), + )?; + style.set_property( + "top", + &format!("{}px", canvas.offset_top() as f32 + clamped_y), + )?; Ok(()) } @@ -256,16 +260,3 @@ impl Drop for TextAgent { self.input.remove(); } } - -/// Returns `true` if the app is likely running on a mobile device on navigator Safari. -fn is_mobile_safari() -> bool { - (|| { - let user_agent = web_sys::window()?.navigator().user_agent().ok()?; - let is_ios = user_agent.contains("iPhone") - || user_agent.contains("iPad") - || user_agent.contains("iPod"); - let is_safari = user_agent.contains("Safari"); - Some(is_ios && is_safari) - })() - .unwrap_or(false) -} From 6268c84d8a46092adea32826aa22089f55ae4986 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 29 Jul 2026 18:16:48 +0200 Subject: [PATCH 02/49] Prevent accidentally dropping `TexturesDelta` (#8356) We had a ton of issues around `TexturesDelta` that weren't properly applied because we early-out of some function: * https://github.com/emilk/egui/pull/8313 * https://github.com/emilk/egui/pull/8250 * https://github.com/emilk/egui/pull/8279 This PR changes texture updates, so that we always store them after taking them out of `FullOutput` and keep the delta around until it's actually applied (by passing &mut refs and draining instead of iterating). So even if we add a new early return somewhere, that can't break texture updates. It also optimizes `TexturesDelta::append` by dropping any previous deltas if there's a new `whole` delta or a `free`. It also adds a debug assert that any `TexturesDelta` is empty when dropped, as an additional safeguard in case the bug sneaks back in. --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/eframe/src/native/glow_integration.rs | 73 +++++++++----- crates/eframe/src/native/wgpu_integration.rs | 35 ++++++- crates/eframe/src/web/app_runner.rs | 3 +- crates/eframe/src/web/web_painter.rs | 2 +- crates/eframe/src/web/web_painter_glow.rs | 12 ++- crates/eframe/src/web/web_painter_wgpu.rs | 24 +++-- crates/egui-wgpu/src/winit.rs | 34 ++++--- crates/egui/src/context.rs | 8 ++ crates/egui/src/data/output.rs | 8 +- crates/egui/src/lib.rs | 6 +- .../text_selection/label_text_selection.rs | 6 +- crates/egui/src/viewport.rs | 3 +- crates/egui_demo_lib/benches/benchmark.rs | 19 ++-- crates/egui_demo_lib/src/lib.rs | 6 +- crates/egui_glow/src/painter.rs | 12 ++- crates/egui_glow/src/winit.rs | 10 +- crates/egui_kittest/src/lib.rs | 4 +- crates/egui_kittest/src/renderer.rs | 33 +++++-- crates/egui_kittest/src/wgpu.rs | 24 +++-- crates/egui_kittest/tests/accesskit.rs | 3 +- crates/epaint/src/textures.rs | 96 ++++++++++++++----- 21 files changed, 291 insertions(+), 130 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index cb53609ee..451aa0d82 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -31,16 +31,17 @@ use egui::{ }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; - -use crate::{ - App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized}, -}; +use log::warn; use super::{ epi_integration, event_loop_context, winit_integration::{EventResult, UserEvent, WinitApp, create_egui_context}, }; +use crate::epaint::textures::TexturesDelta; +use crate::{ + App, AppCreator, CreationContext, NativeOptions, Result, Storage, + native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized}, +}; // ---------------------------------------------------------------------------- // Types: @@ -73,6 +74,16 @@ struct GlowWinitRunning<'app> { // NOTE: one painter shared by all viewports. painter: Rc>, + + /// Any not yet applied deltas for this app. + pending_deltas: TexturesDelta, +} + +impl Drop for GlowWinitRunning<'_> { + fn drop(&mut self) { + // Avoid debug panic when dropping unapplied deltas on teardown + self.pending_deltas.clear(); + } } /// This struct will contain both persistent and temporary glutin state. @@ -114,6 +125,9 @@ struct Viewport { info: ViewportInfo, actions_requested: Vec, + /// Any not yet applied deltas for this viewport. + pending_delta: TexturesDelta, + /// The user-callback that shows the ui. /// None for immediate viewports. viewport_ui_cb: Option>, @@ -125,6 +139,13 @@ struct Viewport { egui_winit: Option, } +impl Drop for Viewport { + fn drop(&mut self) { + // Avoid debug panic when dropping unapplied deltas on teardown + self.pending_delta.clear(); + } +} + // ---------------------------------------------------------------------------- impl<'app> GlowWinitApp<'app> { @@ -353,6 +374,7 @@ impl<'app> GlowWinitApp<'app> { app, glutin, painter, + pending_deltas: Default::default(), })) } } @@ -653,6 +675,7 @@ impl GlowWinitRunning<'_> { app, glutin, painter, + pending_deltas, .. } = self; @@ -666,6 +689,7 @@ impl GlowWinitRunning<'_> { pixels_per_point, viewport_output, } = full_output; + pending_deltas.append(textures_delta); glutin.remove_viewports_not_in(&viewport_output); @@ -687,30 +711,28 @@ impl GlowWinitRunning<'_> { egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output); - // Upload textures even when not visible: the atlas dirty region is already - // consumed, so dropping the delta would desync the font texture. - let has_texture_updates = !textures_delta.set.is_empty() || !textures_delta.free.is_empty(); - if is_visible || has_texture_updates { - // We may need to switch contexts again, because of immediate viewports: - frame_timer.pause(); - change_gl_context(current_gl_context, not_current_gl_context, gl_surface); - frame_timer.resume(); - } - - for (id, image_delta) in &textures_delta.set { - painter.set_texture(*id, image_delta); - } - if is_visible { let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + { + // We may need to switch contexts again, because of immediate viewports: + frame_timer.pause(); + change_gl_context(current_gl_context, not_current_gl_context, gl_surface); + frame_timer.resume(); + } + let screen_size_in_pixels: [u32; 2] = window.inner_size().into(); if !clear_before_update { painter.clear(screen_size_in_pixels, clear_color); } - painter.paint_primitives(screen_size_in_pixels, pixels_per_point, &clipped_primitives); + painter.paint_and_update_textures( + screen_size_in_pixels, + pixels_per_point, + &clipped_primitives, + pending_deltas, + ); { for action in viewport.actions_requested.drain(..) { @@ -772,11 +794,6 @@ impl GlowWinitRunning<'_> { } } - // Free textures *after* painting, since they may still be used in the frame we just drew. - for id in &textures_delta.free { - painter.free_texture(*id); - } - glutin.handle_viewport_output(event_loop, &integration.egui_ctx, &viewport_output); integration.report_frame_time(frame_timer.total_time_sec()); // don't count auto-save time as part of regular frame time @@ -1120,6 +1137,7 @@ impl GlutinWindowContext { deferred_commands: vec![], info: viewport_info, actions_requested: Default::default(), + pending_delta: Default::default(), viewport_ui_cb: None, gl_surface: None, window: window.map(Arc::new), @@ -1436,6 +1454,7 @@ fn initialize_or_update_viewport( deferred_commands: vec![], info: Default::default(), actions_requested: Default::default(), + pending_delta: Default::default(), viewport_ui_cb, window: None, egui_winit: None, @@ -1584,8 +1603,10 @@ fn render_immediate_viewport( } = &mut *glutin; let Some(viewport) = viewports.get_mut(&viewport_id) else { + warn!("Viewport disappeared unexpectedly!"); return; }; + viewport.pending_delta.append(textures_delta); viewport.info.events.clear(); // they should have been processed @@ -1621,7 +1642,7 @@ fn render_immediate_viewport( screen_size_in_pixels, pixels_per_point, &clipped_primitives, - &textures_delta, + &mut viewport.pending_delta, ); { diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 918b8419f..e01b4d9a3 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -17,12 +17,13 @@ use winit::{ use ahash::HashMap; use egui::{ - DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, + DeferredViewportUiCallback, FullOutput, ImmediateViewport, OrderedViewportIdMap, TexturesDelta, ViewportBuilder, ViewportClass, ViewportId, ViewportIdPair, ViewportIdSet, ViewportInfo, ViewportOutput, }; #[cfg(feature = "accesskit")] use egui_winit::accesskit_winit; +use log::warn; use winit_integration::UserEvent; use crate::{ @@ -65,6 +66,15 @@ struct WgpuWinitRunning<'app> { /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. shared: Rc>, + + pending_deltas: TexturesDelta, +} + +impl Drop for WgpuWinitRunning<'_> { + fn drop(&mut self) { + // Avoid debug panic when dropping unapplied deltas on teardown + self.pending_deltas.clear(); + } } /// Everything needed by the immediate viewport renderer.\ @@ -91,6 +101,9 @@ pub struct Viewport { info: ViewportInfo, actions_requested: Vec, + /// Any not yet applied deltas for this viewport. + pending_delta: TexturesDelta, + /// `None` for sync viewports. viewport_ui_cb: Option>, @@ -102,6 +115,13 @@ pub struct Viewport { egui_winit: Option, } +impl Drop for Viewport { + fn drop(&mut self) { + // Avoid debug panic when dropping unapplied deltas on teardown + self.pending_delta.clear(); + } +} + // ---------------------------------------------------------------------------- impl<'app> WgpuWinitApp<'app> { @@ -328,6 +348,7 @@ impl<'app> WgpuWinitApp<'app> { viewport_ui_cb: None, window: Some(window), egui_winit: Some(egui_winit), + pending_delta: Default::default(), }, ); @@ -358,6 +379,7 @@ impl<'app> WgpuWinitApp<'app> { integration, app, shared, + pending_deltas: Default::default(), })) } } @@ -596,6 +618,7 @@ impl WgpuWinitRunning<'_> { app, integration, shared, + pending_deltas, } = self; let mut frame_timer = crate::stopwatch::Stopwatch::new(); @@ -699,6 +722,8 @@ impl WgpuWinitRunning<'_> { viewport_output, } = full_output; + pending_deltas.append(textures_delta); + remove_viewports_not_in(viewports, painter, viewport_from_window, &viewport_output); let Some(viewport) = viewports.get_mut(&viewport_id) else { @@ -735,7 +760,7 @@ impl WgpuWinitRunning<'_> { pixels_per_point, app.clear_color(&egui_ctx.global_style().visuals), &clipped_primitives, - &textures_delta, + pending_deltas, screenshot_commands, window, ); @@ -1125,8 +1150,11 @@ fn render_immediate_viewport( } = &mut *shared_mut; let Some(viewport) = viewports.get_mut(&ids.this) else { + warn!("Viewport disappeared unexpectedly!"); return; }; + viewport.pending_delta.append(textures_delta); + viewport.info.events.clear(); // they should have been processed let (Some(egui_winit), Some(window)) = (&mut viewport.egui_winit, &viewport.window) else { return; @@ -1149,7 +1177,7 @@ fn render_immediate_viewport( pixels_per_point, [0.0, 0.0, 0.0, 0.0], &clipped_primitives, - &textures_delta, + &mut viewport.pending_delta, vec![], window, ); @@ -1268,6 +1296,7 @@ fn initialize_or_update_viewport<'a>( viewport_ui_cb, window: None, egui_winit: None, + pending_delta: Default::default(), }) } diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index e259e4019..f913e9b6d 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -324,7 +324,6 @@ impl AppRunner { /// Paint the results of the last call to [`Self::logic`]. pub fn paint(&mut self) { - let textures_delta = std::mem::take(&mut self.textures_delta); let clipped_primitives = std::mem::take(&mut self.clipped_primitives); if let Some(clipped_primitives) = clipped_primitives { @@ -347,7 +346,7 @@ impl AppRunner { self.app.clear_color(&self.egui_ctx.global_style().visuals), &clipped_primitives, self.egui_ctx.pixels_per_point(), - &textures_delta, + &mut self.textures_delta, screenshot_commands, ) { log::error!("Failed to paint: {}", super::string_from_js_value(&err)); diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index fe751bf16..69f60e271 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -24,7 +24,7 @@ pub(crate) trait WebPainter { clear_color: [f32; 4], clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, - textures_delta: &egui::TexturesDelta, + textures_delta: &mut egui::TexturesDelta, capture: Vec, ) -> Result<(), JsValue>; diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index c9b846d50..04d48210e 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -61,13 +61,16 @@ impl WebPainter for WebPainterGlow { clear_color: [f32; 4], clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, - textures_delta: &egui::TexturesDelta, + textures_delta: &mut egui::TexturesDelta, capture: Vec, ) -> Result<(), JsValue> { let canvas_dimension = [self.canvas.width(), self.canvas.height()]; - for (id, image_delta) in &textures_delta.set { - self.painter.set_texture(*id, image_delta); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for (id, image_deltas) in textures_delta.set.drain() { + for image_delta in image_deltas { + self.painter.set_texture(id, &image_delta); + } } egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color); @@ -79,7 +82,8 @@ impl WebPainter for WebPainterGlow { self.screenshots.push((image, capture)); } - for &id in &textures_delta.free { + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for id in textures_delta.free.drain() { self.painter.free_texture(id); } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 0665d99f9..273675475 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -164,7 +164,7 @@ impl WebPainter for WebPainterWgpu { clear_color: [f32; 4], clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, - textures_delta: &egui::TexturesDelta, + textures_delta: &mut egui::TexturesDelta, capture_data: Vec, ) -> Result<(), JsValue> { let capture = !capture_data.is_empty(); @@ -210,13 +210,16 @@ impl WebPainter for WebPainterWgpu { let user_cmd_bufs = { let mut renderer = render_state.renderer.write(); - for (id, image_delta) in &textures_delta.set { - renderer.update_texture( - &render_state.device, - &render_state.queue, - *id, - image_delta, - ); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for (id, image_deltas) in textures_delta.set.drain() { + for image_delta in image_deltas { + renderer.update_texture( + &render_state.device, + &render_state.queue, + id, + &image_delta, + ); + } } renderer.update_buffers( @@ -388,8 +391,9 @@ impl WebPainter for WebPainterWgpu { // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. { let mut renderer = render_state.renderer.write(); - for id in &textures_delta.free { - renderer.free_texture(id); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for id in textures_delta.free.drain() { + renderer.free_texture(&id); } } diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 1abf2d2fa..62b5d9197 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -478,7 +478,7 @@ impl Painter { pixels_per_point: f32, clear_color: [f32; 4], clipped_primitives: &[epaint::ClippedPrimitive], - textures_delta: &epaint::textures::TexturesDelta, + textures_delta: &mut epaint::textures::TexturesDelta, capture_data: Vec, window: &Arc, ) -> f32 { @@ -545,21 +545,6 @@ impl Painter { commands_submitted: false, }; - { - // Upload textures before the surface-dependent early-returns below: - // uploads only need the device + queue, and the atlas dirty region is - // already consumed, so dropping the delta would desync the font texture. - let mut renderer = render_state.renderer.write(); - for (id, image_delta) in &textures_delta.set { - renderer.update_texture( - &render_state.device, - &render_state.queue, - *id, - image_delta, - ); - } - } - let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else { return vsync_sec; }; @@ -579,6 +564,18 @@ impl Painter { let user_cmd_bufs = { let mut renderer = render_state.renderer.write(); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for (id, image_deltas) in textures_delta.set.drain() { + for image_delta in image_deltas { + renderer.update_texture( + &render_state.device, + &render_state.queue, + id, + &image_delta, + ); + } + } + renderer.update_buffers( &render_state.device, &render_state.queue, @@ -742,8 +739,9 @@ impl Painter { // However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live. { let mut renderer = render_state.renderer.write(); - for id in &textures_delta.free { - renderer.free_texture(id); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for id in textures_delta.free.drain() { + renderer.free_texture(&id); } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index bfd6410bd..78f6ead11 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -775,6 +775,7 @@ impl Context { /// ui.label("Hello egui!"); /// }); /// // handle full_output + /// # full_output.drop_without_applying_deltas(); /// ``` #[must_use] pub fn run_ui(&self, new_input: RawInput, mut run_ui: impl FnMut(&mut Ui)) -> FullOutput { @@ -892,6 +893,7 @@ impl Context { /// /// let full_output = ctx.end_pass(); /// // handle full_output + /// # full_output.drop_without_applying_deltas(); /// ``` pub fn begin_pass(&self, mut new_input: RawInput) { profiling::function_scope!(); @@ -4296,6 +4298,7 @@ mod test { assert_eq!(num_calls, 1); assert_eq!(output.platform_output.num_completed_passes, 1); assert!(!output.platform_output.requested_discard()); + output.drop_without_applying_deltas(); } // A single call, with a denied request to discard: @@ -4321,6 +4324,7 @@ mod test { .reason, "test" ); + output.drop_without_applying_deltas(); } } @@ -4341,6 +4345,7 @@ mod test { assert_eq!(num_calls, 1); assert_eq!(output.platform_output.num_completed_passes, 1); assert!(!output.platform_output.requested_discard()); + output.drop_without_applying_deltas(); } // Request discard once: @@ -4363,6 +4368,7 @@ mod test { !output.platform_output.requested_discard(), "The request should have been cleared when fulfilled" ); + output.drop_without_applying_deltas(); } // Request discard twice: @@ -4387,6 +4393,7 @@ mod test { output.platform_output.requested_discard(), "The unfulfilled request should be reported" ); + output.drop_without_applying_deltas(); } } @@ -4415,6 +4422,7 @@ mod test { !output.platform_output.requested_discard(), "The request should have been cleared when fulfilled" ); + output.drop_without_applying_deltas(); } } } diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 5808ccabc..dc55d4712 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -16,7 +16,7 @@ pub struct FullOutput { /// Texture changes since last frame (including the font texture). /// - /// The backend needs to apply [`crate::TexturesDelta::set`] _before_ painting, + /// The backend needs to apply [`crate::TexturesDelta::push`] _before_ painting, /// and free any texture in [`crate::TexturesDelta::free`] _after_ painting. /// /// It is assumed that all egui viewports share the same painter and texture namespace. @@ -68,6 +68,12 @@ impl FullOutput { } } } + + /// [`epaint::textures::TexturesDelta`] will panic when dropped with still unapplied deltas, + /// this is a helper to clear the deltas. + pub fn drop_without_applying_deltas(mut self) { + self.textures_delta.clear(); + } } /// Information about text being edited. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index f0d4698e3..88de74b49 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -673,18 +673,20 @@ pub enum WidgetType { pub fn __run_test_ctx(mut run_ui: impl FnMut(&Context)) { let ctx = Context::default(); ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) - let _ = ctx.run_ui(Default::default(), |ui| { + let output = ctx.run_ui(Default::default(), |ui| { run_ui(ui.ctx()); }); + output.drop_without_applying_deltas(); } /// For use in tests; especially doctests. pub fn __run_test_ui(mut add_contents: impl FnMut(&mut Ui)) { let ctx = Context::default(); ctx.set_fonts(FontDefinitions::empty()); // prevent fonts from being loaded (save CPU time) - let _ = ctx.run_ui(Default::default(), |ui| { + let output = ctx.run_ui(Default::default(), |ui| { add_contents(ui); }); + output.drop_without_applying_deltas(); } pub fn accesskit_root_id() -> Id { diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index a49579eee..80cc90c8a 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -773,7 +773,7 @@ mod tests { .or_default() .selection = Some(test_selection()); - let _ = ctx.run_ui(RawInput::default(), |_| {}); + let output = ctx.run_ui(RawInput::default(), |_| {}); assert!( plugin .lock() @@ -782,11 +782,13 @@ mod tests { .is_some_and(ViewportLabelSelectionState::has_selection), "a pass in another viewport must not clear the child viewport selection" ); + output.drop_without_applying_deltas(); - let _ = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {}); + let output = ctx.run_ui(child_viewport_input(child_viewport_id), |_| {}); assert!( !plugin.lock().has_selection(), "the selection must be cleared when its labels disappear from the same viewport" ); + output.drop_without_applying_deltas(); } } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 1b1e64fe1..962b065a3 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -71,9 +71,8 @@ use std::sync::Arc; -use epaint::{Pos2, Vec2}; - use crate::{AsId, Context, Id, Ui}; +use epaint::{Pos2, Vec2}; // ---------------------------------------------------------------------------- diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index d9186b80a..36accead3 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -28,18 +28,21 @@ pub fn criterion_benchmark(c: &mut Criterion) { // The most end-to-end benchmark. c.bench_function("demo_with_tessellate__realistic", |b| { b.iter(|| { - let full_output = ctx.run_ui(RawInput::default(), |ui| { + let mut full_output = ctx.run_ui(RawInput::default(), |ui| { demo_windows.ui(ui); }); - ctx.tessellate(full_output.shapes, full_output.pixels_per_point) + ctx.tessellate(full_output.shapes, full_output.pixels_per_point); + + full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas }); }); c.bench_function("demo_no_tessellate", |b| { b.iter(|| { - ctx.run_ui(RawInput::default(), |ui| { + let output = ctx.run_ui(RawInput::default(), |ui| { demo_windows.ui(ui); - }) + }); + output.drop_without_applying_deltas(); }); }); @@ -49,6 +52,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { c.bench_function("demo_only_tessellate", |b| { b.iter(|| ctx.tessellate(full_output.shapes.clone(), full_output.pixels_per_point)); }); + full_output.drop_without_applying_deltas(); } if false { @@ -66,7 +70,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { { let ctx = egui::Context::default(); - let _ = ctx.run_ui(RawInput::default(), |ui| { + let output = ctx.run_ui(RawInput::default(), |ui| { c.bench_function("label &str", |b| { b.iter_batched_ref( || create_benchmark_ui(ui), @@ -86,11 +90,12 @@ pub fn criterion_benchmark(c: &mut Criterion) { ); }); }); + output.drop_without_applying_deltas(); } { let ctx = egui::Context::default(); - let _ = ctx.run_ui(RawInput::default(), |ui| { + let output = ctx.run_ui(RawInput::default(), |ui| { let mut group = c.benchmark_group("button"); // To ensure we have a valid image, let's use the font texture. The size @@ -134,6 +139,8 @@ pub fn criterion_benchmark(c: &mut Criterion) { ); }); }); + + output.drop_without_applying_deltas(); } { diff --git a/crates/egui_demo_lib/src/lib.rs b/crates/egui_demo_lib/src/lib.rs index 3d715f3c0..45f4640bd 100644 --- a/crates/egui_demo_lib/src/lib.rs +++ b/crates/egui_demo_lib/src/lib.rs @@ -72,11 +72,12 @@ fn test_egui_e2e() { const NUM_FRAMES: usize = 5; for _ in 0..NUM_FRAMES { - let full_output = ctx.run_ui(raw_input.clone(), |ui| { + let mut full_output = ctx.run_ui(raw_input.clone(), |ui| { demo_windows.ui(ui); }); let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); assert!(!clipped_primitives.is_empty()); + full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas } } @@ -91,7 +92,7 @@ fn test_egui_zero_window_size() { const NUM_FRAMES: usize = 5; for _ in 0..NUM_FRAMES { - let full_output = ctx.run_ui(raw_input.clone(), |ui| { + let mut full_output = ctx.run_ui(raw_input.clone(), |ui| { demo_windows.ui(ui); }); let clipped_primitives = ctx.tessellate(full_output.shapes, full_output.pixels_per_point); @@ -100,6 +101,7 @@ fn test_egui_zero_window_size() { "There should be nothing to show, has at least one primitive with clip_rect: {:?}", clipped_primitives[0].clip_rect ); + full_output.textures_delta.clear(); // Don't panic on drop with unapplied deltas } } diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 97588ede8..004f5d58c 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -358,17 +358,21 @@ impl Painter { screen_size_px: [u32; 2], pixels_per_point: f32, clipped_primitives: &[egui::ClippedPrimitive], - textures_delta: &egui::TexturesDelta, + textures_delta: &mut egui::TexturesDelta, ) { profiling::function_scope!(); - for (id, image_delta) in &textures_delta.set { - self.set_texture(*id, image_delta); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for (id, image_deltas) in textures_delta.set.drain() { + for image_delta in image_deltas { + self.set_texture(id, &image_delta); + } } self.paint_primitives(screen_size_px, pixels_per_point, clipped_primitives); - for &id in &textures_delta.free { + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for id in textures_delta.free.drain() { self.free_texture(id); } } diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index 6ed67ae8e..9d2b71310 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -107,8 +107,11 @@ impl EguiGlow { let shapes = std::mem::take(&mut self.shapes); let mut textures_delta = std::mem::take(&mut self.textures_delta); - for (id, image_delta) in textures_delta.set { - self.painter.set_texture(id, &image_delta); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for (id, image_deltas) in textures_delta.set.drain() { + for image_delta in image_deltas { + self.painter.set_texture(id, &image_delta); + } } let pixels_per_point = self.pixels_per_point; @@ -117,7 +120,8 @@ impl EguiGlow { self.painter .paint_primitives(dimensions, pixels_per_point, &clipped_primitives); - for id in textures_delta.free.drain(..) { + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for id in textures_delta.free.drain() { self.painter.free_texture(id); } } diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 6873be579..2a8575ef1 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -147,7 +147,7 @@ impl<'a, State> Harness<'a, State> { response = app.run(ui, &mut state, false); }); - renderer.handle_delta(&output.textures_delta); + renderer.handle_delta(&mut output.textures_delta); let mut harness = Self { app, @@ -269,7 +269,7 @@ impl<'a, State> Harness<'a, State> { .take() .expect("AccessKit was disabled"), ); - self.renderer.handle_delta(&output.textures_delta); + self.renderer.handle_delta(&mut output.textures_delta); self.output = output; self.handle_viewport_commands(); diff --git a/crates/egui_kittest/src/renderer.rs b/crates/egui_kittest/src/renderer.rs index 0806c4ead..3bffb3844 100644 --- a/crates/egui_kittest/src/renderer.rs +++ b/crates/egui_kittest/src/renderer.rs @@ -1,4 +1,5 @@ use egui::TexturesDelta; +use std::mem; pub trait TestRenderer { /// We use this to pass the glow / wgpu render state to [`eframe::Frame`]. @@ -6,7 +7,7 @@ pub trait TestRenderer { fn setup_eframe(&self, _cc: &mut eframe::CreationContext<'_>, _frame: &mut eframe::Frame) {} /// Handle a [`TexturesDelta`] by updating the renderer's textures. - fn handle_delta(&mut self, delta: &TexturesDelta); + fn handle_delta(&mut self, delta: &mut TexturesDelta); /// Render the [`crate::Harness`] and return the resulting image. /// @@ -25,7 +26,7 @@ pub trait TestRenderer { /// By default, this will create a wgpu renderer if the wgpu feature is enabled. pub enum LazyRenderer { Uninitialized { - texture_ops: Vec, + textures_delta: TexturesDelta, builder: Option Box>>, }, Initialized { @@ -39,7 +40,7 @@ impl Default for LazyRenderer { return Self::new(crate::wgpu::WgpuTestRenderer::new); #[cfg(not(feature = "wgpu"))] return Self::Uninitialized { - texture_ops: Vec::new(), + textures_delta: Vec::new(), builder: None, }; } @@ -48,16 +49,19 @@ impl Default for LazyRenderer { impl LazyRenderer { pub fn new(create_renderer: impl FnOnce() -> T + 'static) -> Self { Self::Uninitialized { - texture_ops: Vec::new(), + textures_delta: Default::default(), builder: Some(Box::new(move || Box::new(create_renderer()))), } } } impl TestRenderer for LazyRenderer { - fn handle_delta(&mut self, delta: &TexturesDelta) { + fn handle_delta(&mut self, delta: &mut TexturesDelta) { match self { - Self::Uninitialized { texture_ops, .. } => texture_ops.push(delta.clone()), + Self::Uninitialized { + textures_delta: texture_ops, + .. + } => texture_ops.append(mem::take(delta)), Self::Initialized { renderer } => renderer.handle_delta(delta), } } @@ -70,15 +74,15 @@ impl TestRenderer for LazyRenderer { ) -> Result { match self { Self::Uninitialized { - texture_ops, + textures_delta, builder: build, } => { let mut renderer = build.take().ok_or({ "No default renderer available. \ Enable the wgpu feature or set one via HarnessBuilder::renderer" })?(); - for delta in texture_ops.drain(..) { - renderer.handle_delta(&delta); + if !textures_delta.is_empty() { + renderer.handle_delta(textures_delta); } let image = renderer.render(ctx, output)?; *self = Self::Initialized { renderer }; @@ -88,3 +92,14 @@ impl TestRenderer for LazyRenderer { } } } + +impl Drop for LazyRenderer { + fn drop(&mut self) { + match self { + Self::Uninitialized { textures_delta, .. } => { + textures_delta.clear(); // Don't panic when dropping unapplied deltas + } + Self::Initialized { .. } => {} + } + } +} diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index 22972eabc..e5266aead 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -137,15 +137,23 @@ impl crate::TestRenderer for WgpuTestRenderer { frame.wgpu_render_state = Some(self.render_state.clone()); } - fn handle_delta(&mut self, delta: &TexturesDelta) { + fn handle_delta(&mut self, delta: &mut TexturesDelta) { let mut renderer = self.render_state.renderer.write(); - for (id, image) in &delta.set { - renderer.update_texture( - &self.render_state.device, - &self.render_state.queue, - *id, - image, - ); + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for (id, images) in delta.set.drain() { + for image in images { + renderer.update_texture( + &self.render_state.device, + &self.render_state.queue, + id, + &image, + ); + } + } + + #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here + for id in delta.free.drain() { + renderer.free_texture(&id); } } diff --git a/crates/egui_kittest/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs index 30dcbaf29..fea8f26da 100644 --- a/crates/egui_kittest/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -144,7 +144,8 @@ fn accesskit_output_single_egui_frame(run_ui: impl FnMut(&mut Ui)) -> TreeUpdate ctx.global_style_mut(|style| style.animation_time = 0.0); ctx.enable_accesskit(); - let output = ctx.run_ui(RawInput::default(), run_ui); + let mut output = ctx.run_ui(RawInput::default(), run_ui); + output.textures_delta.clear(); // Don't panic on drop with unapplied deltas output .platform_output diff --git a/crates/epaint/src/textures.rs b/crates/epaint/src/textures.rs index 0944a9052..1c4104a6e 100644 --- a/crates/epaint/src/textures.rs +++ b/crates/epaint/src/textures.rs @@ -1,4 +1,7 @@ use crate::{ImageData, ImageDelta, TextureId}; +use ahash::{HashMap, HashSet}; +use smallvec::{SmallVec, smallvec}; +use std::mem; // ---------------------------------------------------------------------------- @@ -40,7 +43,7 @@ impl TextureManager { options, }); - self.delta.set.push((id, ImageDelta::full(image, options))); + self.delta.push(id, ImageDelta::full(image, options)); id } @@ -58,10 +61,8 @@ impl TextureManager { // whole update meta.size = delta.image.size(); meta.bytes_per_pixel = delta.image.bytes_per_pixel(); - // since we update the whole image, we can discard all old enqueued deltas - self.delta.set.retain(|(x, _)| x != &id); } - self.delta.set.push((id, delta)); + self.delta.push(id, delta); } else { debug_assert!(false, "Tried setting texture {id:?} which is not allocated"); } @@ -74,7 +75,7 @@ impl TextureManager { meta.retain_count -= 1; if meta.retain_count == 0 { entry.remove(); - self.delta.free.push(id); + self.delta.free(id); } } else { debug_assert!(false, "Tried freeing texture {id:?} which is not allocated"); @@ -118,6 +119,12 @@ impl TextureManager { } } +impl Drop for TextureManager { + fn drop(&mut self) { + self.delta.clear(); // Prevent a debug panic on application shutdown + } +} + /// Meta-data about an allocated texture. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TextureMeta { @@ -276,10 +283,10 @@ pub enum TextureWrapMode { #[must_use = "The painter must take care of this"] pub struct TexturesDelta { /// New or changed textures. Apply before painting. - pub set: Vec<(TextureId, ImageDelta)>, + pub set: HashMap>, /// Textures to free after painting. - pub free: Vec, + pub free: HashSet, } impl TexturesDelta { @@ -287,9 +294,36 @@ impl TexturesDelta { self.set.is_empty() && self.free.is_empty() } + /// Inserts a [`ImageDelta`]. + /// + /// If this [`TexturesDelta`] already contains this [`TextureId`], and this is a `whole` delta, + /// the previous deltas for this id are discarded. + pub fn push(&mut self, id: TextureId, delta: ImageDelta) { + if delta.is_whole() { + // It replaces the whole texture, fine to overwrite any previous deltas + self.set.insert(id, smallvec![delta]); + } else { + self.set.entry(id).or_default().push(delta); + } + } + + pub fn free(&mut self, id: TextureId) { + self.free.insert(id); + } + + #[expect(clippy::iter_over_hash_type)] pub fn append(&mut self, mut newer: Self) { - self.set.extend(newer.set); - self.free.append(&mut newer.free); + // Only clear previous entries on append, not on set, since within a frame a texture might + // be created and immediately removed again. + for id in &newer.free { + self.set.remove(id); + } + for (id, deltas) in newer.set.drain() { + for delta in deltas { + self.push(id, delta); + } + } + self.free.extend(mem::take(&mut newer.free)); } pub fn clear(&mut self) { @@ -298,6 +332,17 @@ impl TexturesDelta { } } +impl Drop for TexturesDelta { + fn drop(&mut self) { + debug_assert!( + self.is_empty(), + "Dropped TexturesDelta with {} unapplied deltas. Deltas need to be handled. \ + If you want to drop this intentionally call `clear` before dropping.", + self.free.len() + self.set.len() + ); + } +} + impl std::fmt::Debug for TexturesDelta { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use std::fmt::Write as _; @@ -305,21 +350,24 @@ impl std::fmt::Debug for TexturesDelta { let mut debug_struct = f.debug_struct("TexturesDelta"); if !self.set.is_empty() { let mut string = String::new(); - for (tex_id, delta) in &self.set { - let size = delta.image.size(); - if let Some(pos) = delta.pos { - write!( - string, - "{:?} partial ([{} {}] - [{} {}]), ", - tex_id, - pos[0], - pos[1], - pos[0] + size[0], - pos[1] + size[1] - ) - .ok(); - } else { - write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok(); + #[expect(clippy::iter_over_hash_type)] + for (tex_id, deltas) in &self.set { + for delta in deltas { + let size = delta.image.size(); + if let Some(pos) = delta.pos { + write!( + string, + "{:?} partial ([{} {}] - [{} {}]), ", + tex_id, + pos[0], + pos[1], + pos[0] + size[0], + pos[1] + size[1] + ) + .ok(); + } else { + write!(string, "{:?} full {}x{}, ", tex_id, size[0], size[1]).ok(); + } } } debug_struct.field("set", &string); From c69834e65a0681d4fa40c30545b006ce39527034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Uma=C4=B5o?= <107099960+umajho@users.noreply.github.com> Date: Thu, 30 Jul 2026 00:55:23 +0800 Subject: [PATCH 03/49] Improve robustness of text input handling for `eframe/web` (#8045) * Fix [the Samsung Keyboard Korean Cheonjiin layout bug](https://github.com/emilk/egui/pull/7967#issuecomment-4098503570) * Partially fix (does not close) #8046 * Supersedes #7914 * Supersedes #8047 * Related: #8068 * Related: #7983 * Related: #8078 * [x] I have followed the instructions in the PR template This PR reworks the text input handling logic in eframe's web integration, primarily in `text_agent.rs`. It also adds a new `ImeEvent::DeleteSurrounding` variant, along with the corresponding handling logic in `egui` to support the changes. ## Fix: Samsung Keyboard Cheonjiin issue This PR fixes a bug reported by @rustbasic when using Samsung Keyboard's Cheonjiin Korean layout. Since Samsung Keyboard is only available on Samsung devices, I wasn't able to verify it myself. The fix is based on @rustbasic's testing and confirmation. The root cause is that the layout relies on the preceding text to correctly handle batchim composition. Previously, the text agent eagerly cleared the text input after every IME composition, removing the context too early. This PR makes that cleanup more conservative, preserving the text when it may still be needed. My understanding is that this is a quirk specific to Samsung Keyboard: The IME reports that composition has finished even though it is effectively still active and the composed text may continue to change. ## Fix: Keystrokes resetting keyboard layout (numpad/symbols/etc.) Partially fixes #8046. Previously, keystrokes would cause the on-screen keyboard to switch back to its primary layout. One remaining issue is that tapping within the active `TextEdit` to reposition the cursor still resets the keyboard to its primary layout. ## About text suggestions I originally planned to include text suggestion support in this PR because #8068 implemented it. However, adding text suggestion support would broaden the scope of this PR, so I think it is better addressed in a separate PR. For reference, [the reverted implementation](https://github.com/emilk/egui/pull/8045/commits/8a4f70859c8ba1e00186db1431e8f6906d2d1426) works fine on Android (Gboard), but not on iOS (iPadOS 17 + SwiftKey). --- crates/eframe/src/web/app_runner.rs | 7 +- crates/eframe/src/web/events.rs | 5 - crates/eframe/src/web/text_agent.rs | 468 ++++++++++++------ crates/egui-winit/src/lib.rs | 21 +- crates/egui/src/data/input/ime_event.rs | 9 + crates/egui/src/data/output.rs | 3 + crates/egui/src/widgets/text_edit/builder.rs | 158 ++++-- .../egui/src/widgets/text_edit/text_buffer.rs | 130 +++++ 8 files changed, 603 insertions(+), 198 deletions(-) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index f913e9b6d..3364d83ce 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -394,7 +394,10 @@ impl AppRunner { if self.has_focus() { // The eframe app has focus. - if ime.is_some() { + if let Some(ime) = ime { + if ime.should_interrupt_composition { + self.text_agent.interrupt_ime_composition(); + } // We are editing text: give the focus to the text agent. self.text_agent.focus(); } else { @@ -406,7 +409,7 @@ impl AppRunner { if let Err(err) = self .text_agent - .move_to(ime, self.canvas(), self.egui_ctx.zoom_factor()) + .update(ime, self.canvas(), self.egui_ctx.zoom_factor()) { log::error!( "failed to update text agent position: {}", diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 326ba1556..f9d992c2f 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -190,11 +190,6 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) return; } - if event.is_composing() || event.key_code() == 229 { - // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ - return; - } - let modifiers = modifiers_from_kb_event(&event); runner.input.set_modifiers(modifiers); diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index d8d6b672e..b80882769 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -1,7 +1,7 @@ //! The text agent is a hidden `` element used to capture //! IME and mobile keyboard input events. -use std::cell::Cell; +use std::{cell::RefCell, rc::Rc}; use wasm_bindgen::prelude::*; @@ -9,7 +9,7 @@ use super::{AppRunner, WebRunner}; pub struct TextAgent { input: web_sys::HtmlInputElement, - prev_ime_output: Cell>, + input_state: Rc>, } impl TextAgent { @@ -18,7 +18,8 @@ impl TextAgent { runner_ref: &WebRunner, canvas: &web_sys::HtmlCanvasElement, ) -> Result { - let document = web_sys::window().unwrap().document().unwrap(); + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); // create an `` element let input = document @@ -26,6 +27,7 @@ impl TextAgent { .dyn_into::()?; input.set_type("text"); input.set_attribute("autocapitalize", "off")?; + let input_state = Rc::new(RefCell::new(InputState::new(input.clone()))); // Hide the element, and park it over the top-left corner of the canvas // so that focusing it can never scroll some other part @@ -65,157 +67,67 @@ impl TextAgent { // attach event listeners - let on_input = { - let input = input.clone(); - move |event: web_sys::InputEvent, runner: &mut AppRunner| { - let text = input.value(); - // Workaround for an Android Gboard issue: after typing a word, - // the user has to delete invisible characters (whose count - // matches the length of the current suggestion) before actual - // characters are deleted, unless the focus has been reset. - // - // this issue appears to have been fixed in Gboard sometime - // between versions 14.7.09 and 17.0.12. - if !event.is_composing() { - input.blur().ok(); - super::focus_without_scroll(&input).ok(); - } - - if event.is_composing() { - // if `is_composing` is true, then user is using IME, for - // example: emoji, pinyin, kanji, hangul, etc. In that case, - // the browser emits both `input` and `compositionupdate` - // events. - // We handle the composition update here instead of in the - // `compositionupdate` event because the selection range - // has not yet been updated when `compositionupdate` fires. - - let Some(text) = event.data() else { return }; - let selection_start = input - .selection_start() - .unwrap_or(None) - .map(|pos| pos as usize); - let selection_end = input - .selection_end() - .unwrap_or(None) - .map(|pos| pos as usize); - let active_range_chars = if let Some(selection_start) = selection_start - && let Some(selection_end) = selection_end - { - let text_utf16 = text.encode_utf16().collect::>(); - let text_before_selection = - String::from_utf16_lossy(&text_utf16[..selection_start]); - let text_in_selection = - String::from_utf16_lossy(&text_utf16[selection_start..selection_end]); - let count_before_selection = text_before_selection.chars().count(); - let count_in_selection = text_in_selection.chars().count(); - Some(count_before_selection..count_before_selection + count_in_selection) - } else { - None - }; - let event = egui::Event::Ime(egui::ImeEvent::Preedit { - text, - active_range_chars, - }); - runner.input.raw.events.push(event); - } else { - if text.is_empty() { - return; - } - - input.set_value(""); - let event = egui::Event::Text(text); - runner.input.raw.events.push(event); - } - - runner.needs_repaint.repaint_asap(); - } - }; - - let on_composition_start = { + runner_ref.add_event_listener( + &input, + "compositionstart", move |_: web_sys::CompositionEvent, runner: &mut AppRunner| { // Repaint moves the text agent into place, - // see `move_to` in `AppRunner::handle_platform_output`. + // see `AppRunner::handle_platform_output`, which calls + // `TextAgent::update`. runner.needs_repaint.repaint_asap(); + }, + )?; + + runner_ref.add_event_listener(&input, "input", { + let input_state = Rc::clone(&input_state); + move |event: web_sys::InputEvent, runner: &mut AppRunner| { + input_state.borrow_mut().handle_input_event(&event, runner); } - }; - - let on_composition_end = { - let input = input.clone(); - move |event: web_sys::CompositionEvent, runner: &mut AppRunner| { - let Some(text) = event.data() else { return }; - input.set_value(""); - let event = egui::Event::Ime(egui::ImeEvent::Commit(text)); - runner.input.raw.events.push(event); - runner.needs_repaint.repaint_asap(); + })?; + runner_ref.add_event_listener(&input, "compositionend", { + let input_state = Rc::clone(&input_state); + move |_event: web_sys::CompositionEvent, runner: &mut AppRunner| { + input_state + .borrow_mut() + .handle_composition_end_event(runner); } - }; + })?; - runner_ref.add_event_listener(&input, "input", on_input)?; - runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?; - runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?; + runner_ref.add_event_listener(&input, "keydown", { + let input_state = Rc::clone(&input_state); + move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| { + let is_consumed = InputState::handle_keydown_event(&input_state, &event); + if !is_consumed { + // The canvas doesn't get keydown/keyup events when the text agent is focused, + // so we need to forward them to the runner: + super::events::on_keydown(event, runner); + } + } + })?; + runner_ref.add_event_listener(&input, "keyup", { + let input_state = Rc::clone(&input_state); + move |event: web_sys::KeyboardEvent, runner: &mut AppRunner| { + let is_consumed = InputState::handle_keyup_event(&input_state, &event); + if !is_consumed { + // The canvas doesn't get keydown/keyup events when the text agent is focused, + // so we need to forward them to the runner: + super::events::on_keyup(event, runner); + } + } + })?; - // The canvas doesn't get keydown/keyup events when the text agent is focused, - // so we need to forward them to the runner: - runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?; - runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?; - - Ok(Self { - input, - prev_ime_output: Default::default(), - }) + Ok(Self { input, input_state }) } - pub fn move_to( + pub fn update( &self, ime: Option, canvas: &web_sys::HtmlCanvasElement, zoom_factor: f32, ) -> Result<(), JsValue> { - // Don't move the text agent unless the position actually changed: - if self.prev_ime_output.get() == ime { - return Ok(()); - } - self.prev_ime_output.set(ime); - - let Some(ime) = ime else { return Ok(()) }; - - if ime.should_interrupt_composition { - // no-op for now: currently, the text agent is sizeless, so any - // click shifts focus to the canvas, which naturally interrupts the - // composition. - } - - let style = self.input.style(); - let native_ppp = super::native_pixels_per_point(); - - // The input is a sibling of the canvas (see `attach`), so we position - // it relative to the same containing block using the canvas offset. - // Unlike `get_bounding_client_rect`, the offset is unaffected by page - // scrolling, and doesn't flap when the virtual keyboard is shown on - // mobile Safari. - - // Clamp the input position within the canvas width to prevent unwanted horizontal scrolling. - let logical_canvas_width = canvas.width() as f32 / native_ppp; - let visible_x = ime.cursor_rect.center().x * zoom_factor; - let clamped_x = visible_x.clamp(0.0, logical_canvas_width); - - // Clamp the input position within the canvas height to prevent unwanted vertical scrolling. - let logical_canvas_height = canvas.height() as f32 / native_ppp; - let visible_y = ime.cursor_rect.center().y * zoom_factor; - let clamped_y = visible_y.clamp(0.0, logical_canvas_height); - - // This is where the IME input will point to: - style.set_property( - "left", - &format!("{}px", canvas.offset_left() as f32 + clamped_x), - )?; - style.set_property( - "top", - &format!("{}px", canvas.offset_top() as f32 + clamped_y), - )?; - - Ok(()) + self.input_state + .borrow_mut() + .update(ime, canvas, zoom_factor) } pub fn set_focus(&self, on: bool) { @@ -252,6 +164,11 @@ impl TextAgent { if let Err(err) = self.input.blur() { log::error!("failed to set focus: {}", super::string_from_js_value(&err)); } + self.input_state.borrow_mut().clear(); + } + + pub(crate) fn interrupt_ime_composition(&self) { + self.input_state.borrow_mut().clear(); } } @@ -260,3 +177,274 @@ impl Drop for TextAgent { self.input.remove(); } } + +struct InputState { + input: web_sys::HtmlInputElement, + last_text: String, + ime_output: Option, + keydown_special_case: KeydownSpecialCase, +} + +#[derive(Clone, Copy)] +enum KeydownSpecialCase { + None, + + /// On Android Gboard 14.7.09, when suggestions remain visible while typing + /// letters without IME composition (e.g., Latin or Cyrillic), pressing + /// Backspace produces key code 229 instead of the expected Backspace key + /// code. + /// Without the workaround, users have to press Backspace twice before text + /// starts being deleted. + /// + /// This workaround is also required for Android Gboard corrections and + /// completions (e.g., `tex|` -> `Texas`) to work correctly. In these + /// cases, a `deleteContentBackward` input event fires first (e.g., to + /// delete `tex`), followed by an `insertText` input event (e.g., to insert + /// `Texas`). + /// + /// Since it is difficult to distinguish between a Backspace press and a + /// correction or completion (e.g., when the state is `t|`, it is unclear + /// whether the user wants to delete `t` or replace it with `Texas`), we + /// send a `DeleteSurrounding` IME event in all cases instead of + /// synthetically generating Backspace press and release events. + AndroidKeycode229, + + /// iOS (18.6)'s built-in Korean keyboard uses `deleteContentBackward` to + /// compose Hangul characters. In these cases, the key code is 0. + IosKeycode0, +} + +impl InputState { + fn new(input: web_sys::HtmlInputElement) -> Self { + Self { + input, + last_text: String::new(), + ime_output: None, + keydown_special_case: KeydownSpecialCase::None, + } + } + + fn update( + &mut self, + ime: Option, + canvas: &web_sys::HtmlCanvasElement, + zoom_factor: f32, + ) -> Result<(), JsValue> { + // Don't move the text agent unless the position actually changed: + if self.ime_output == ime { + return Ok(()); + } + self.ime_output = ime; + + let Some(ime) = ime else { return Ok(()) }; + + // NOTE: we don't set the input's `type` to `password` based on + // `ime.purpose`, because that would confuse some password managers. + // For example, Chrome's password manager will always think the last + // letter typed in the password field is the password. + + let style = self.input.style(); + let native_ppp = super::native_pixels_per_point(); + + // The input is a sibling of the canvas (see `attach`), so we position + // it relative to the same containing block using the canvas offset. + // Unlike `get_bounding_client_rect`, the offset is unaffected by page + // scrolling, and doesn't flap when the virtual keyboard is shown on + // mobile Safari. + + // Clamp the input position within the canvas width to prevent unwanted horizontal scrolling. + let logical_canvas_width = canvas.width() as f32 / native_ppp; + let visible_x = ime.cursor_rect.center().x * zoom_factor; + let clamped_x = visible_x.clamp(0.0, logical_canvas_width); + + // Clamp the input position within the canvas height to prevent unwanted vertical scrolling. + let logical_canvas_height = canvas.height() as f32 / native_ppp; + let visible_y = ime.cursor_rect.center().y * zoom_factor; + let clamped_y = visible_y.clamp(0.0, logical_canvas_height); + + // This is where the IME input will point to: + style.set_property( + "left", + &format!("{}px", canvas.offset_left() as f32 + clamped_x), + )?; + style.set_property( + "top", + &format!("{}px", canvas.offset_top() as f32 + clamped_y), + )?; + + Ok(()) + } + + fn clear(&mut self) { + self.input.set_value(""); + self.last_text.clear(); + } + + fn handle_input_event(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) { + if self + .ime_output + .as_ref() + .is_some_and(|ime| ime.purpose == egui::IMEPurpose::Password) + { + self.handle_input_event_password(event, runner); + return; + } + + let input_type = event.input_type(); + + if !event.is_composing() + && input_type != "insertText" + // iOS uses this for corrections and completions (e.g., `tex|` -> + // `Texas`). + && input_type != "insertReplacementText" + && (matches!(self.keydown_special_case, KeydownSpecialCase::None) + || input_type != "deleteContentBackward") + { + self.clear(); + + return; + } + + let text = self.input.value(); + + let prefix_len = longest_common_prefix_length(&text, &self.last_text); + let last_text_len = self.last_text.chars().count(); + if prefix_len < last_text_len { + let out_event = egui::Event::Ime(egui::ImeEvent::DeleteSurrounding { + before_chars: last_text_len - prefix_len, + after_chars: 0, + }); + runner.input.raw.events.push(out_event); + } + + let preedit_text: String = text.chars().skip(prefix_len).collect(); + let out_event = if event.is_composing() { + // We handle the composition update here instead of in a + // `compositionupdate` event because the selection range + // has not yet been updated when `compositionupdate` fires. + let active_range_chars = self.active_range_chars(&text, prefix_len); + egui::Event::Ime(egui::ImeEvent::Preedit { + text: preedit_text, + active_range_chars, + }) + } else { + egui::Event::Text(preedit_text) + }; + runner.input.raw.events.push(out_event); + + if event.is_composing() { + self.last_text = text.chars().take(prefix_len).collect(); + } else { + self.last_text = text; + } + + runner.needs_repaint.repaint_asap(); + } + + fn handle_input_event_password(&mut self, event: &web_sys::InputEvent, runner: &mut AppRunner) { + let input_type = event.input_type(); + + if input_type != "insertText" { + return; + } + + let text = self.input.value(); + + runner.input.raw.events.push(egui::Event::Text(text)); + self.clear(); + } + + /// Compute the active range (cursor or conversion segment) within the + /// preedit text, based on the selection in the input element. + /// + /// `text` is the full `input.value()`, and `prefix_len_chars` is the + /// number of chars at the start of `text` that are committed (not part + /// of the preedit). `selectionStart`/`selectionEnd` are UTF-16 offsets + /// within the full `input.value()`, so they are adjusted to be relative + /// to the preedit text. + fn active_range_chars( + &self, + text: &str, + prefix_len_chars: usize, + ) -> Option> { + let selection_start = self.input.selection_start().unwrap_or(None)? as usize; + let selection_end = self.input.selection_end().unwrap_or(None)? as usize; + + let text_utf16 = text.encode_utf16().collect::>(); + if selection_start > text_utf16.len() || selection_end > text_utf16.len() { + // This can occur on Android Chrome. see discussion in: + // . + return None; + } + + let text_before_selection = String::from_utf16_lossy(&text_utf16[..selection_start]); + let text_in_selection = + String::from_utf16_lossy(&text_utf16[selection_start..selection_end]); + let count_before_selection = text_before_selection.chars().count(); + let count_in_selection = text_in_selection.chars().count(); + + // Adjust for the committed prefix to get the range within the preedit text. + let start = count_before_selection.saturating_sub(prefix_len_chars); + let end = start + count_in_selection; + Some(start..end) + } + + fn handle_composition_end_event(&mut self, runner: &mut AppRunner) { + let text = self.input.value(); + + let commit_text = { + let prefix_len = self.last_text.chars().count(); + text.chars().skip(prefix_len).collect::() + }; + let out_event = egui::Event::Ime(egui::ImeEvent::Commit(commit_text)); + runner.input.raw.events.push(out_event); + + self.last_text = text; + + runner.needs_repaint.repaint_asap(); + } + + /// ## Returns + /// Whether the event is consumed. If `true`, the caller should not do + /// further processing for this event. + fn handle_keydown_event(input_state: &RefCell, event: &web_sys::KeyboardEvent) -> bool { + // Platform-sniffing methods are unreliable, so they are not used as + // guards here. + let special_case = match event.key_code() { + 229 => KeydownSpecialCase::AndroidKeycode229, + 0 => KeydownSpecialCase::IosKeycode0, + _ => KeydownSpecialCase::None, + }; + input_state.borrow_mut().keydown_special_case = special_case; + + // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ + if event.is_composing() || !matches!(special_case, KeydownSpecialCase::None) { + true + } else { + if event.key().chars().count() > 1 + || event.ctrl_key() + || event.alt_key() + || event.meta_key() + { + input_state.borrow_mut().clear(); + } + false + } + } + + /// ## Returns + /// Whether the event is consumed. If `true`, the caller should not do + /// further processing for this event. + fn handle_keyup_event(input_state: &RefCell, event: &web_sys::KeyboardEvent) -> bool { + input_state.borrow_mut().keydown_special_case = KeydownSpecialCase::None; + + // https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/ + event.is_composing() || event.key_code() == 229 + } +} + +fn longest_common_prefix_length(a: &str, b: &str) -> usize { + std::iter::zip(a.chars(), b.chars()) + .take_while(|(a, b)| a == b) + .count() +} diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index d17428adb..85b22a997 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -121,6 +121,7 @@ pub struct State { allow_ime: bool, ime_rect_px: Option, + old_ime_purpose: egui::IMEPurpose, /// Used by [`State::try_on_ime_processed_keyboard_input`] to track key /// release events that should be filtered out. See comments in that method @@ -171,6 +172,7 @@ impl State { allow_ime: false, ime_rect_px: None, + old_ime_purpose: egui::IMEPurpose::Normal, #[cfg(target_os = "windows")] pressed_processed_physical_keys: HashSet::new(), }; @@ -1158,6 +1160,11 @@ impl State { window.set_ime_allowed(true); } + if ime.purpose != self.old_ime_purpose { + self.old_ime_purpose = ime.purpose; + window.set_ime_purpose(to_winit_ime_purpose(ime.purpose)); + } + let pixels_per_point = pixels_per_point(&self.egui_ctx, window); let ime_rect_px = pixels_per_point * ime.rect; if self.ime_rect_px != Some(ime_rect_px) @@ -1880,11 +1887,7 @@ fn process_viewport_command( ); } ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), - ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { - egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, - egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, - egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, - }), + ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(to_winit_ime_purpose(p)), ViewportCommand::Focus => { if !window.has_focus() { window.focus_window(); @@ -1945,6 +1948,14 @@ fn process_viewport_command( } } +fn to_winit_ime_purpose(purpose: egui::IMEPurpose) -> winit::window::ImePurpose { + match purpose { + egui::IMEPurpose::Password => winit::window::ImePurpose::Password, + egui::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, + egui::IMEPurpose::Normal => winit::window::ImePurpose::Normal, + } +} + /// Build and intitlaize a window. /// /// Wrapper around `create_winit_window_builder` and `apply_viewport_builder_to_window`. diff --git a/crates/egui/src/data/input/ime_event.rs b/crates/egui/src/data/input/ime_event.rs index de12a920f..b814b51cd 100644 --- a/crates/egui/src/data/input/ime_event.rs +++ b/crates/egui/src/data/input/ime_event.rs @@ -22,6 +22,15 @@ pub enum ImeEvent { /// The IME is considered dismissed after this event. Commit(String), + /// Notifies when the text surrounding the cursor should be deleted. + /// + /// `before_chars` and `after_chars` are the number of characters (not + /// bytes) to delete before and after the cursor, respectively. + DeleteSurrounding { + before_chars: usize, + after_chars: usize, + }, + /// Notifies when the IME was disabled. #[deprecated = "No longer used by egui"] Disabled, diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index dc55d4712..bbd271b71 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -82,6 +82,9 @@ impl FullOutput { #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct IMEOutput { + /// IME's purpose. + pub purpose: crate::IMEPurpose, + /// Where the [`crate::TextEdit`] is located on screen. pub rect: crate::Rect, diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index ccc3a56c8..0a52d636c 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -5,9 +5,10 @@ use epaint::text::{Galley, LayoutJob, TextWrapMode, cursor::CCursor}; use crate::{ Align, Align2, AsIdSalt, AtomExt as _, AtomKind, AtomLayout, Atoms, Color32, Context, - CursorIcon, Event, EventFilter, FontSelection, Frame, Id, IdSalt, ImeEvent, IntoAtoms, - IntoSizedResult, Key, KeyboardShortcut, Margin, Modifiers, NumExt as _, Response, Sense, - SizedAtomKind, TextBuffer, TextStyle, Ui, Vec2, Widget, WidgetInfo, WidgetWithState, epaint, + CursorIcon, Event, EventFilter, FontSelection, Frame, IMEPurpose, Id, IdSalt, 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, @@ -527,6 +528,19 @@ impl TextEdit<'_> { let mut cursor_range = None; let mut prev_cursor_range = None; + let owns_ime_events = ui.memory(|mem| mem.owns_ime_events(id)); + if !owns_ime_events { + state.cursor_purpose = TextEditCursorPurpose::Selection; + if !state.cursor.is_empty() { + state.cursor.set_char_range( + state + .cursor + .char_range() + .map(|r| CCursorRange::one(r.primary)), + ); + } + } + let mut text_changed = false; let text_mutable = text.is_mutable(); @@ -547,14 +561,17 @@ impl TextEdit<'_> { text, galley, layouter, - id, - wrap_width, - multiline, - password, - default_cursor_range, - char_limit, - event_filter, - return_key, + &EventsOptions { + id, + wrap_width, + multiline, + password, + default_cursor_range, + owns_ime_events, + char_limit, + event_filter, + return_key, + }, ); if changed { @@ -772,6 +789,7 @@ impl TextEdit<'_> { if did_interact || response.clicked() { ui.memory_mut(|mem| mem.request_focus(response.id)); + state.cursor_purpose = TextEditCursorPurpose::Selection; state.last_interaction_time = ui.input(|i| i.time); } @@ -907,6 +925,11 @@ impl TextEdit<'_> { .unwrap_or_default(); ui.output_mut(|o| { o.ime = Some(crate::output::IMEOutput { + purpose: if password { + IMEPurpose::Password + } else { + IMEPurpose::Normal + }, rect: to_global * inner_rect, cursor_rect: to_global * primary_cursor_rect, should_interrupt_composition: false, @@ -993,23 +1016,41 @@ fn mask_if_password(is_password: bool, text: &str) -> String { // ---------------------------------------------------------------------------- +/// Bundles parameters for [`events`] to avoid `clippy::too_many_arguments` and +/// `clippy::fn_params_excessive_bools`. +struct EventsOptions { + id: Id, + wrap_width: f32, + multiline: bool, + password: bool, + default_cursor_range: CCursorRange, + owns_ime_events: bool, + char_limit: usize, + event_filter: EventFilter, + return_key: Option, +} + /// Check for (keyboard) events to edit the cursor and/or text. -#[expect(clippy::too_many_arguments)] fn events( ui: &crate::Ui, state: &mut TextEditState, text: &mut dyn TextBuffer, galley: &mut Arc, layouter: &mut dyn FnMut(&Ui, &dyn TextBuffer, f32) -> Arc, - id: Id, - wrap_width: f32, - multiline: bool, - password: bool, - default_cursor_range: CCursorRange, - char_limit: usize, - event_filter: EventFilter, - return_key: Option, + opts: &EventsOptions, ) -> (bool, CCursorRange) { + let EventsOptions { + id, + wrap_width, + multiline, + password, + default_cursor_range, + owns_ime_events, + char_limit, + event_filter, + return_key, + } = *opts; + let os = ui.os(); let mut cursor_range = state.cursor.range(galley).unwrap_or(default_cursor_range); @@ -1031,9 +1072,13 @@ 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; + enum CursorMutation { + Selection(CCursorRange), + ImeComposition { + cursor_range: CCursorRange, + active_range: Option>, + }, + ImeCompositionCursorRange(CCursorRange), } for event in &events { @@ -1052,7 +1097,9 @@ fn events( None } else { copy_if_not_password(ui, cursor_range.slice_str(text.as_str()).to_owned()); - Some(CCursorRange::one(text.delete_selected(&cursor_range))) + Some(CursorMutation::Selection(CCursorRange::one( + text.delete_selected(&cursor_range), + ))) } } Event::Paste(text_to_insert) => { @@ -1067,7 +1114,7 @@ fn events( text.insert_text_at(&mut ccursor, &single_line, char_limit); } - Some(CCursorRange::one(ccursor)) + Some(CursorMutation::Selection(CCursorRange::one(ccursor))) } } Event::Text(text_to_insert) => { @@ -1077,7 +1124,7 @@ fn events( text.insert_text_at(&mut ccursor, text_to_insert, char_limit); - Some(CCursorRange::one(ccursor)) + Some(CursorMutation::Selection(CCursorRange::one(ccursor))) } else { None } @@ -1095,7 +1142,7 @@ fn events( } else { text.insert_text_at(&mut ccursor, "\t", char_limit); } - Some(CCursorRange::one(ccursor)) + Some(CursorMutation::Selection(CCursorRange::one(ccursor))) } Event::Key { key, @@ -1110,7 +1157,7 @@ fn events( let mut ccursor = text.delete_selected(&cursor_range); text.insert_text_at(&mut ccursor, "\n", char_limit); // TODO(emilk): if code editor, auto-indent by same leading tabs, + one if the lines end on an opening bracket - Some(CCursorRange::one(ccursor)) + Some(CursorMutation::Selection(CCursorRange::one(ccursor))) } else { ui.memory_mut(|mem| mem.surrender_focus(id)); // End input with enter break; @@ -1132,7 +1179,7 @@ fn events( .redo(&(cursor_range, text.as_str().to_owned())) { text.replace_with(redo_txt); - Some(*redo_ccursor_range) + Some(CursorMutation::Selection(*redo_ccursor_range)) } else { None } @@ -1150,7 +1197,7 @@ fn events( .undo(&(cursor_range, text.as_str().to_owned())) { text.replace_with(undo_txt); - Some(*undo_ccursor_range) + Some(CursorMutation::Selection(*undo_ccursor_range)) } else { None } @@ -1161,8 +1208,8 @@ fn events( key, pressed: true, .. - } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key), - + } => check_for_mutating_key_press(os, &cursor_range, text, galley, modifiers, *key) + .map(CursorMutation::Selection), Event::Ime(ime_event) if owns_ime_events => { /// Both `ImeEvent::Preedit("")` and `ImeEvent::Commit("")` /// might be emitted from different integrations to signify that @@ -1235,22 +1282,20 @@ fn events( text: preedit_text, active_range_chars, } => { - state.cursor_purpose = if preedit_text.is_empty() { - TextEditCursorPurpose::Selection + let mut ccursor = clear_preedit_text(text, &cursor_range); + + if preedit_text.is_empty() { + Some(CursorMutation::Selection(CCursorRange::one(ccursor))) } else { - TextEditCursorPurpose::ImeComposition { + let start_cursor = ccursor; + text.insert_text_at(&mut ccursor, preedit_text, char_limit); + Some(CursorMutation::ImeComposition { + cursor_range: CCursorRange::two(start_cursor, ccursor), active_range: active_range_chars.clone().map(|range| { CCursor::new(range.start)..CCursor::new(range.end) }), - } - }; - 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); + }) } - Some(CCursorRange::two(start_cursor, ccursor)) } ImeEvent::Commit(commit_text) => { state.cursor_purpose = TextEditCursorPurpose::Selection; @@ -1260,22 +1305,43 @@ fn events( text.insert_text_at(&mut ccursor, commit_text, char_limit); } - Some(CCursorRange::one(ccursor)) + Some(CursorMutation::Selection(CCursorRange::one(ccursor))) } + ImeEvent::DeleteSurrounding { + before_chars, + after_chars, + } => Some(CursorMutation::ImeCompositionCursorRange( + text.delete_surrounding_chars(cursor_range, *before_chars, *after_chars), + )), } } _ => None, }; - if let Some(new_ccursor_range) = did_mutate_text { + if let Some(cursor_mutation) = did_mutate_text { any_change = true; // Layout again to avoid frame delay, and to keep `text` and `galley` in sync. *galley = layouter(ui, text, wrap_width); // Set cursor_range using new galley: - cursor_range = new_ccursor_range; + match cursor_mutation { + CursorMutation::Selection(new_cursor_range) => { + cursor_range = new_cursor_range; + state.cursor_purpose = TextEditCursorPurpose::Selection; + } + CursorMutation::ImeComposition { + cursor_range: new_cursor_range, + active_range, + } => { + cursor_range = new_cursor_range; + state.cursor_purpose = TextEditCursorPurpose::ImeComposition { active_range }; + } + CursorMutation::ImeCompositionCursorRange(new_cursor_range) => { + cursor_range = new_cursor_range; + } + } } } diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index 848b993d0..1fada9626 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -154,6 +154,30 @@ pub trait TextBuffer { self.delete_selected_ccursor_range([min_ccursor, max_ccursor]) } + /// Deletes characters surrounding the current cursor range. + /// + /// Removes `before_chars` characters before the selection start and + /// `after_chars` characters after the selection end. + /// The returned [`CCursorRange`] is adjusted to account for the removed + /// characters before the selection. + fn delete_surrounding_chars( + &mut self, + mut cursor_range: CCursorRange, + before_chars: usize, + after_chars: usize, + ) -> CCursorRange { + let [min, max] = cursor_range.sorted_cursors(); + if after_chars > 0 { + self.delete_selected_ccursor_range([max, max + after_chars]); + } + if before_chars > 0 { + self.delete_selected_ccursor_range([min - before_chars, min]); + cursor_range.primary -= before_chars; + cursor_range.secondary -= before_chars; + } + cursor_range + } + fn delete_paragraph_before_cursor( &mut self, galley: &Galley, @@ -320,3 +344,109 @@ impl TextBuffer for &str { std::any::TypeId::of::<&str>() } } + +#[cfg(test)] +mod tests { + use super::*; + + fn txt_n_sel(input: &str) -> (String, CCursorRange) { + assert!( + input.matches('[').count() == 1 && input.matches(']').count() == 1, + "`input` must contain exactly one `[` and one `]` to indicate the selection (cursor range)" + ); + let mut primary_index = input.chars().position(|c| c == ']').unwrap(); + let mut secondary_index = input.chars().position(|c| c == '[').unwrap(); + let text = input.replace(['[', ']'], ""); + if primary_index > secondary_index { + primary_index -= 1; + } else { + secondary_index -= 1; + } + let cursor_range = CCursorRange { + primary: CCursor::new(primary_index), + secondary: CCursor::new(secondary_index), + h_pos: None, + }; + (text, cursor_range) + } + + #[test] + fn test_txt_n_sel() { + assert_eq!( + txt_n_sel("<>"), + ("<>".to_owned(), CCursorRange::one(CCursor::new(3))) + ); + assert_eq!( + txt_n_sel("<>"), + ( + "<>".to_owned(), + CCursorRange::two(CCursor::new(3), CCursor::new(4)) + ) + ); + assert_eq!( + txt_n_sel("<<左[_]右>>"), + ( + "<<左_右>>".to_owned(), + CCursorRange::two(CCursor::new(3), CCursor::new(4)) + ) + ); + assert_eq!( + txt_n_sel("<>"), + ( + "<>".to_owned(), + CCursorRange::two(CCursor::new(4), CCursor::new(3)) + ) + ); + } + + #[test] + fn test_delete_surrounding_chars() { + fn test_case( + (mut input_text, input_cursor_range): (String, CCursorRange), + before_chars: usize, + after_chars: usize, + (expected_text, expected_cursor_range): (String, CCursorRange), + ) { + let new_cursor_range = + input_text.delete_surrounding_chars(input_cursor_range, before_chars, after_chars); + assert_eq!(input_text, expected_text); + assert_eq!(new_cursor_range, expected_cursor_range); + } + + // 1 byte per char + test_case(txt_n_sel("<>"), 1, 1, txt_n_sel("<<[]>>")); + test_case(txt_n_sel("<>"), 1, 0, txt_n_sel("<<[_]R>>")); + test_case(txt_n_sel("<>"), 0, 1, txt_n_sel("<>")); + test_case(txt_n_sel("<>"), 1, 1, txt_n_sel("<<[_]>>")); + test_case(txt_n_sel("<>"), 1, 1, txt_n_sel("<<[__]>>")); + test_case(txt_n_sel("<>"), 2, 2, txt_n_sel("<<[_]>>")); + test_case(txt_n_sel("<>"), 1, 0, txt_n_sel("<<]_[R>>")); + test_case(txt_n_sel("<>"), 0, 1, txt_n_sel("<>")); + test_case(txt_n_sel("<>"), 1, 1, txt_n_sel("<<]_[>>")); + + // 2 bytes per char: `˻` = `0xCB 0xBB`, `˼` = `0xCB 0xBC` + test_case(txt_n_sel("<<˻[]˼>>"), 1, 1, txt_n_sel("<<[]>>")); + test_case(txt_n_sel("<<˻[_]˼>>"), 1, 0, txt_n_sel("<<[_]˼>>")); + test_case(txt_n_sel("<<˻[_]˼>>"), 0, 1, txt_n_sel("<<˻[_]>>")); + test_case(txt_n_sel("<<˻[_]˼>>"), 1, 1, txt_n_sel("<<[_]>>")); + test_case(txt_n_sel("<<˻[__]˼>>"), 1, 1, txt_n_sel("<<[__]>>")); + test_case(txt_n_sel("<<˻˻[_]˼˼>>"), 2, 2, txt_n_sel("<<[_]>>")); + test_case(txt_n_sel("<<˻]_[˼>>"), 1, 0, txt_n_sel("<<]_[˼>>")); + test_case(txt_n_sel("<<˻]_[˼>>"), 0, 1, txt_n_sel("<<˻]_[>>")); + test_case(txt_n_sel("<<˻]_[˼>>"), 1, 1, txt_n_sel("<<]_[>>")); + + // 3 bytes per char: `左` = `0xE5 0xB7 0xA6`, `右` = `0xE5 0x8F 0xB3` + test_case(txt_n_sel("<<左[]右>>"), 1, 1, txt_n_sel("<<[]>>")); + test_case(txt_n_sel("<<左[_]右>>"), 1, 0, txt_n_sel("<<[_]右>>")); + test_case(txt_n_sel("<<左[_]右>>"), 0, 1, txt_n_sel("<<左[_]>>")); + test_case(txt_n_sel("<<左[_]右>>"), 1, 1, txt_n_sel("<<[_]>>")); + test_case(txt_n_sel("<<左[__]右>>"), 1, 1, txt_n_sel("<<[__]>>")); + test_case(txt_n_sel("<<左左[_]右右>>"), 2, 2, txt_n_sel("<<[_]>>")); + test_case(txt_n_sel("<<左]_[右>>"), 1, 0, txt_n_sel("<<]_[右>>")); + test_case(txt_n_sel("<<左]_[右>>"), 0, 1, txt_n_sel("<<左]_[>>")); + test_case(txt_n_sel("<<左]_[右>>"), 1, 1, txt_n_sel("<<]_[>>")); + + // mixed + test_case(txt_n_sel("<>"), 3, 3, txt_n_sel("<<[_]>>")); + } +} From 36341c21fe7422f742f860949f68e2b6e381934c Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 31 Jul 2026 14:30:46 +0200 Subject: [PATCH 04/49] Make non-interactive tooltips not interactable (#8362) Fixes two bugs around tooltips: - During sizing pass tooltips would render at a different size, at which point they might overlap the interacted widget causing the hover state to change and the tooltip to never be shown - Fix: During sizing pass make the area `interactable: false` so events pass through - When hovering close to the boarder of a widget (in the area where `interact_radius` takes effect), there could be a feedback loop where the tooltip is shown one frame and hidden the next. - Fix: Always make tooltips `interactable: false` when they don't have interactive contents This PR changes the behavior of `Area::interactable` to it's original behavior: Now interactions will pass through the area background _and_ it's containing widgets. It worked this way initially but got changed in #4026 --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/egui/src/containers/area.rs | 6 +- crates/egui/src/containers/popup.rs | 13 +++ crates/egui/src/containers/tooltip.rs | 32 +++++-- crates/egui/src/context.rs | 8 +- crates/egui/src/memory/mod.rs | 5 ++ tests/egui_tests/tests/regression_tests.rs | 84 ++++++++++++++++++- .../test_tooltip_hover_regression.png | 3 + ..._button_should_not_cause_feedback_loop.png | 3 + 8 files changed, 140 insertions(+), 14 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png create mode 100644 tests/egui_tests/tests/snapshots/tooltip_covering_button_should_not_cause_feedback_loop.png diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 4b3a4f722..61375b580 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -454,14 +454,12 @@ impl Area { state.size = None; } state.pivot = pivot; - state.interactable = interactable; if let Some(new_pos) = new_pos { state.pivot_pos = Some(new_pos); } state.pivot_pos.get_or_insert_with(|| { default_pos.unwrap_or_else(|| automatic_area_position(ctx, constrain_rect, layer_id)) }); - state.interactable = interactable; let size = *state.size.get_or_insert_with(|| { sizing_pass = true; @@ -484,6 +482,10 @@ impl Area { size }); + // We should never be interactable during a sizing pass, since then we are shown at a different + // size which might interfere with hover state of the hovered widget causing popup feedback loops. + state.interactable = interactable && !sizing_pass; + // TODO(emilk): if last frame was sizing pass, it should be considered invisible for smoother fade-in let visible_last_frame = ctx.memory(|mem| mem.areas().visible_last_frame(&layer_id)); diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 080c00bd1..587c77e23 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -180,6 +180,7 @@ pub struct Popup<'a> { /// Default width passed to the Area width: Option, sense: Sense, + interactable: bool, layout: Layout, frame: Option, style: StyleModifier, @@ -202,6 +203,7 @@ impl<'a> Popup<'a> { gap: 0.0, width: None, sense: Sense::click(), + interactable: true, layout: Layout::default(), frame: None, style: StyleModifier::default(), @@ -369,6 +371,15 @@ impl<'a> Popup<'a> { self } + /// If `false`, the pointer goes straight through the popup and it's widgets to whatever is behind it. + /// + /// Default: `true`. + #[inline] + pub fn interactable(mut self, interactable: bool) -> Self { + self.interactable = interactable; + self + } + /// Set the sense of the popup. #[inline] pub fn sense(mut self, sense: Sense) -> Self { @@ -546,6 +557,7 @@ impl<'a> Popup<'a> { gap, width, sense, + interactable, layout, frame, style, @@ -570,6 +582,7 @@ impl<'a> Popup<'a> { .pivot(pivot) .fixed_pos(anchor) .sense(sense) + .interactable(interactable) .layout(layout) .sizing_pass(!was_open_last_frame) .info(info.unwrap_or_else(|| { diff --git a/crates/egui/src/containers/tooltip.rs b/crates/egui/src/containers/tooltip.rs index 22c319569..a372d6cd1 100644 --- a/crates/egui/src/containers/tooltip.rs +++ b/crates/egui/src/containers/tooltip.rs @@ -129,7 +129,15 @@ impl Tooltip<'_> { }); let tooltip_area_id = Self::tooltip_id(parent_widget, state.tooltip_count); - popup = popup.anchor(state.bounding_rect).id(tooltip_area_id); + + // Tooltips without interactive contents should not be interactable (hover should pass + // through to the widget below). + let interactable = Self::had_interactive_widgets(popup.ctx(), tooltip_area_id); + + popup = popup + .anchor(state.bounding_rect) + .id(tooltip_area_id) + .interactable(interactable); let response = popup.show(|ui| { // By default, the text in tooltips aren't selectable. @@ -192,6 +200,20 @@ impl Tooltip<'_> { widget_id.with(tooltip_count) } + /// Did this tooltip contain anything the user can interact with, last pass? + /// + /// Most tooltips are just text. Those should not react to the pointer at all, + /// or they would steal the hover from the widget they belong to. + fn had_interactive_widgets(ctx: &Context, tooltip_id: Id) -> bool { + let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id); + ctx.viewport(|vp| { + vp.prev_pass + .widgets + .get_layer(tooltip_layer_id) + .any(|w| w.enabled && w.sense.interactive()) + }) + } + /// Should we show a tooltip for this response? /// /// Argument `allow_interactive_tooltip` controls whether mouse can interact with tooltip that @@ -247,15 +269,9 @@ impl Tooltip<'_> { // Check if we should automatically stay open: let tooltip_id = Self::next_tooltip_id(&response.ctx, response.id); - let tooltip_layer_id = LayerId::new(Order::Tooltip, tooltip_id); let tooltip_has_interactive_widget = allow_interactive_tooltip - && response.ctx.viewport(|vp| { - vp.prev_pass - .widgets - .get_layer(tooltip_layer_id) - .any(|w| w.enabled && w.sense.interactive()) - }); + && Self::had_interactive_widgets(&response.ctx, tooltip_id); if tooltip_has_interactive_widget { // We keep the tooltip open if hovered, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 78f6ead11..e0705b201 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -466,7 +466,13 @@ impl ContextImpl { viewport.this_pass.begin_pass(); { - let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); + // Areas that are not interactable are click-through: skip them in the hit-test. + let mut layers: Vec = viewport + .prev_pass + .widgets + .layer_ids() + .filter(|layer_id| self.memory.areas().is_interactable(*layer_id)) + .collect(); layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b)); viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 1f22b3dd4..8fd407cc9 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1194,6 +1194,11 @@ impl Areas { self.areas.get_mut(&id) } + /// Can the user interact with this layer or it's widgets, or do clicks go straight through it? + pub(crate) fn is_interactable(&self, layer_id: LayerId) -> bool { + self.get(layer_id.id).is_none_or(|area| area.interactable) + } + /// All layers back-to-front, top is last. pub(crate) fn order(&self) -> &[LayerId] { &self.order diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index f250b1ab2..34527f4cc 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -1,15 +1,15 @@ use std::sync::Arc; -use egui::ScrollArea; use egui::accesskit::Role; #[cfg(debug_assertions)] use egui::epaint::Shape; use egui::style::ScrollAnimation; use egui::text::{LayoutJob, TextWrapping}; use egui::{ - Align, Button, Color32, FontFamily, FontId, Image, Label, Layout, RichText, Sense, TextBuffer, - TextFormat, TextWrapMode, Ui, include_image, vec2, + Align, Button, Color32, FontFamily, FontId, Image, Label, Layout, Rect, RichText, Sense, + TextBuffer, TextFormat, TextWrapMode, Ui, Vec2, include_image, vec2, }; +use egui::{Pos2, ScrollArea}; use egui_kittest::Harness; use egui_kittest::kittest::{NodeT as _, Queryable as _}; @@ -481,3 +481,81 @@ fn animated_scroll_beats_sticky_bottom() { "animated explicit scroll should leave the sticky bottom" ); } + +/// Tests that tooltips are shown correctly for buttons that are only shown on hover. +/// +/// Basically, this tests that a tooltip overlapping the mouse cursor does not interfere with a +/// buttons hover state. +#[test] +fn tooltip_should_work_for_hover_button() { + let button_rect = Rect::from_min_size(Pos2::new(4.0, 4.0), Vec2::new(80.0, 20.0)); + let mut harness = Harness::builder().with_size((320.0, 80.0)).build_ui(|ui| { + if ui.rect_contains_pointer(button_rect) { + ui.button("A tooltip should be shown") + .on_hover_text("My tooltip"); + } + }); + + harness.hover_at(button_rect.center()); + + harness.run(); + + harness.snapshot("test_tooltip_hover_regression"); +} + +/// Ensure that hovering close to a widget doesn't cause a tooltip feedback loop (due to a +/// difference between `hovered` and `contains_pointer` caused by the interact radius). +#[test] +fn tooltip_covering_button_should_not_cause_feedback_loop() { + let mut harness = Harness::builder().with_size((200.0, 30.0)).build_ui(|ui| { + ui.button("A tooltip should be shown") + .on_hover_text("This tooltip is larger than the button"); + }); + + harness.hover_at( + harness + .get_by_label("A tooltip should be shown") + .rect() + .left_center() + - Vec2::X, + ); + + harness.run(); + + harness.snapshot("tooltip_covering_button_should_not_cause_feedback_loop"); +} + +/// Tests that a tooltip closes when the pointer moves onto a neighboring widget, +/// so that the neighbor can show its own tooltip. +/// +/// The two buttons are only `item_spacing.y` (3 pt) apart, which is less than the +/// hit-test `interact_radius` (5 pt), so the first button is still close enough to +/// interact with when the pointer is on the second one. +#[test] +fn tooltip_should_hand_over_to_neighboring_widget() { + let mut harness = Harness::builder().with_size((300.0, 200.0)).build_ui(|ui| { + ui.button("Button A").on_hover_text("Tooltip A"); + ui.button("Button B").on_hover_text("Tooltip B"); + }); + + let a_rect = harness.get_by_label("Button A").rect(); + let b_rect = harness.get_by_label("Button B").rect(); + + harness.hover_at(a_rect.center_bottom() - Vec2::Y); + harness.run(); + assert!( + harness.query_by_label("Tooltip A").is_some(), + "Tooltip A should be shown when hovering Button A" + ); + + harness.hover_at(b_rect.center_top() + Vec2::Y); + harness.run(); + assert!( + harness.query_by_label("Tooltip B").is_some(), + "Tooltip B should be shown when hovering Button B" + ); + assert!( + harness.query_by_label("Tooltip A").is_none(), + "Tooltip A should be hidden when hovering Button B" + ); +} diff --git a/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png b/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png new file mode 100644 index 000000000..9f489aa81 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/test_tooltip_hover_regression.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59bef0e3593896985988c03171c710d8ed31ed8bb89c7a3f63c060c573e4fb74 +size 6510 diff --git a/tests/egui_tests/tests/snapshots/tooltip_covering_button_should_not_cause_feedback_loop.png b/tests/egui_tests/tests/snapshots/tooltip_covering_button_should_not_cause_feedback_loop.png new file mode 100644 index 000000000..53f8ff24d --- /dev/null +++ b/tests/egui_tests/tests/snapshots/tooltip_covering_button_should_not_cause_feedback_loop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed2f80921ab864b0df3303834a8ab943a3aa5e10456896991c2dca81be0c4968 +size 3968 From b06f5fea09a472e4b676721406d43d7b0059a1c9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 31 Jul 2026 14:21:17 -0700 Subject: [PATCH 05/49] Remove `clip_rect_margin` (#8366) * Closes https://github.com/emilk/egui/issues/5605 It has been zero by default for a few months now, and I do not wish to support it. It was always an ugly hack, and it is no longer needed. --- crates/egui/src/containers/resize.rs | 8 ++++---- crates/egui/src/containers/scroll_area.rs | 11 +++-------- crates/egui/src/style.rs | 10 ---------- crates/egui_demo_lib/src/demo/scrolling.rs | 6 ++---- crates/egui_extras/src/layout.rs | 5 +---- tests/test_viewports/src/main.rs | 8 ++------ 6 files changed, 12 insertions(+), 36 deletions(-) diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index b6c086aca..80a40454f 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -289,16 +289,16 @@ impl Resize { Rect::from_min_size(position, state.desired_size) }; - let mut content_clip_rect = inner_rect.expand(ui.visuals().clip_rect_margin); + let mut content_clip_rect = inner_rect; // If we pull the resize handle to shrink, we want to TRY to shrink it. // After laying out the contents, we might be much bigger. // In those cases we don't want the clip_rect to be smaller, because // then we will clip the contents of the region even thought the result gets larger. This is simply ugly! // So we use the memory of last_content_size to make the clip rect large enough. - content_clip_rect.max = content_clip_rect.max.max( - inner_rect.min + state.last_content_size + Vec2::splat(ui.visuals().clip_rect_margin), - ); + content_clip_rect.max = content_clip_rect + .max + .max(inner_rect.min + state.last_content_size); content_clip_rect = content_clip_rect.intersect(ui.clip_rect()); // Respect parent region diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 0fa4bba0c..e59f7e01d 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -810,12 +810,11 @@ impl ScrollArea { { // Clip the content, but only when we really need to: - let clip_rect_margin = ui.visuals().clip_rect_margin; let mut content_clip_rect = ui.clip_rect(); for d in 0..2 { if direction_enabled[d] { - content_clip_rect.min[d] = inner_rect.min[d] - clip_rect_margin; - content_clip_rect.max[d] = inner_rect.max[d] + clip_rect_margin; + content_clip_rect.min[d] = inner_rect.min[d]; + content_clip_rect.max[d] = inner_rect.max[d]; } else { // Nice handling of forced resizing beyond the possible: content_clip_rect.max[d] = ui.clip_rect().max[d] - current_bar_use[d]; @@ -1306,8 +1305,6 @@ impl Prepared { // * When one ScrollArea is nested inside another, and the outer // is scrolled so that the scroll-bars of the inner ScrollArea (us) // is outside the clip rectangle. - // Really this should use the tighter clip_rect that ignores clip_rect_margin, but we don't store that. - // clip_rect_margin is quite a hack. It would be nice to get rid of it. max_cross = ui.clip_rect().max[1 - d] - outer_margin; } @@ -1575,9 +1572,7 @@ fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset: let overflow = content_size - inner_rect.size(); - let paint_rect = inner_rect - .intersect(ui.min_rect()) - .expand(ui.visuals().clip_rect_margin); + let paint_rect = inner_rect.intersect(ui.min_rect()); // Top fade: animate opacity based on how far we've scrolled down. if 0.0 < offset.y { diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 27be5920b..a6f30d765 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1074,12 +1074,6 @@ pub struct Visuals { /// How the text cursor acts. pub text_cursor: TextCursorStyle, - /// Allow widgets to paint this much outside the scroll area rect. - /// - /// Legacy. Should not be used anymore. - /// Use [`crate::ScrollArea::content_margin`] instead. - pub clip_rect_margin: f32, - /// Show a background behind buttons. pub button_frame: bool, @@ -1534,7 +1528,6 @@ impl Visuals { text_cursor: Default::default(), - clip_rect_margin: 0.0, button_frame: true, collapsing_header_frame: false, indent_has_left_vline: true, @@ -2297,7 +2290,6 @@ impl Visuals { text_cursor, - clip_rect_margin, button_frame, collapsing_header_frame, indent_has_left_vline, @@ -2484,8 +2476,6 @@ impl Visuals { ui.collapsing("Misc", |ui| { ui.add(Slider::new(resize_corner_size, 0.0..=20.0).text("resize_corner_size")); - ui.add(Slider::new(clip_rect_margin, 0.0..=20.0).text("clip_rect_margin")); - ui.checkbox(button_frame, "Button has a frame"); ui.checkbox(collapsing_header_frame, "Collapsing header has a frame"); ui.checkbox( diff --git a/crates/egui_demo_lib/src/demo/scrolling.rs b/crates/egui_demo_lib/src/demo/scrolling.rs index cee525aaa..d9eb71927 100644 --- a/crates/egui_demo_lib/src/demo/scrolling.rs +++ b/crates/egui_demo_lib/src/demo/scrolling.rs @@ -340,10 +340,8 @@ impl crate::View for ScrollTo { ui.scroll_to_cursor(Some(Align::BOTTOM)); } - let margin = ui.visuals().clip_rect_margin; - - let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin; - let max_scroll = ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin; + let current_scroll = ui.clip_rect().top() - ui.min_rect().top(); + let max_scroll = ui.min_rect().height() - ui.clip_rect().height(); (current_scroll, max_scroll) }) .inner; diff --git a/crates/egui_extras/src/layout.rs b/crates/egui_extras/src/layout.rs index 594763daf..66d401a69 100644 --- a/crates/egui_extras/src/layout.rs +++ b/crates/egui_extras/src/layout.rs @@ -217,10 +217,7 @@ impl<'l> StripLayout<'l> { let mut child_ui = self.ui.new_child(ui_builder); if flags.clip { - let margin = egui::Vec2::splat(self.ui.visuals().clip_rect_margin); - let margin = margin.min(0.5 * self.ui.spacing().item_spacing); - let clip_rect = max_rect.expand2(margin); - child_ui.shrink_clip_rect(clip_rect); + child_ui.shrink_clip_rect(max_rect); if !child_ui.is_sizing_pass() { // Better to truncate (if we can), rather than hard clipping: diff --git a/tests/test_viewports/src/main.rs b/tests/test_viewports/src/main.rs index 62d357fa8..8dc00d7d9 100644 --- a/tests/test_viewports/src/main.rs +++ b/tests/test_viewports/src/main.rs @@ -451,17 +451,13 @@ fn drop_target( ) -> egui::InnerResponse { let is_being_dragged = ui.ctx().dragged_id().is_some(); - let margin = egui::Vec2::splat(ui.visuals().clip_rect_margin); // 3.0 - let background_id = ui.painter().add(egui::Shape::Noop); let available_rect = ui.available_rect_before_wrap(); - let inner_rect = available_rect.shrink2(margin); - let mut content_ui = ui.new_child(UiBuilder::new().max_rect(inner_rect)); + let mut content_ui = ui.new_child(UiBuilder::new().max_rect(available_rect)); let ret = body(&mut content_ui); - let outer_rect = - egui::Rect::from_min_max(available_rect.min, content_ui.min_rect().max + margin); + let outer_rect = egui::Rect::from_min_max(available_rect.min, content_ui.min_rect().max); let (rect, response) = ui.allocate_at_least(outer_rect.size(), egui::Sense::hover()); let style = if is_being_dragged && response.hovered() { From 4471969a16a130bc07f65eb747d5e2c3bfcf1d88 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 1 Aug 2026 03:13:52 -0700 Subject: [PATCH 06/49] Panels: Take separator line width into account (#8367) This fixes a small styling bug: the width of the panel's resize-handle-line was not included in the outer width of the panel. Now it is. --- crates/egui/src/containers/panel.rs | 23 +++++++++++++++++-- .../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/Panels.png | 4 ++-- .../tests/snapshots/demos/Tooltips.png | 2 +- .../panel_drag/between_collapsed.png | 2 +- .../panel_drag/between_initial_expanded.png | 2 +- .../snapshots/panel_drag/between_reopened.png | 2 +- .../snapshots/panel_drag/inside_initial.png | 4 ++-- 11 files changed, 37 insertions(+), 18 deletions(-) diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index 1b8e9f320..a5f57f5db 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -859,8 +859,27 @@ impl Panel { /// The configured [`Frame`], or the default side/top panel frame for this [`Ui`]. fn resolve_frame(&self, ui: &Ui) -> Frame { - self.frame - .unwrap_or_else(|| Frame::side_top_panel(ui.style())) + let mut frame = self + .frame + .unwrap_or_else(|| Frame::side_top_panel(ui.style())); + + let has_separator_line = self.show_separator_line || self.resizable; + + if has_separator_line { + // The separator line has a thickness that we need to account for. + let widgets = &ui.style().visuals.widgets; + let stroke_width = widgets.noninteractive.bg_stroke.width.round() as i8; + + let margin_side = match self.side { + PanelSide::Left => &mut frame.inner_margin.right, + PanelSide::Right => &mut frame.inner_margin.left, + PanelSide::Top => &mut frame.inner_margin.bottom, + PanelSide::Bottom => &mut frame.inner_margin.top, + }; + *margin_side = (*margin_side).saturating_add(stroke_width); + } + + frame } /// Panel is fully closed. If the user is still dragging the resize handle diff --git a/crates/egui_demo_app/tests/snapshots/clock.png b/crates/egui_demo_app/tests/snapshots/clock.png index 347bdd568..87c4b5374 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:288e11a1fa684575155826a760d5aecc5855e1f4b68bc8954441bf3ac015ee84 -size 335175 +oid sha256:dda3ad81551d0001bb9d3614a266010e44c8c18700964d8734c3e825672d9383 +size 334759 diff --git a/crates/egui_demo_app/tests/snapshots/custom3d.png b/crates/egui_demo_app/tests/snapshots/custom3d.png index 6778d8b92..fc393aa9e 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:d674918c635bfc865043f2123c0f5d4a671dd21ba7b878c056e817b19f2e8f00 -size 92770 +oid sha256:9035687ecd3aae0d8639000f091592d69acec0e73e7ba17e04c9794c5321cb1e +size 92771 diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index 81485c09a..7f908cbea 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:1f066e712888a57b5c5ca6ccd6e138c933ab04acc44a3fce5912cfe47852c672 -size 168875 +oid sha256:418f9bc7eb13dd32ce2bba8e4b8003b095dd00aa8c30f1640ea0f94f87a085eb +size 168430 diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index 80725c619..7381f2737 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:1cf4c34af7b69cd8220b11ff7e355ddf8d7b52a43a60a1748abf9cc1d5c7da9b -size 98869 +oid sha256:8d278d0d879e8cb44c4c2e65bdee4441616abcf3507adc37959ccf042991b31c +size 98780 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Panels.png b/crates/egui_demo_lib/tests/snapshots/demos/Panels.png index 14fa4884e..7e6beeac1 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:8d48079f85e9529f4b463bbaf2c948a64126388ef32df0584b586dc0ae48a35b -size 344919 +oid sha256:d20f4bf882ee16abde6268fe766ceb67dccdfc5d2a509a952cef1d7af72dc3c6 +size 344141 diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png b/crates/egui_demo_lib/tests/snapshots/demos/Tooltips.png index a3fe390e6..0fdb9277d 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:3e44cef45d27ddd64b0d2f6de0b7005846147bce54a9b8b1223ba9a0a02c2416 +oid sha256:125d6fe0cdc81a0c7973317e62c528c1beff7ceaedb9cb1fb91bc8d6674c8dc6 size 62830 diff --git a/tests/egui_tests/tests/snapshots/panel_drag/between_collapsed.png b/tests/egui_tests/tests/snapshots/panel_drag/between_collapsed.png index cb3b393e6..6c824bd91 100644 --- a/tests/egui_tests/tests/snapshots/panel_drag/between_collapsed.png +++ b/tests/egui_tests/tests/snapshots/panel_drag/between_collapsed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eba6e690937fbbd22c8edfce14078f50998e968324d8073d5db5829493d957e0 +oid sha256:9a21708315ae4514c1ff2c71075ccce2bef4627720808e4eebc9d7922388a17f size 5292 diff --git a/tests/egui_tests/tests/snapshots/panel_drag/between_initial_expanded.png b/tests/egui_tests/tests/snapshots/panel_drag/between_initial_expanded.png index 06679d16d..6310e8309 100644 --- a/tests/egui_tests/tests/snapshots/panel_drag/between_initial_expanded.png +++ b/tests/egui_tests/tests/snapshots/panel_drag/between_initial_expanded.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84e6030760561e308a190d2eb9781f53b896fbea5d11a3548b73d68c49f4d525 +oid sha256:a0c8d12d6f3741d33b993b5390fba5b845778a1f4dcf6d661513e65243978e18 size 65797 diff --git a/tests/egui_tests/tests/snapshots/panel_drag/between_reopened.png b/tests/egui_tests/tests/snapshots/panel_drag/between_reopened.png index 06679d16d..6310e8309 100644 --- a/tests/egui_tests/tests/snapshots/panel_drag/between_reopened.png +++ b/tests/egui_tests/tests/snapshots/panel_drag/between_reopened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84e6030760561e308a190d2eb9781f53b896fbea5d11a3548b73d68c49f4d525 +oid sha256:a0c8d12d6f3741d33b993b5390fba5b845778a1f4dcf6d661513e65243978e18 size 65797 diff --git a/tests/egui_tests/tests/snapshots/panel_drag/inside_initial.png b/tests/egui_tests/tests/snapshots/panel_drag/inside_initial.png index 8a9963284..b25aa5581 100644 --- a/tests/egui_tests/tests/snapshots/panel_drag/inside_initial.png +++ b/tests/egui_tests/tests/snapshots/panel_drag/inside_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf09470a6628e62421bfa754ce238dd13820ba0bf8146ae835e539874d39a150 -size 4882 +oid sha256:054c9005865ff2d6c1ee6c76539453fbad6005d1c62693de5653c4bb00164322 +size 4881 From 7fd54ef741d55232f7ab107eec293b3bc894a278 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Aug 2026 12:22:59 -0700 Subject: [PATCH 07/49] Add `BoxedWidget`: dynamically dispatched widgets (#8378) ## Summary - Add `BoxedWidget` and `Widget::boxed` for heterogeneous widget collections. --- crates/egui/src/widgets/mod.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/egui/src/widgets/mod.rs b/crates/egui/src/widgets/mod.rs index 21346d64a..ea2e17046 100644 --- a/crates/egui/src/widgets/mod.rs +++ b/crates/egui/src/widgets/mod.rs @@ -6,6 +6,12 @@ use crate::{Response, Ui}; +/// A dynamically dispatched [`Widget`]. +/// +/// [`Widget`] is not dyn compatible because [`Widget::ui`] takes `self` by value. +/// This alias uses a closure, which implements [`Widget`]. +pub type BoxedWidget<'a> = Box Response + 'a>; + mod button; mod checkbox; pub mod color_picker; @@ -63,6 +69,20 @@ pub trait Widget { /// /// Tip: you can `impl Widget for &mut YourObject { }`. fn ui(self, ui: &mut Ui) -> Response; + + /// Box this widget for dynamic dispatch. + #[inline] + fn boxed<'a>(self) -> BoxedWidget<'a> + where + Self: Sized + 'a, + { + Box::new(move |ui: &mut Ui| ui.add(self)) + } +} + +#[test] +fn widgets_can_be_boxed() { + let _: BoxedWidget<'static> = Button::new("boxed").boxed(); } /// This enables functions that return `impl Widget`, so that you can From 7f30623cff45d91f4fd025aa25bd472442e49e71 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Aug 2026 12:25:32 -0700 Subject: [PATCH 08/49] Add `WidgetText::size` (#8377) ## Summary - Add `WidgetText::size` for sizing plain, rich, and layout-job text uniformly. - Preserve already-laid-out galleys. ## Test - `cargo clippy -p egui --all-features --all-targets` - `cargo test -p egui --all-features` * [x] I have followed the instructions in the PR template --- crates/egui/src/widget_text.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 8670398fa..37018c0b2 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -558,6 +558,25 @@ impl Default for WidgetText { } impl WidgetText { + /// Override the font size. + /// + /// For [`Self::Galley`], this does nothing because it has already been laid out. + #[must_use] + pub fn size(self, size: f32) -> Self { + match self { + Self::Text(text) => RichText::new(text).size(size).into(), + Self::RichText(text) => Self::RichText(Arc::new(Arc::unwrap_or_clone(text).size(size))), + Self::LayoutJob(job) => { + let mut job = Arc::unwrap_or_clone(job); + for section in &mut job.sections { + section.format.font_id.size = size; + } + Self::LayoutJob(Arc::new(job)) + } + Self::Galley(galley) => Self::Galley(galley), + } + } + #[inline] pub fn is_empty(&self) -> bool { match self { From cb2b306f1157aebb7dc47a5809f58318aeaa0e59 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Aug 2026 12:26:27 -0700 Subject: [PATCH 09/49] Add `LayoutJob::clear` (#8376) ## Summary - Add `LayoutJob::clear` to reuse layout settings while rebuilding text. - Cover preservation of every layout setting. ## Test - `cargo clippy -p epaint --all-features --all-targets` - `cargo test -p epaint --all-features` * [x] I have followed the instructions in the PR template --- crates/epaint/src/text/text_layout_types.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 5ae784468..97af735b4 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -114,6 +114,13 @@ impl Default for LayoutJob { } impl LayoutJob { + /// Clear the text and sections while preserving the layout settings. + #[inline] + pub fn clear(&mut self) { + self.text.clear(); + self.sections.clear(); + } + /// Break on `\n` and at the given wrap width. #[inline] pub fn simple(text: String, font_id: FontId, color: Color32, wrap_width: f32) -> Self { From 65109a0da04c3f136f1b501c2ea6b4111595cc15 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sun, 2 Aug 2026 23:38:37 -0700 Subject: [PATCH 10/49] Update crates (#8379) Routine dependency update. Updated: `font-types` 0.12, `harfrust` 0.12, `jiff` 0.2.35, `open` 5.4, `pollster` 1.0, `rand` 0.10.2, `self_cell` 1.3, `skrifa` 0.44, `tokio` 1.53, `toml` 1.1, `vello_cpu` 0.1. --- Cargo.lock | 239 ++++++++++++++------------ Cargo.toml | 39 ++--- crates/epaint/src/text/font.rs | 2 +- crates/epaint/src/text/text_layout.rs | 2 +- 4 files changed, 149 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ba363c26..c07d28369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,7 +173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" dependencies = [ "android-properties", - "bitflags 2.13.0", + "bitflags 2.13.1", "cc", "jni", "libc", @@ -575,9 +575,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] @@ -632,18 +632,18 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", @@ -674,7 +674,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "log", "polling", "rustix 0.38.44", @@ -702,9 +702,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.66" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -1194,7 +1194,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1278,7 +1278,7 @@ dependencies = [ "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", - "pollster", + "pollster 1.0.1", "profiling", "raw-window-handle", "ron", @@ -1300,7 +1300,7 @@ dependencies = [ "accesskit", "ahash", "backtrace", - "bitflags 2.13.0", + "bitflags 2.13.1", "document-features", "emath", "epaint", @@ -1466,7 +1466,7 @@ dependencies = [ "kittest", "log", "open", - "pollster", + "pollster 1.0.1", "serde", "tempfile", "toml", @@ -1783,9 +1783,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "font-types" -version = "0.11.3" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +checksum = "0a7299a780854a6d391be2ae1c8521c9368471b559dbfd6a8dbd9f407eaff100" dependencies = [ "bytemuck", "serde", @@ -1859,9 +1859,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-io" @@ -1884,9 +1884,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", @@ -1895,15 +1895,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", "futures-macro", @@ -1995,9 +1995,9 @@ dependencies = [ [[package]] name = "glifo" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99fc21d493812643aae86d53b7bbd02f376434a90317e8a790bc209fdd6605e" +checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" dependencies = [ "bytemuck", "foldhash", @@ -2027,7 +2027,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg_aliases", "cgl", "dispatch2", @@ -2124,11 +2124,11 @@ dependencies = [ [[package]] name = "harfrust" -version = "0.7.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0431e8e389aa0f1e72bb9d1c2db8957a1a7a3580e8ed97db819c14837aac9b3e" +checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytemuck", "read-fonts", "smallvec", @@ -2446,11 +2446,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.31" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", + "jiff-core", "jiff-static", "js-sys", "log", @@ -2462,11 +2463,21 @@ dependencies = [ ] [[package]] -name = "jiff-static" -version = "0.2.31" +name = "jiff-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", "proc-macro2", "quote", "syn", @@ -2623,9 +2634,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -2658,7 +2669,7 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "libc", "plain", "redox_syscall 0.9.0", @@ -2723,9 +2734,9 @@ checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -2815,7 +2826,7 @@ checksum = "23bf0a141a9ab6f07dbb492db53245e464bc9db42f407772d9ae03d83a2c1033" dependencies = [ "arrayvec", "bit-set 0.10.0", - "bitflags 2.13.0", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -2851,7 +2862,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -2950,7 +2961,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -2966,7 +2977,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -2980,7 +2991,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3004,7 +3015,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3016,7 +3027,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", ] @@ -3027,7 +3038,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -3070,7 +3081,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "dispatch", "libc", @@ -3083,7 +3094,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", @@ -3095,7 +3106,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -3118,7 +3129,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3130,7 +3141,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -3142,7 +3153,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3155,7 +3166,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -3179,7 +3190,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-cloud-kit", @@ -3200,7 +3211,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -3223,7 +3234,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", @@ -3259,9 +3270,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "open" -version = "5.3.6" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" dependencies = [ "is-wsl", "libc", @@ -3577,6 +3588,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" +[[package]] +name = "pollster" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6355899e1c9462875b6757c79f3caa011a1fdae12bbb1a2e72dd1f234f8336" + [[package]] name = "polycool" version = "0.4.0" @@ -3650,9 +3667,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3745,9 +3762,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3857,12 +3874,13 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.39.2" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" dependencies = [ "bytemuck", "font-types", + "once_cell", ] [[package]] @@ -3880,7 +3898,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3889,7 +3907,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -3968,7 +3986,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "percent-encoding", - "pollster", + "pollster 0.4.0", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -4027,7 +4045,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "once_cell", "serde", "serde_derive", @@ -4074,7 +4092,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4087,7 +4105,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -4141,7 +4159,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytemuck", "core_maths", "log", @@ -4198,9 +4216,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -4308,9 +4326,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" @@ -4365,9 +4383,9 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "skrifa" -version = "0.42.1" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" dependencies = [ "bytemuck", "read-fonts", @@ -4403,7 +4421,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "calloop", "calloop-wayland-source", "cursor-icon", @@ -4458,7 +4476,7 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", ] [[package]] @@ -4500,9 +4518,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -4741,9 +4759,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -4756,9 +4774,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "libc", "mio", @@ -4769,9 +4787,9 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "serde_core", "serde_spanned", @@ -4803,9 +4821,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] @@ -5075,14 +5093,13 @@ dependencies = [ [[package]] name = "vello_common" -version = "0.0.9" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d672facaa2d697285a786cd9d44d614cd2ce54cdc022504bf339f8fff3b750" +checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" dependencies = [ "bytemuck", "fearless_simd", "guillotiere", - "hashbrown 0.17.1", "log", "peniko", "smallvec", @@ -5091,9 +5108,9 @@ dependencies = [ [[package]] name = "vello_cpu" -version = "0.0.9" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "588691169aed86b5c8fb487266afee01323234e6fd0a3f2aaec0eaa8e4007f23" +checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" dependencies = [ "bytemuck", "glifo", @@ -5207,7 +5224,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "rustix 1.1.4", "wayland-backend", "wayland-scanner", @@ -5219,7 +5236,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "cursor-icon", "wayland-backend", ] @@ -5241,7 +5258,7 @@ version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5253,7 +5270,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5266,7 +5283,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -5354,7 +5371,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d8f4bd44d92da5270f03409dba9f952dab24f128e05d6a554926101d1bf9114" dependencies = [ "arrayvec", - "bitflags 2.13.0", + "bitflags 2.13.1", "bytemuck", "cfg-if", "cfg_aliases", @@ -5386,7 +5403,7 @@ dependencies = [ "arrayvec", "bit-set 0.10.0", "bit-vec 0.9.1", - "bitflags 2.13.0", + "bitflags 2.13.1", "bytemuck", "cfg_aliases", "document-features", @@ -5458,7 +5475,7 @@ dependencies = [ "arrayvec", "ash", "bit-set 0.10.0", - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.6.2", "bytemuck", "cfg-if", @@ -5520,7 +5537,7 @@ version = "30.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a9c93c2b35edde326df60ffdee4c0f5864eac3011d6768b70d43f028ad93565" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "bytemuck", "js-sys", "log", @@ -5762,7 +5779,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.13.0", + "bitflags 2.13.1", "block2 0.5.1", "bytemuck", "calloop", @@ -5870,7 +5887,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.13.0", + "bitflags 2.13.1", "dlib", "log", "once_cell", @@ -6030,18 +6047,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 6c8566cf0..5a4193dad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ eframe = { version = "0.35.0", path = "crates/eframe", default-features = false accesskit = "0.24.1" accesskit_consumer = "0.35.0" # Can't update to 0.36+: kittest 0.4 pins accesskit_consumer 0.35, so bumping splits it into two versions -accesskit_winit = "0.32.0" # Can't update to 0.33: it needs accesskit_macos 0.26.2, which pulls accesskit_consumer 0.37, duplicating the 0.35 that kittest 0.4 needs +accesskit_winit = "0.32.0" # Can't update to 0.33: it needs accesskit_macos 0.26.2, which pulls accesskit_consumer 0.37, duplicating the 0.35 that kittest 0.4 needs. For the same reason, `accesskit_macos` is held at 0.26.0 in `Cargo.lock`. ahash = { version = "0.8.12", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead "std", @@ -89,19 +89,17 @@ dify = { version = "0.8.0", default-features = false } directories = "6.0" document-features = "0.2.12" ehttp = { version = "0.7.1", default-features = false } -enum-map = "2.7" -env_logger = { version = "0.11.8", default-features = false } # 0.11.9+ pulls env_filter 1.x, duplicating the 0.1.x that android_logger needs -font-types = { version = "0.11.3", default-features = false, features = [ - "std", -] } # Can't update to 0.12: vello_cpu's glifo 0.1.1 pins font-types 0.11 (via skrifa/read-fonts), so bumping splits it into two versions -glow = "0.17.0" +enum-map = "2.7" # Can't update to 3.1: its `enum-map-derive` moved to syn 3, duplicating the syn 2 that most other proc-macro crates still use +env_logger = { version = "0.11.8", default-features = false } # 0.11.9+ pulls env_filter 1.x/2.x, duplicating the 0.1.x that android_logger needs +font-types = { version = "0.12.2", default-features = false, features = ["std"] } +glow = "0.17.0" # Can't update to 0.18: wgpu 30's wgpu-hal pins glow 0.17, so bumping splits it into two versions glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } -harfrust = "0.7.0" # Can't update to 0.8+: newer versions need read-fonts 0.40+/font-types 0.12, but vello_cpu's glifo 0.1.1 pins read-fonts 0.39/font-types 0.11, so bumping duplicates them +harfrust = "0.12.0" home = "0.5.12" image = { version = "0.25.6", default-features = false } # Can't update to 0.25.7+: it needs png 0.18, which only matches resvg once resvg moves to tiny-skia 0.12 — blocked, see resvg below itertools = "0.15.0" -jiff = { version = "0.2.29", default-features = false } +jiff = { version = "0.2.35", default-features = false } js-sys = "0.3.103" kittest = { version = "0.4.0" } log = { version = "0.4.33", features = ["std"] } @@ -114,42 +112,43 @@ 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" +open = "5.4" parking_lot = "0.12.5" percent-encoding = "2.3" poll-promise = { version = "0.3.0", default-features = false } -pollster = "0.4.0" +pollster = "1.0" profiling = { version = "1.0", default-features = false } puffin = "0.20.0" puffin_http = "0.17.0" -rand = "0.10.1" +rand = "0.10.2" raw-window-handle = "0.6.2" rayon = "1.12" -resvg = { version = "0.45.1", default-features = false } # Can't update to 0.47: it needs tiny-skia 0.12, but winit 0.30's sctk-adwaita is stuck on tiny-skia 0.11, so bumping duplicates tiny-skia. (0.46 keeps tiny-skia 0.11 but its fontconfig-parser duplicates roxmltree.) +resvg = { version = "0.45.1", default-features = false } # Can't update to 0.47+: it needs tiny-skia 0.12, but winit 0.30's sctk-adwaita 0.10 is stuck on tiny-skia 0.11, so bumping duplicates tiny-skia. (0.46 keeps tiny-skia 0.11 but its fontconfig-parser duplicates roxmltree.) rfd = "0.17.2" rmp-serde = "1.3" ron = "0.12.2" -self_cell = "1.2" +self_cell = "1.3" +# `serde_derive` is held at 1.0.228 in `Cargo.lock`: 1.0.229 moved to syn 3, duplicating the syn 2 that most other proc-macro crates still use serde = { version = "1.0", features = ["derive"] } serde_bytes = "0.11.19" similar-asserts = "2.0" -skrifa = { version = "0.42.1", default-features = false, features = [ +skrifa = { version = "0.44.0", default-features = false, features = [ "std", "autohint_shaping", -] } # Can't update to 0.43: vello_cpu's glifo 0.1.1 pins skrifa 0.42, so bumping splits it into two versions +] } # Can't update to 0.45: it needs read-fonts 0.42, but harfrust 0.12 pins read-fonts 0.41, so bumping splits read-fonts into two versions smallvec = "1.15" smithay-clipboard = "0.7.2" # 0.7.3 pulls smithay-client-toolkit 0.20 (calloop 0.14), duplicating the 0.19/0.13 that winit 0.30 needs static_assertions = "1.1" syntect = { version = "5.3", default-features = false } tempfile = "3.27" -thiserror = "2.0" -tokio = "1.52" -toml = { version = "1.0", default-features = false } +thiserror = "2.0" # Held at 2.0.18 in `Cargo.lock`: `thiserror-impl` 2.0.19 moved to syn 3, duplicating the syn 2 that most other proc-macro crates still use +tokio = "1.53" +toml = { version = "1.1", default-features = false } type-map = "0.5.1" unicode_names2 = { version = "3.1", default-features = false } unicode-general-category = "1.1" unicode-segmentation = "1.13" -vello_cpu = { version = "0.0.9", default-features = false, features = [ +vello_cpu = { version = "0.1.0", default-features = false, features = [ "std", "u8_pipeline", "f32_pipeline", diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index fad657e6e..12885596e 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -276,7 +276,7 @@ impl FontCell { ctx.fill_path(&path); let mut dest = vello_cpu::Pixmap::new(width, height); let mut resources = vello_cpu::Resources::new(); - ctx.render_to_pixmap(&mut resources, &mut dest); + ctx.render(&mut dest, &mut resources); let glyph_pos = { let color_transfer_function = atlas.options().color_transfer_function; diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index ffe187e6c..442f1f86c 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1430,7 +1430,7 @@ fn shape_text( buffer.push_str(text); buffer.guess_segment_properties(); - shaper.shape(buffer, &[]) + shaper.shape(buffer, harfrust::ShapeOptions::new()) } // ---------------------------------------------------------------------------- From a80ed6bab7ac4a70342d9330e39b68199ae7caaa Mon Sep 17 00:00:00 2001 From: Calin P <50150016+wyvernbw@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:00:02 +0300 Subject: [PATCH 11/49] Eframe: make webbrowser dependency optional (#8372) * Closes #8371 * [x] I have followed the instructions in the PR template Adds a `link` feature to eframe to allow disabling links on egui-winit. The feature is enabled by default so nothing changes for existing users of eframe. --- crates/eframe/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 2e21fcc19..60b796bc5 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -28,6 +28,7 @@ workspace = true default = [ "accesskit", "default_fonts", + "links", "wayland", # Required for Linux support (including CI!) "web_screen_reader", "wgpu", @@ -122,6 +123,9 @@ __screenshot = [] ## and capture screenshots. Off unless the env var is set; no-op on wasm. inspection = ["dep:egui_inspection", "accesskit"] +## Enables the `links` feature on `egui-winit`, allowing for links to open in browser. +links = ["egui-winit/links"] + [dependencies] egui = { workspace = true, default-features = false, features = ["bytemuck"] } @@ -145,7 +149,7 @@ serde = { workspace = true, optional = true } # ------------------------------------------- # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -egui-winit = { workspace = true, default-features = false, features = ["clipboard", "links"] } +egui-winit = { workspace = true, default-features = false, features = ["clipboard"] } image = { workspace = true, features = ["png"] } # Needed for app icon winit = { workspace = true, default-features = false, features = ["rwh_06"] } From 998b413739b352220c5ee4f44fd102af39e36a6d Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 3 Aug 2026 09:40:29 +0200 Subject: [PATCH 12/49] Sync window theme with egui theme (#8299) Adds a new option to sync the window theme with the egui theme, enabled by default. Works across viewports. https://github.com/user-attachments/assets/513c2318-cd6e-4e2b-805d-04002c375a10 --------- Co-authored-by: Claude Opus 4.8 (1M context) --- crates/egui/src/context.rs | 40 +++++++++++++++++++++++++++++++++++ crates/egui/src/memory/mod.rs | 16 ++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index e0705b201..dbb714bed 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -243,6 +243,12 @@ pub struct ViewportState { // ---------------------- // Cross-frame statistics: pub num_multipass_in_row: usize, + + /// The last theme we sent to the native window via [`ViewportCommand::SetTheme`], + /// used to avoid sending redundant commands. + /// + /// See [`crate::Options::sync_window_theme`]. + pub(crate) last_sent_window_theme: Option, } /// What called [`Context::request_repaint`] or [`Context::request_discard`]? @@ -2393,6 +2399,8 @@ impl Context { } } + self.sync_window_theme(); + #[cfg(debug_assertions)] self.debug_painting(); @@ -2404,6 +2412,38 @@ impl Context { output } + /// Keep the native window theme in sync with the egui [`crate::ThemePreference`], + /// if [`crate::Options::sync_window_theme`] is enabled. + /// + /// Sends a [`ViewportCommand::SetTheme`] to the current viewport whenever the + /// derived theme changes, so the native window decorations match the egui theme. + fn sync_window_theme(&self) { + if !self.options(|o| o.sync_window_theme) { + return; + } + + use crate::{SystemTheme, ThemePreference}; + let window_theme = match self.options(|o| o.theme_preference) { + ThemePreference::System => SystemTheme::SystemDefault, + ThemePreference::Dark => SystemTheme::Dark, + ThemePreference::Light => SystemTheme::Light, + }; + + let changed = self.write(|ctx| { + let viewport = ctx.viewport(); + if viewport.last_sent_window_theme == Some(window_theme) { + false + } else { + viewport.last_sent_window_theme = Some(window_theme); + true + } + }); + + if changed { + self.send_viewport_cmd(ViewportCommand::SetTheme(window_theme)); + } + } + /// Called at the end of the pass. #[cfg(debug_assertions)] fn debug_painting(&self) { diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 8fd407cc9..d963373b4 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -216,6 +216,18 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) system_theme: Option, + /// If `true`, egui will keep the native window theme in sync with + /// [`Self::theme_preference`] by sending a [`crate::ViewportCommand::SetTheme`] + /// to the root viewport whenever the preference changes. + /// + /// This makes the native window decorations (title bar, borders, …) match the + /// theme selected inside egui. + /// + /// Set this to `false` if you want to manage the native window theme yourself. + /// + /// This is `true` by default. + pub sync_window_theme: bool, + /// Global zoom factor of the UI. /// /// This is used to calculate the `pixels_per_point` @@ -318,6 +330,7 @@ impl Default for Options { theme_preference: Default::default(), fallback_theme: Theme::Dark, system_theme: None, + sync_window_theme: true, zoom_factor: 1.0, zoom_with_keyboard: true, quit_shortcuts: vec![crate::KeyboardShortcut::new( @@ -381,6 +394,7 @@ impl Options { theme_preference, fallback_theme: _, system_theme: _, + sync_window_theme, zoom_factor, zoom_with_keyboard, quit_shortcuts: _, // not shown in ui @@ -429,6 +443,8 @@ impl Options { .show(ui, |ui| { theme_preference.radio_buttons(ui); + ui.checkbox(sync_window_theme, "Sync window theme with egui theme"); + let style = std::sync::Arc::make_mut(match theme { Theme::Dark => dark_style, Theme::Light => light_style, From 967aa1137a95b59fd0341f773fa38b24c34d86fc Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Aug 2026 00:57:35 -0700 Subject: [PATCH 13/49] Re-add `Visuals::clip_rect_margin` as a deprecated no-op (#8380) Follow-up to #8366, which removed `Visuals::clip_rect_margin` outright Co-authored-by: Claude Opus 5 (1M context) --- crates/egui/src/style.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index a6f30d765..faa65b49e 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1074,6 +1074,14 @@ pub struct Visuals { /// How the text cursor acts. pub text_cursor: TextCursorStyle, + /// Unused. Kept only for backwards compatibility. + /// + /// Used to allow widgets to paint this much outside the scroll area rect. + /// Setting it now has no effect. + /// Use [`crate::ScrollArea::content_margin`] instead. + #[deprecated(note = "This is now unused and has no effect")] + pub clip_rect_margin: f32, + /// Show a background behind buttons. pub button_frame: bool, @@ -1481,6 +1489,7 @@ impl Default for Interaction { impl Visuals { /// Default dark theme. + #[expect(deprecated)] pub fn dark() -> Self { Self { dark_mode: true, @@ -1528,6 +1537,7 @@ impl Visuals { text_cursor: Default::default(), + clip_rect_margin: 0.0, button_frame: true, collapsing_header_frame: false, indent_has_left_vline: true, @@ -2256,6 +2266,7 @@ impl WidgetVisuals { } impl Visuals { + #[expect(deprecated)] pub fn ui(&mut self, ui: &mut crate::Ui) { let Self { dark_mode, @@ -2290,6 +2301,7 @@ impl Visuals { text_cursor, + clip_rect_margin: _, button_frame, collapsing_header_frame, indent_has_left_vline, From c676d939ca61668358cd7bd41c33c79c193595a2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Aug 2026 01:08:09 -0700 Subject: [PATCH 14/49] Report failing pixels by threshold when a kittest snapshot fails (#8360) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an image snapshot fails, you get the number of pixels differing by more than the `threshold` you happened to configure — which doesn't tell you what threshold *would* have passed. So picking `SnapshotOptions::threshold` / `failed_pixel_count_threshold` is trial and error, one CI round-trip per guess. This measures the failing pixel count at a sweep of thresholds (new public `THRESHOLD_SWEEP`) and includes it in `SnapshotError::Diff`: ``` 'sweep_demo' Image did not match snapshot. Diff: 293, …/sweep_demo.diff.png. Failing pixels by threshold: 0.0: 1522, 0.1: 1522, 0.2: 293, 0.4: 293, 0.6: 293, 1.0: 293, … Run `UPDATE_SNAPSHOTS=1 cargo test --all-features` to update the snapshots. ``` The sweep only runs for snapshots that already failed, so passing tests are unaffected. Breaking: `SnapshotError::Diff` gained a `failing_pixels_by_threshold` field. * [x] I have followed the instructions in the PR template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) --- crates/egui_kittest/src/snapshot.rs | 65 +++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 703ebe3f2..268e0749f 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -214,6 +214,14 @@ pub enum SnapshotError { /// Path where the diff image was saved diff_path: PathBuf, + + /// How many pixels would have failed at other per-pixel thresholds. + /// + /// Measured at [`THRESHOLD_SWEEP`], lowest threshold first. + /// Use this to pick a [`SnapshotOptions::threshold`] and a + /// [`SnapshotOptions::failed_pixel_count_threshold`] from measurements, + /// instead of by trial and error. + failing_pixels_by_threshold: Vec<(f32, i32)>, }, /// Error opening the existing snapshot (it probably doesn't exist, check the @@ -264,14 +272,24 @@ impl Display for SnapshotError { name, diff, diff_path, + failing_pixels_by_threshold, } => { let diff_path = std::path::absolute(diff_path).unwrap_or_else(|_| diff_path.clone()); write!( f, - "'{name}' Image did not match snapshot. Diff: {diff}, {}. {HOW_TO_UPDATE_SCREENSHOTS}", + "'{name}' Image did not match snapshot. Diff: {diff}, {}.", diff_path.display() - ) + )?; + if !failing_pixels_by_threshold.is_empty() { + let sweep = failing_pixels_by_threshold + .iter() + .map(|(threshold, count)| format!("{threshold:.1}: {count}")) + .collect::>() + .join(", "); + write!(f, "\n Failing pixels by threshold: {sweep}")?; + } + write!(f, "\n {HOW_TO_UPDATE_SCREENSHOTS}") } Self::OpenSnapshot { path, err } => { let path = std::path::absolute(path).unwrap_or_else(|_| path.clone()); @@ -380,6 +398,37 @@ pub fn try_image_snapshot_options( try_image_snapshot_options_impl(new, name.into(), options) } +/// The per-pixel thresholds that a failing snapshot is measured against, +/// to help you pick a [`SnapshotOptions::threshold`]. +/// +/// Same unit as [`SnapshotOptions::threshold`]. +pub const THRESHOLD_SWEEP: &[f32] = &[0.0, 0.1, 0.2, 0.4, 0.6, 1.0, 1.5, 2.0, 3.0, 5.0, 10.0]; + +/// How many pixels differ by more than each of [`THRESHOLD_SWEEP`]? +/// +/// Only called for failing snapshots, so the extra comparisons don't slow down passing tests. +fn failing_pixels_by_threshold( + previous: &image::RgbaImage, + new: &image::RgbaImage, +) -> Vec<(f32, i32)> { + THRESHOLD_SWEEP + .iter() + .map(|&threshold| { + let num_wrong_pixels = dify::diff::get_results( + previous.clone(), + new.clone(), + threshold, + true, + None, + &None, + &None, + ) + .map_or(0, |(num_wrong_pixels, _diff_image)| num_wrong_pixels); + (threshold, num_wrong_pixels) + }) + .collect() +} + fn try_image_snapshot_options_impl( new: &image::RgbaImage, name: String, @@ -481,8 +530,15 @@ fn try_image_snapshot_options_impl( *threshold }; - let result = - dify::diff::get_results(previous, new.clone(), threshold, true, None, &None, &None); + let result = dify::diff::get_results( + previous.clone(), + new.clone(), + threshold, + true, + None, + &None, + &None, + ); let Some((num_wrong_pixels, diff_image)) = result else { return Ok(()); // Difference below threshold @@ -510,6 +566,7 @@ fn try_image_snapshot_options_impl( name, diff: num_wrong_pixels, diff_path, + failing_pixels_by_threshold: failing_pixels_by_threshold(&previous, new), }) } } From dcd0c72d53695068e9ba2627c726aaa9b6c3ae2c Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Mon, 3 Aug 2026 01:55:18 -0700 Subject: [PATCH 15/49] Fix TextEdit hint text not following horizontal_align/vertical_align (#8332) ## Summary - Closes #8309 - [x] I have followed the instructions in the PR template `TextEdit` hint text was always aligned to `Align2::LEFT_TOP`, ignoring the alignment set via `TextEdit::horizontal_align` / `vertical_align`. This caused the hint text, the cursor, and the typed text to disagree on alignment: e.g. a centered `TextEdit` showed a left-aligned hint with a centered cursor. The hint text atoms now use the widget's `align`, so the hint matches the input text alignment. The default `align` is still `LEFT_TOP`, so multi line text edits (and the default styling) are unchanged. ### Root cause In `crates/egui/src/widgets/text_edit/builder.rs`, the hint-text branch hardcoded: ```rust atoms.push_right(atom.atom_align(Align2::LEFT_TOP)); ``` while the input-text branch used `.atom_align(self.align)`. The hint path now uses `align` as well. ### Drive-by: silence `clippy::unnecessary_wraps` in `egui_kittest::app_kind` `AppKind::run` returns `Option`. The `Option` wrap is required when the `eframe` feature is enabled (the `Eframe` branch returns `None`), but `clippy::unnecessary_wraps` fires when `egui_kittest` is built standalone without the `eframe` feature (e.g. `cargo clippy -p egui_kittest`). The workspace CI run doesn't hit it because feature unification via `egui_demo_app` enables `eframe`, but it's a real annoyance for anyone linting the crate on its own. Added a scoped `#[cfg_attr(not(feature = "eframe"), expect(clippy::unnecessary_wraps))]` with an explanatory comment. ## Test plan - [x] Added `textedit_hint_text_should_follow_text_alignment` kittest regression in `crates/egui_kittest/tests/regression_tests.rs`. It fails before the fix (`hint_center_x=24.25` vs `edit_center_x=100`) and passes after. - [x] `cargo test -p egui` - [x] `cargo test -p egui_kittest --all-features --test regression_tests` - [x] `cargo clippy -p egui_kittest --all-features --test regression_tests -- -D warnings` - [x] `RUSTFLAGS="-D warnings" cargo clippy -p egui_kittest --lib` (pre-existing `unnecessary_wraps` now silenced) - [x] `cargo clippy -p egui -- -D warnings` - [x] `cargo fmt --check` --------- Co-authored-by: Lucas Meurer --- crates/egui/src/widgets/text_edit/builder.rs | 7 +-- crates/egui_kittest/src/app_kind.rs | 4 ++ crates/egui_kittest/tests/regression_tests.rs | 46 +++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 0a52d636c..aa4f18d8a 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -622,9 +622,10 @@ impl TextEdit<'_> { 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)); + // Align the hint text the same as the input text so the hint, the + // cursor, and the typed text all share one alignment. The default + // `align` is `LEFT_TOP`, which keeps multi line text edits unchanged. + atoms.push_right(atom.atom_align(align)); } // Calculate the empty galley, so it can be read later. The available width is diff --git a/crates/egui_kittest/src/app_kind.rs b/crates/egui_kittest/src/app_kind.rs index 942ec4b85..9e92192bb 100644 --- a/crates/egui_kittest/src/app_kind.rs +++ b/crates/egui_kittest/src/app_kind.rs @@ -23,6 +23,10 @@ pub(crate) enum AppKind<'a, State> { } impl AppKind<'_, State> { + // The `Option` is needed when the `eframe` feature is enabled, because the + // `Eframe` variant has no `egui::Response` to return. Without `eframe` the + // wrap is unnecessary, so we silence `clippy::unnecessary_wraps` for that case. + #[cfg_attr(not(feature = "eframe"), expect(clippy::unnecessary_wraps))] pub fn run( &mut self, ui: &mut egui::Ui, diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index ba39a909b..459e2f024 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -713,3 +713,49 @@ fn collapsing_panel_must_not_grow_enclosing_window() { ); } } + +/// The hint text of a `TextEdit` should follow the same alignment as the input +/// text, instead of always being left-top aligned. +/// +/// Regression test for . +#[test] +pub fn textedit_hint_text_should_follow_text_alignment() { + let mut input = String::new(); + + let mut harness = Harness::builder() + .with_size(Vec2::new(200.0, 40.0)) + .build_ui(|ui| { + ui.add( + egui::TextEdit::singleline(&mut input) + .hint_text("Hint") + .desired_width(200.0) + .horizontal_align(egui::Align::Center), + ); + }); + harness.run(); + + let text_edit = harness.get_by_role(accesskit::Role::TextInput); + let edit_rect = text_edit.rect(); + + // Find the hint text shape (the only text shape while the input is empty). + let hint_shape = harness + .output() + .shapes + .iter() + .find_map(|clipped| { + let egui::epaint::Shape::Text(text_shape) = &clipped.shape else { + return None; + }; + (text_shape.galley.text() == "Hint").then_some(text_shape) + }) + .expect("hint text shape should be painted"); + + let hint_center_x = hint_shape.pos.x + hint_shape.galley.size().x / 2.0; + let edit_center_x = edit_rect.center().x; + + assert!( + (hint_center_x - edit_center_x).abs() < 1.0, + "hint text should be centered in the TextEdit: hint_center_x={hint_center_x}, \ + edit_center_x={edit_center_x}, edit_rect={edit_rect:?}", + ); +} From ddec5f3e4ce2142dfbc95f3216d9851605682963 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Aug 2026 03:32:56 -0700 Subject: [PATCH 16/49] Fix where `Panel` puts its separator line, and how much room it reserves (#8382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to the separator line of `Panel` (`resolve_frame` was added in #8367): * **Reserve room only when the line is always drawn.** Before, `show_separator_line || resizable` reserved the line's thickness, so a resizable panel that opted out still got a permanently visible gap along its inner edge — space held for a line only drawn transiently, while hovering or dragging the resize handle. * **Paint the line outside the frame's outline**, in room reserved in `Frame::outer_margin` rather than `inner_margin`, so going outwards from the panel contents you get: `contents | inner_margin | stroke | separator line | outer_margin` Previously the line landed on top of the frame's outline (or outside its outer margin). Default panels — no stroke, no outer margin — are unchanged pixel-wise. Found in the Rerun viewer: the time panel is `.resizable(true).show_separator_line(false)` and draws its own top line, so the extra 1pt landed above the top bar's buttons, making them look 1pt too low. Tests in `tests/egui_tests/tests/test_panel_separator_line.rs`, both spanning `show_separator_line` on/off × resize handle hovered/not: snapshots of a top panel with a garish outline, plus a pixel probe across the inner edge of a panel on each of the four sides. Both fail on `main`. * [x] I have followed the instructions in the PR template 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/egui/src/containers/panel.rs | 62 ++++++++--- .../separator_off_hovered.png | 3 + .../separator_off_idle.png | 3 + .../separator_on_hovered.png | 3 + .../separator_on_idle.png | 3 + .../tests/test_panel_separator_line.rs | 105 ++++++++++++++++++ 6 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png create mode 100644 tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png create mode 100644 tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png create mode 100644 tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png create mode 100644 tests/egui_tests/tests/test_panel_separator_line.rs diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index a5f57f5db..e4954b6cd 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -18,8 +18,8 @@ use emath::GuiRounding as _; use crate::{ - Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, NumExt as _, Rangef, Rect, - Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, + Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, Margin, NumExt as _, Rangef, + Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, }; fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 { @@ -126,6 +126,22 @@ impl PanelSide { } } + /// The component of `margin` on the panel's _resizable_ edge, + /// i.e. the edge facing the rest of the ui, where the separator line goes. + fn resize_margin(self, mut margin: Margin) -> i8 { + *self.resize_margin_mut(&mut margin) + } + + /// Mutable version of [`Self::resize_margin`]. + fn resize_margin_mut(self, margin: &mut Margin) -> &mut i8 { + match self { + Self::Left => &mut margin.right, + Self::Right => &mut margin.left, + Self::Top => &mut margin.bottom, + Self::Bottom => &mut margin.top, + } + } + /// Resize by keeping `self` side fixed, and moving the opposite side. fn set_rect_size(self, rect: &mut Rect, size: f32) { match self { @@ -298,6 +314,19 @@ impl Panel { /// Show a separator line, even when not interacting with it? /// + /// The separator line sits on the panel's inner edge, i.e. the edge facing the rest of the ui. + /// It is painted _outside_ the [`Frame`]'s outline, in room the panel reserves for it in the + /// frame's [`Frame::outer_margin`], so that going from the panel contents outwards you get: + /// + /// contents | [`Frame::inner_margin`] | [`Frame::stroke`] | separator line | [`Frame::outer_margin`] + /// + /// Turning this off removes that reserved room too, so the panel gets no permanent gap along + /// that edge. + /// + /// A `resizable` panel still shows a line while hovered or dragged, regardless of this setting. + /// With this setting off there is no room reserved for it, so that transient line is painted + /// just outside the frame's outline, overlapping the [`Frame::outer_margin`]. + /// /// Default: `true`. #[inline] pub fn show_separator_line(mut self, show_separator_line: bool) -> Self { @@ -845,7 +874,14 @@ impl Panel { Stroke::NONE }; // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done - let line_pos = side.resize_pos(shifted_outer_rect) + 0.5 * side.sign() * stroke.width; + + // The line goes just _outside_ the frame's outline, in the room `resolve_frame` + // reserved for it in the outer margin, i.e.: + // + // contents | `inner_margin` | outline | separator line | `outer_margin` + let outer_margin = f32::from(side.resize_margin(frame.outer_margin)); + let outline_edge = side.resize_pos(shifted_outer_rect) + side.sign() * outer_margin; + let line_pos = outline_edge - 0.5 * side.sign() * stroke.width; let cross_range = shifted_outer_rect.range_along(side.cross_axis()); if axis == 0 { parent_ui.painter().vline(line_pos, cross_range, stroke); @@ -863,19 +899,19 @@ impl Panel { .frame .unwrap_or_else(|| Frame::side_top_panel(ui.style())); - let has_separator_line = self.show_separator_line || self.resizable; - - if has_separator_line { - // The separator line has a thickness that we need to account for. + if self.show_separator_line { + // Reserve room for the separator line in the frame's _outer_ margin, so the line + // lands just outside the frame's outline instead of painting on top of it: + // + // contents | `inner_margin` | outline | separator line | `outer_margin` + // + // We deliberately don't do this for a `resizable` panel that has opted out of the + // separator line: the line it shows while hovered/dragged is a transient affordance, + // and reserving room for it would leave a permanently visible gap. let widgets = &ui.style().visuals.widgets; let stroke_width = widgets.noninteractive.bg_stroke.width.round() as i8; - let margin_side = match self.side { - PanelSide::Left => &mut frame.inner_margin.right, - PanelSide::Right => &mut frame.inner_margin.left, - PanelSide::Top => &mut frame.inner_margin.bottom, - PanelSide::Bottom => &mut frame.inner_margin.top, - }; + let margin_side = self.side.resize_margin_mut(&mut frame.outer_margin); *margin_side = (*margin_side).saturating_add(stroke_width); } diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png new file mode 100644 index 000000000..bb3745c59 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_hovered.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbe8eda2e6162220ff8baa6e84156ea18f0bb15ffc46ebf9d60b6d3eeb8861d4 +size 9016 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png new file mode 100644 index 000000000..8a690c1a5 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_off_idle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:547316973b563bd92841f428838848323d656688cd03e8babcbc24626a389419 +size 7195 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png new file mode 100644 index 000000000..43e51aa02 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_hovered.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b18774a40c1c1065645be7b59c90e65bb6fb40d4931ab37ccf40126e2767b709 +size 9019 diff --git a/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png new file mode 100644 index 000000000..3c92145ab --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_separator_line/separator_on_idle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e9ffcb9f14f86155a4a8279d694ae3e43c6bb8df59fa681673199c01ff68f96 +size 7206 diff --git a/tests/egui_tests/tests/test_panel_separator_line.rs b/tests/egui_tests/tests/test_panel_separator_line.rs new file mode 100644 index 000000000..96afb509f --- /dev/null +++ b/tests/egui_tests/tests/test_panel_separator_line.rs @@ -0,0 +1,105 @@ +//! Snapshot tests for where a [`Panel`] puts its separator line, and how much room it reserves. +//! +//! Going outwards from the panel contents, the order is: +//! +//! contents | `Frame::inner_margin` | `Frame::stroke` | separator line | `Frame::outer_margin` +//! +//! i.e. the line is painted _outside_ the frame's outline, in room the panel reserves for it in the +//! frame's outer margin. A panel that opted out of the separator line must not reserve that room, +//! or it ends up with a permanently visible gap along that edge — even though it is `resizable` and +//! therefore still shows a line while hovered or dragged. +//! +//! The snapshots span `show_separator_line` on/off × resize handle hovered/not. The panel uses a +//! garish frame outline and separator colors so both are unmistakable, and its only content is a +//! [`egui::SelectableLabel`] vertically centered in the panel: if the panel reserves room it +//! shouldn't, the label drifts off center. + +use egui::{Color32, CornerRadius, Frame, Margin, Panel, Pos2, Stroke, Vec2}; +use egui_kittest::{Harness, SnapshotResults}; + +/// [`Frame::fill`] of the test panel. +const FILL: Color32 = Color32::from_rgb(20, 20, 40); + +/// [`Frame::stroke`] color of the test panel. +const OUTLINE: Color32 = Color32::from_rgb(255, 0, 255); + +/// The dim, always-visible separator line (`noninteractive.bg_stroke`). +const SEPARATOR: Color32 = Color32::from_rgb(0, 255, 0); + +/// The bright separator line shown while the resize handle is hovered (`hovered.fg_stroke`). +const HOVERED_SEPARATOR: Color32 = Color32::from_rgb(255, 255, 0); + +const PANEL_ID: &str = "test_panel"; + +fn build_harness(show_separator_line: bool) -> Harness<'static> { + let mut harness = Harness::builder() + .with_size(Vec2::new(200.0, 120.0)) + // So the thin lines are legible to a human reviewing the snapshots: + .with_pixels_per_point(2.0) + .build_ui(move |ui| { + // Loud, distinguishable colors, so we can tell the separator line, the frame outline + // and the frame fill apart. + let widgets = &mut ui.visuals_mut().widgets; + widgets.noninteractive.bg_stroke = Stroke::new(1.0, SEPARATOR); + widgets.hovered.fg_stroke = Stroke::new(1.0, HOVERED_SEPARATOR); + + let frame = Frame::new() + .fill(FILL) + .stroke(Stroke::new(2.0, OUTLINE)) + .corner_radius(CornerRadius::ZERO) + .inner_margin(Margin::same(4)) + .outer_margin(Margin::same(2)); + + Panel::top(PANEL_ID) + .frame(frame) + .resizable(true) + .default_size(60.0) + .show_separator_line(show_separator_line) + .show(ui, |ui| { + // Vertically centered in whatever room the panel gave us. + ui.horizontal_centered(|ui| { + let _ = ui.selectable_label(true, "Centered"); + }); + }); + + egui::CentralPanel::default() + .frame(Frame::default().fill(Color32::GRAY)) + .show(ui, |ui| { + ui.label("CentralPanel"); + }); + }); + harness.run(); + harness +} + +fn hover_resize_handle(harness: &mut Harness<'_>) { + let outer = egui::PanelState::load(&harness.ctx, egui::Id::new(PANEL_ID)) + .expect("PanelState should be persisted after the first frame") + .outer_rect; + + // Hover just _inside_ the panel's inner edge, but still well within the resize grab radius: + // the `CentralPanel` and its label start exactly at that edge, and would otherwise take the + // hover from the resize handle. + harness.hover_at(Pos2::new(outer.center().x, outer.bottom() - 1.0)); + harness.run(); +} + +#[test] +fn separator_line_matrix() { + let mut results = SnapshotResults::new(); + + for show_separator_line in [false, true] { + let suffix = if show_separator_line { "on" } else { "off" }; + + // Not hovered: the line is dim (`show_separator_line`) or absent. + let mut harness = build_harness(show_separator_line); + results.add(harness.try_snapshot(format!("panel_separator_line/separator_{suffix}_idle"))); + + // Hovered: a `resizable` panel shows a bright line regardless of `show_separator_line`, + // and must not shift its contents to make room for it. + let mut harness = build_harness(show_separator_line); + hover_resize_handle(&mut harness); + results + .add(harness.try_snapshot(format!("panel_separator_line/separator_{suffix}_hovered"))); + } +} From 3d70aa1123e395046fc468f45e097bdac017ebb4 Mon Sep 17 00:00:00 2001 From: oleflb <45100017+oleflb@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:53:28 +0200 Subject: [PATCH 17/49] Make wgpu Instance public (#8321) --- crates/egui-wgpu/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index d48198ed7..195167177 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -115,6 +115,9 @@ pub struct RenderState { #[cfg(not(target_arch = "wasm32"))] pub available_adapters: Vec, + /// Wgpu instance used for creating surfaces and adapters. + pub instance: wgpu::Instance, + /// Wgpu device used for rendering, created from the adapter. pub device: wgpu::Device, @@ -218,7 +221,7 @@ impl RenderState { instance.enumerate_adapters(backends).await }; - let (adapter, device, queue) = match config.wgpu_setup.clone() { + let (instance, adapter, device, queue) = match config.wgpu_setup.clone() { WgpuSetup::CreateNew(WgpuSetupCreateNew { instance_descriptor: _, display_handle: _, @@ -253,14 +256,14 @@ impl RenderState { .await? }; - (adapter, device, queue) + (instance.clone(), adapter, device, queue) } WgpuSetup::Existing(WgpuSetupExisting { - instance: _, + instance, adapter, device, queue, - }) => (adapter, device, queue), + }) => (instance, adapter, device, queue), }; log_adapter_info(&adapter.get_info()); @@ -280,6 +283,7 @@ impl RenderState { // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint. #[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm Ok(Self { + instance, adapter, #[cfg(not(target_arch = "wasm32"))] available_adapters, From eba2780dbafd2d0a422f93d54c4a3589817f23c3 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Aug 2026 04:58:01 -0700 Subject: [PATCH 18/49] Fix `egui_kittest` failing to compile without the `wgpu` feature (#8381) Co-authored-by: Claude Opus 5 (1M context) --- crates/egui_kittest/src/renderer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui_kittest/src/renderer.rs b/crates/egui_kittest/src/renderer.rs index 3bffb3844..4abcf31c4 100644 --- a/crates/egui_kittest/src/renderer.rs +++ b/crates/egui_kittest/src/renderer.rs @@ -40,7 +40,7 @@ impl Default for LazyRenderer { return Self::new(crate::wgpu::WgpuTestRenderer::new); #[cfg(not(feature = "wgpu"))] return Self::Uninitialized { - textures_delta: Vec::new(), + textures_delta: Default::default(), builder: None, }; } From fa608a1b40ec9a0683fd9c272828bc10105e164e Mon Sep 17 00:00:00 2001 From: n4n5 Date: Mon, 3 Aug 2026 14:15:07 +0200 Subject: [PATCH 19/49] Add `egui::Window::title_frame` (#8353) * [X] I have followed the instructions in the PR template Add a way to set the frame for the content and for the title of the window - `self.frame` will be used for the margins of the body - `self.title_frame` will be used for the margins of the header (title) --- crates/egui/src/containers/window.rs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index cf6c58f88..b39a6ec3c 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -84,6 +84,7 @@ pub struct Window<'a> { open: Option<&'a mut bool>, area: Area, frame: Option, + title_frame: Option, resize: Resize, scroll: ScrollArea, collapsible: bool, @@ -106,6 +107,7 @@ impl<'a> Window<'a> { open: None, area, frame: None, + title_frame: None, resize: Resize::default() .with_stroke(false) .min_size([96.0, 32.0]) @@ -265,6 +267,13 @@ impl<'a> Window<'a> { self } + /// Change the background color, margins, etc. of the title + #[inline] + pub fn title_frame(mut self, frame: Frame) -> Self { + self.title_frame = Some(frame); + self + } + /// Set minimum width of the window. #[inline] pub fn min_width(mut self, min_width: f32) -> Self { @@ -549,6 +558,7 @@ impl Window<'_> { mut open, area, frame, + title_frame, resize, scroll, collapsible, @@ -616,10 +626,12 @@ impl Window<'_> { let style = ctx.global_style(); + // We get or create the Frame for the title and content + let window_title_frame = title_frame.unwrap_or_else(|| Frame::window(&style)); let window_frame = frame.unwrap_or_else(|| Frame::window(&style)); // We apply the window margin by using the `ScrollArea::content_margin`. - let window_margin = window_frame.inner_margin; + let window_content_margin = window_frame.inner_margin; let window_frame = window_frame.inner_margin(0.0); let is_explicitly_closed = matches!(open, Some(false)); @@ -711,7 +723,7 @@ impl Window<'_> { title_ui( ui, title, - window_frame.inner_margin(window_margin), + window_title_frame, &mut collapsing, collapsible, on_top, @@ -725,12 +737,12 @@ impl Window<'_> { .show_body_unindented(ui, |ui| { if scroll.is_any_scroll_enabled() { scroll - .content_margin(window_margin) + .content_margin(window_content_margin) .show(ui, add_contents) .inner } else { crate::Frame::NONE - .inner_margin(window_margin) + .inner_margin(window_content_margin) .show(ui, add_contents) .inner } From 5f75aa29d3a1f7ab0e18380851591dc307b8442b Mon Sep 17 00:00:00 2001 From: Calbabreaker Date: Mon, 3 Aug 2026 22:29:11 +1000 Subject: [PATCH 20/49] Remove dependency `home` (#8307) Replaces `home::home_dir` with `std::env::home_dir` as these functions do the exact same thing Removes dependency home from eframe by replacing `home::home_dir` with `std::env::home_dir`. `home` was probably originally used since `std::env::home_dir` was once deprecated because of a bug. Post Rust version 1.87 this has been fixed and now these two functions do exactly the same thing. * [X] I have followed the instructions in the PR template Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 10 ---------- Cargo.toml | 1 - crates/eframe/Cargo.toml | 3 +-- crates/eframe/src/native/file_storage.rs | 4 ++-- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c07d28369..2bab448c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1269,7 +1269,6 @@ dependencies = [ "glow", "glutin", "glutin-winit", - "home", "image", "js-sys", "log", @@ -2204,15 +2203,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "1.4.2" diff --git a/Cargo.toml b/Cargo.toml index 5a4193dad..884f9c7de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,7 +96,6 @@ glow = "0.17.0" # Can't update to 0.18: wgpu 30's wgpu-hal pins glow 0.17, so bu glutin = { version = "0.32.3", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } harfrust = "0.12.0" -home = "0.5.12" image = { version = "0.25.6", default-features = false } # Can't update to 0.25.7+: it needs png 0.18, which only matches resvg once resvg moves to tiny-skia 0.12 — blocked, see resvg below itertools = "0.15.0" jiff = { version = "0.2.35", default-features = false } diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 60b796bc5..65e3150a8 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -61,7 +61,7 @@ default_fonts = ["egui/default_fonts"] glow = ["dep:egui_glow", "dep:glow", "dep:glutin-winit", "dep:glutin"] ## Enable saving app state to disk. -persistence = ["dep:home", "egui-winit/serde", "egui/persistence", "ron", "serde"] +persistence = ["egui-winit/serde", "egui/persistence", "ron", "serde"] ## Enables wayland support and fixes clipboard issue. ## @@ -165,7 +165,6 @@ glutin-winit = { workspace = true, optional = true, default-features = false, fe "egl", "wgl", ] } -home = { workspace = true, optional = true } # mac: [target.'cfg(any(target_os = "macos"))'.dependencies] diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 947554a00..830fdcc24 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -21,7 +21,7 @@ pub fn storage_dir(app_id: &str) -> Option { OS::Nix => var_os("XDG_DATA_HOME") .map(PathBuf::from) .filter(|p| p.is_absolute()) - .or_else(|| home::home_dir().map(|p| p.join(".local").join("share"))) + .or_else(|| std::env::home_dir().map(|p| p.join(".local").join("share"))) .map(|p| { p.join( app_id @@ -29,7 +29,7 @@ pub fn storage_dir(app_id: &str) -> Option { .replace(|c: char| c.is_ascii_whitespace(), ""), ) }), - OS::Mac => home::home_dir().map(|p| { + OS::Mac => std::env::home_dir().map(|p| { p.join("Library") .join("Application Support") .join(app_id.replace(|c: char| c.is_ascii_whitespace(), "-")) From ef846f53e67398e86374234872b9ccecd5879080 Mon Sep 17 00:00:00 2001 From: Sybrand Aarnoutse Date: Mon, 3 Aug 2026 14:37:09 +0200 Subject: [PATCH 21/49] Remove dependency on `memoffset` (#8304) Hi, I may or may not have used your crate but I'd like to say a quick thank you for it anyway! I'm going down the list of reverse dependencies on `memoffset`. This PR aims to remove the `memoffset` crate from your dependencies. [`core::mem::offset_of`](https://doc.rust-lang.org/core/mem/macro.offset_of.html) was stabilised in rustc 1.77 which I believe is at or below your MSRV. The `memoffset` crate 0.9.1 says that > If you're using a rustc version greater or equal to 1.77, > this crate's offset_of!() macro simply forwards to core::mem::offset_of!(). I consider it very unlikely (see [here](https://github.com/rust-lang/rust/issues/111839)) for any usage of the `offset_of!` macro to break but please check anyway. I hope we can all enjoy the benefits of one less dependency :) --- * [x] I have followed the instructions in the PR template *except for `./scripts/check.sh` which doesn't run in my environment* (I'm unwilling to chase it down because I'm firing off a whole bunch of these PRs to various repositories, sorry.) `cargo clippy` gives 1 unrelated warning. Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 1 - Cargo.toml | 1 - crates/egui_glow/Cargo.toml | 1 - crates/egui_glow/src/painter.rs | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2bab448c0..4afcaf461 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1433,7 +1433,6 @@ dependencies = [ "glutin", "glutin-winit", "log", - "memoffset", "profiling", "winit", ] diff --git a/Cargo.toml b/Cargo.toml index 884f9c7de..f310dd9af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,6 @@ jiff = { version = "0.2.35", default-features = false } js-sys = "0.3.103" kittest = { version = "0.4.0" } log = { version = "0.4.33", features = ["std"] } -memoffset = "0.9.1" mimalloc = "0.1.52" mime_guess2 = { version = "2.3", default-features = false } mint = "0.5.9" diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index d5a9f3716..2bd8c3109 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -56,7 +56,6 @@ egui-winit = { workspace = true, optional = true, default-features = false } bytemuck.workspace = true glow.workspace = true log.workspace = true -memoffset.workspace = true profiling.workspace = true #! ### Optional dependencies diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 004f5d58c..2b2341da1 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -1,6 +1,7 @@ #![expect(clippy::unwrap_used)] #![expect(unsafe_code)] +use core::mem::offset_of; use std::{collections::HashMap, sync::Arc}; use egui::{ @@ -8,7 +9,6 @@ use egui::{ epaint::{Mesh, PaintCallbackInfo, Primitive, Vertex}, }; use glow::HasContext as _; -use memoffset::offset_of; use crate::check_for_gl_error; use crate::misc_util::{compile_shader, link_program}; From 2e7a92bc3750bf6d9a1d455ea565b2c97259950e Mon Sep 17 00:00:00 2001 From: limo520 Date: Mon, 3 Aug 2026 20:51:17 +0800 Subject: [PATCH 22/49] Fix incorrect feature name in the code editor demo (#8330) Change the feature name from syntax_highlighting to syntect. * [x] I have followed the instructions in the PR template Co-authored-by: Emil Ernerfeldt --- crates/egui_demo_lib/src/demo/code_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui_demo_lib/src/demo/code_editor.rs b/crates/egui_demo_lib/src/demo/code_editor.rs index 869194114..c79cdfd47 100644 --- a/crates/egui_demo_lib/src/demo/code_editor.rs +++ b/crates/egui_demo_lib/src/demo/code_editor.rs @@ -61,7 +61,7 @@ impl crate::View for CodeEditor { ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing.x = 0.0; ui.label("Compile the demo with the "); - ui.code("syntax_highlighting"); + ui.code("syntect"); ui.label(" feature to enable more accurate syntax highlighting using "); ui.hyperlink_to("syntect", "https://github.com/trishume/syntect"); ui.label("."); From 49d4befe6b6198f5adcb7e2e6b413fa1f8de288c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jochen=20G=C3=B6rtler?= Date: Mon, 3 Aug 2026 17:07:37 +0200 Subject: [PATCH 23/49] Store `web_sys::File` inside of `DroppedFile` (#8354) * Closes #4654 * Related #4667 * [x] I have followed the instructions in the PR template This PR avoids materializing the contents of a file that was dragged into an egui application on the web. It does so by storing the `web_sys::File` handle directly on WASM. This breaks the existing API of `DroppedFile` on the web, because there is no way to retrieve the bytes synchronously form a `DroppedFile` anymore, forcing handling call sites to become asynchronous. The native API remains the same. --------- Co-authored-by: Emil Ernerfeldt --- Cargo.lock | 2 +- crates/eframe/Cargo.toml | 1 - crates/eframe/src/web/dropped_file.rs | 48 ++++++++++++++++ crates/eframe/src/web/events.rs | 65 +++++----------------- crates/eframe/src/web/mod.rs | 11 ++-- crates/egui-winit/src/dropped_file.rs | 22 ++++++++ crates/egui-winit/src/lib.rs | 10 ++-- crates/egui/Cargo.toml | 7 +++ crates/egui/src/data/input/dropped_file.rs | 60 +++++++++++++++----- crates/egui/src/data/input/mod.rs | 2 +- crates/egui/src/data/input/raw_input.rs | 17 +++++- crates/egui_demo_app/src/wrap_app.rs | 36 ++++++------ examples/file_dialog/src/main.rs | 36 ++++++------ 13 files changed, 197 insertions(+), 120 deletions(-) create mode 100644 crates/eframe/src/web/dropped_file.rs create mode 100644 crates/egui-winit/src/dropped_file.rs diff --git a/Cargo.lock b/Cargo.lock index 4afcaf461..adf431072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1284,7 +1284,6 @@ dependencies = [ "serde", "static_assertions", "wasm-bindgen", - "wasm-bindgen-futures", "web-sys", "web-time", "wgpu", @@ -1311,6 +1310,7 @@ dependencies = [ "serde", "smallvec", "unicode-segmentation", + "web-sys", ] [[package]] diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 65e3150a8..4b30ad18d 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -209,7 +209,6 @@ image = { workspace = true, features = ["png"] } # For copying images js-sys.workspace = true percent-encoding.workspace = true wasm-bindgen.workspace = true -wasm-bindgen-futures.workspace = true web-sys = { workspace = true, features = [ "AddEventListenerOptions", "BinaryType", diff --git a/crates/eframe/src/web/dropped_file.rs b/crates/eframe/src/web/dropped_file.rs new file mode 100644 index 000000000..45e454cc5 --- /dev/null +++ b/crates/eframe/src/web/dropped_file.rs @@ -0,0 +1,48 @@ +use std::{ + future::Future, + path::{Path, PathBuf}, + pin::Pin, +}; + +#[derive(Debug)] +pub(crate) struct WebFile { + file: web_sys::File, + // We store a `PathBuf` here so that we can hand out `Path`s + // without allocating each time. + path: PathBuf, +} + +impl From for WebFile { + fn from(file: web_sys::File) -> Self { + let path = file.name().into(); + Self { file, path } + } +} + +impl egui::DroppedFile for WebFile { + fn path(&self) -> &Path { + &self.path + } + + fn bytes_async(&self) -> Pin, String>> + '_>> { + let file = self.file.clone(); + Box::pin(async move { + if file.size() > f64::from(u32::MAX) { + return Err(format!( + "File is too large: browser file reads are limited to {} bytes", + u32::MAX + )); + } + + let array_buffer = file + .array_buffer() + .await + .map_err(|err| crate::web::string_from_js_value(&err))?; + Ok(js_sys::Uint8Array::new(&array_buffer).to_vec()) + }) + } + + fn web_file(&self) -> Option<&web_sys::File> { + Some(&self.file) + } +} diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index f9d992c2f..0303f0cdf 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -973,62 +973,25 @@ fn install_drag_and_drop(runner_ref: &WebRunner, target: &EventTarget) -> Result event.prevent_default(); })?; - runner_ref.add_event_listener(target, "drop", { - let runner_ref = runner_ref.clone(); + runner_ref.add_event_listener(target, "drop", |event: web_sys::DragEvent, runner| { + if let Some(data_transfer) = event.data_transfer() { + // TODO(https://github.com/emilk/egui/issues/3702): support dropping folders + runner.input.raw.hovered_files.clear(); + runner.needs_repaint.repaint_asap(); - move |event: web_sys::DragEvent, runner| { - if let Some(data_transfer) = event.data_transfer() { - // TODO(https://github.com/emilk/egui/issues/3702): support dropping folders - runner.input.raw.hovered_files.clear(); - runner.needs_repaint.repaint_asap(); + if let Some(files) = data_transfer.files() { + for i in 0..files.length() { + if let Some(file) = files.get(i) { + log::debug!("Dropped {:?} ({} bytes)", file.name(), file.size()); - if let Some(files) = data_transfer.files() { - for i in 0..files.length() { - if let Some(file) = files.get(i) { - let name = file.name(); - let mime = file.type_(); - let last_modified = std::time::UNIX_EPOCH - + std::time::Duration::from_millis(file.last_modified() as u64); - - log::debug!("Loading {:?} ({} bytes)…", name, file.size()); - - let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer()); - - let runner_ref = runner_ref.clone(); - let future = async move { - match future.await { - Ok(array_buffer) => { - let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec(); - log::debug!("Loaded {:?} ({} bytes).", name, bytes.len()); - - if let Some(mut runner_lock) = runner_ref.try_lock() { - runner_lock.input.raw.dropped_files.push( - egui::DroppedFile { - name, - mime, - last_modified: Some(last_modified), - bytes: Some(bytes.into()), - ..Default::default() - }, - ); - runner_lock.needs_repaint.repaint_asap(); - } - } - Err(err) => { - log::error!( - "Failed to read file: {}", - string_from_js_value(&err) - ); - } - } - }; - wasm_bindgen_futures::spawn_local(future); - } + runner.input.raw.dropped_files.push(std::sync::Arc::new( + super::dropped_file::WebFile::from(file), + )); } } - event.stop_propagation(); - event.prevent_default(); } + event.stop_propagation(); + event.prevent_default(); } })?; diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 67923987b..bf851d4ad 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -5,6 +5,7 @@ mod app_runner; mod backend; +mod dropped_file; mod events; mod input; mod panic_handler; @@ -207,13 +208,12 @@ fn set_clipboard_text(s: &str) { return; } let promise = window.navigator().clipboard().write_text(s); - let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { - if let Err(err) = future.await { + if let Err(err) = promise.await { log::error!("Copy/cut action failed: {}", string_from_js_value(&err)); } }; - wasm_bindgen_futures::spawn_local(future); + js_sys::futures::spawn_local(future); } } @@ -248,16 +248,15 @@ fn set_clipboard_image(image: &egui::ColorImage) { }; let items = js_sys::Array::of1(&item); let promise = window.navigator().clipboard().write(&items); - let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { - if let Err(err) = future.await { + if let Err(err) = promise.await { log::error!( "Copy/cut image action failed: {}", string_from_js_value(&err) ); } }; - wasm_bindgen_futures::spawn_local(future); + js_sys::futures::spawn_local(future); } } diff --git a/crates/egui-winit/src/dropped_file.rs b/crates/egui-winit/src/dropped_file.rs new file mode 100644 index 000000000..43f8960fc --- /dev/null +++ b/crates/egui-winit/src/dropped_file.rs @@ -0,0 +1,22 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub(crate) struct NativeFile { + path: PathBuf, +} + +impl From for NativeFile { + fn from(path: PathBuf) -> Self { + Self { path } + } +} + +impl egui::DroppedFile for NativeFile { + fn path(&self) -> &Path { + &self.path + } + + fn bytes(&self) -> Result, String> { + std::fs::read(&self.path).map_err(|err| err.to_string()) + } +} diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 85b22a997..0029a47b7 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -21,6 +21,7 @@ use egui::{Pos2, Rect, Theme, Vec2, ViewportBuilder, ViewportCommand, ViewportId pub use winit; pub mod clipboard; +mod dropped_file; mod safe_area; mod window_settings; @@ -28,6 +29,8 @@ pub use window_settings::WindowSettings; use raw_window_handle::HasDisplayHandle; +use dropped_file::NativeFile; + use winit::{ dpi::{PhysicalPosition, PhysicalSize}, event::ElementState, @@ -470,10 +473,9 @@ impl State { } WindowEvent::DroppedFile(path) => { self.egui_input.hovered_files.clear(); - self.egui_input.dropped_files.push(egui::DroppedFile { - path: Some(path.clone()), - ..Default::default() - }); + self.egui_input + .dropped_files + .push(std::sync::Arc::new(NativeFile::from(path.clone()))); EventResponse { repaint: true, consumed: false, diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index fadc27c6c..4aee66384 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -19,6 +19,7 @@ workspace = true [package.metadata.docs.rs] all-features = true rustdoc-args = ["--generate-link-to-definition"] +targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [lib] @@ -90,3 +91,9 @@ document-features = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } + + +# web: +[target.'cfg(target_arch = "wasm32")'.dependencies] +# For `DroppedFile`, which hands web apps a file handle instead of its contents. +web-sys = { workspace = true, features = ["File"] } diff --git a/crates/egui/src/data/input/dropped_file.rs b/crates/egui/src/data/input/dropped_file.rs index 39faceba8..0ca617fa0 100644 --- a/crates/egui/src/data/input/dropped_file.rs +++ b/crates/egui/src/data/input/dropped_file.rs @@ -1,19 +1,51 @@ +use std::{path::Path, sync::Arc}; + +#[cfg(target_arch = "wasm32")] +use std::{future::Future, pin::Pin}; + /// A file dropped into egui. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct DroppedFile { - /// Set by the `egui-winit` backend. - pub path: Option, +/// +/// The integration owns the concrete file handle, letting egui remain independent of windowing +/// backends and file APIs. +pub trait DroppedFile: std::fmt::Debug { + /// The path of the dropped file. + /// + /// This is an absolute path on native platforms. On the web, it is a relative path containing + /// only the file name because browsers do not expose the file's local path. + fn path(&self) -> &Path; - /// Name of the file. Set by the `eframe` web backend. - pub name: String, + /// Read the file contents. + /// + /// This is asynchronous because browsers can only read files asynchronously. + /// + /// # Errors + /// + /// Returns an error if the browser cannot read the file. + #[cfg(target_arch = "wasm32")] + fn bytes_async(&self) -> Pin, String>> + '_>>; - /// With the `eframe` web backend, this is set to the mime-type of the file (if available). - pub mime: String, + /// Read the file contents. + /// + /// # Errors + /// + /// Returns an error if the file cannot be read. + #[cfg(not(target_arch = "wasm32"))] + fn bytes(&self) -> Result, String>; - /// Set by the `eframe` web backend. - pub last_modified: Option, - - /// Set by the `eframe` web backend. - pub bytes: Option>, + /// The browser file handle, if this file was dropped on the web. + #[cfg(target_arch = "wasm32")] + fn web_file(&self) -> Option<&web_sys::File> { + None + } } + +/// A shared reference to a dropped file. +#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] +pub type DroppedFileHandle = Arc; + +/// A shared reference to a dropped file. +/// +/// This is not necessarily `Send + Sync` when wasm threads are enabled, because +/// [`web_sys::File`] is not thread-safe in that configuration. +#[cfg(all(target_arch = "wasm32", target_feature = "atomics"))] +pub type DroppedFileHandle = Arc; diff --git a/crates/egui/src/data/input/mod.rs b/crates/egui/src/data/input/mod.rs index b4e739471..3f2060b06 100644 --- a/crates/egui/src/data/input/mod.rs +++ b/crates/egui/src/data/input/mod.rs @@ -16,7 +16,7 @@ mod touch; mod viewport_info; pub use self::{ - dropped_file::DroppedFile, + dropped_file::{DroppedFile, DroppedFileHandle}, event::Event, event_filter::EventFilter, hovered_file::HoveredFile, diff --git a/crates/egui/src/data/input/raw_input.rs b/crates/egui/src/data/input/raw_input.rs index b9fc6e66a..7135e90e0 100644 --- a/crates/egui/src/data/input/raw_input.rs +++ b/crates/egui/src/data/input/raw_input.rs @@ -1,6 +1,6 @@ use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect}; -use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo}; +use super::{DroppedFileHandle, Event, HoveredFile, SafeAreaInsets, ViewportInfo}; /// What the integrations provides to egui at the start of each frame. /// @@ -13,7 +13,7 @@ use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo}; /// /// Ii "points" can be calculated from native physical pixels /// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct RawInput { /// The id of the active viewport. @@ -65,9 +65,20 @@ pub struct RawInput { /// Dragged files dropped into egui. /// + /// egui never reads the file contents. + #[cfg_attr( + not(target_arch = "wasm32"), + doc = "Call [`crate::DroppedFile::bytes`] to read a dropped file." + )] + #[cfg_attr( + target_arch = "wasm32", + doc = "Call [`crate::DroppedFile::bytes_async`] to read a dropped file." + )] + /// /// Note: when using `eframe` on Windows, this will always be empty if drag-and-drop support has /// been disabled in [`crate::viewport::ViewportBuilder`]. - pub dropped_files: Vec, + #[cfg_attr(feature = "serde", serde(skip))] + pub dropped_files: Vec, /// The native window has the keyboard focus (i.e. is receiving key presses). /// diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 8ed3086d1..540f05ca8 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -183,7 +183,7 @@ pub struct WrapApp { #[cfg(any(feature = "glow", feature = "wgpu"))] custom3d: Option, - dropped_files: Vec, + dropped_files: Vec, } impl WrapApp { @@ -519,25 +519,23 @@ impl WrapApp { .open(&mut open) .show(ctx, |ui| { 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() { - "???".to_owned() - } else { - file.name.clone() - }; + #[cfg(not(target_arch = "wasm32"))] + let info = file.path().display().to_string(); - let mut additional_info = vec![]; - if !file.mime.is_empty() { - additional_info.push(format!("type: {}", file.mime)); - } - if let Some(bytes) = &file.bytes { - additional_info.push(format!("{} bytes", bytes.len())); - } - if !additional_info.is_empty() { - use std::fmt::Write as _; - write!(info, " ({})", additional_info.join(", ")).ok(); - } + // The size and mime-type are free to read; the contents are not, + // so we never touch them here. + #[cfg(target_arch = "wasm32")] + let info = { + let Some(web_file) = file.web_file() else { + continue; + }; + let (name, mime) = (web_file.name(), web_file.type_()); + if mime.is_empty() { + format!("{name} ({} bytes)", web_file.size()) + } else { + format!("{name} ({} bytes, type: {mime})", web_file.size()) + } + }; ui.label(info); } diff --git a/examples/file_dialog/src/main.rs b/examples/file_dialog/src/main.rs index 914bd3423..d42da9b82 100644 --- a/examples/file_dialog/src/main.rs +++ b/examples/file_dialog/src/main.rs @@ -20,7 +20,7 @@ fn main() -> eframe::Result { #[derive(Default)] struct MyApp { - dropped_files: Vec, + dropped_files: Vec, picked_path: Option, } @@ -48,27 +48,23 @@ impl eframe::App for MyApp { ui.label("Dropped files:"); 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() { - "???".to_owned() - } else { - file.name.clone() - }; + #[cfg(not(target_arch = "wasm32"))] + ui.label(file.path().display().to_string()); - let mut additional_info = vec![]; - if !file.mime.is_empty() { - additional_info.push(format!("type: {}", file.mime)); + #[cfg(target_arch = "wasm32")] + { + let Some(web_file) = file.web_file() else { + continue; + }; + let name = web_file.name(); + let mime = web_file.type_(); + let size = web_file.size(); + if mime.is_empty() { + ui.label(format!("{name} ({size} bytes)")); + } else { + ui.label(format!("{name} (type: {mime}, {size} bytes)")); + } } - if let Some(bytes) = &file.bytes { - additional_info.push(format!("{} bytes", bytes.len())); - } - if !additional_info.is_empty() { - use std::fmt::Write as _; - write!(info, " ({})", additional_info.join(", ")).ok(); - } - - ui.label(info); } }); } From dae9adf307b36d97f3254e43c356818280d32ed0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 3 Aug 2026 09:02:41 -0700 Subject: [PATCH 24/49] Rename `failed_pixel_count_threshold` to `max_failed_pixels` (#8383) It was confusing that both tolerances had "threshold" in the name --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/egui_kittest/README.md | 21 ++++-- crates/egui_kittest/src/config.rs | 112 ++++++++++++++++++++++------ crates/egui_kittest/src/snapshot.rs | 81 ++++++++++++++------ 3 files changed, 163 insertions(+), 51 deletions(-) diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 7b97f4e1d..8711aeabb 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -44,25 +44,32 @@ All possible settings and their defaults: # path to the snapshot directory output_path = "tests/snapshots" -# default threshold for image comparison tests +# maximum weighted squared YIQ color distance between two corresponding pixels +# (a per-pixel color tolerance, applied to each pixel pair on its own) threshold = 0.6 -# default failed_pixel_count_threshold -failed_pixel_count_threshold = 0 +# how many pixels may exceed the `threshold` before the test fails +# (an absolute pixel count, not a fraction of the image) +max_failed_pixels = 0 [windows] threshold = 0.6 -failed_pixel_count_threshold = 0 +max_failed_pixels = 0 [macos] threshold = 0.6 -failed_pixel_count_threshold = 0 +max_failed_pixels = 0 [linux] threshold = 0.6 -failed_pixel_count_threshold = 0 +max_failed_pixels = 0 ``` +Raise `max_failed_pixels` only very carefully: a high value (more than ~10) is enough to hide a +real change, such as a moved separator, a shifted one-pixel border, or a small icon rendering +incorrectly. Prefer the smallest value that makes the test pass, and re-check it whenever you +update the snapshot. + ## Snapshot testing There is a snapshot testing feature. To create snapshot tests, enable the `snapshot` and `wgpu` features. Once enabled, you can call `Harness::snapshot` to render the ui and save the image to the `tests/snapshots` directory. @@ -105,7 +112,7 @@ However, especially when you're using custom rendering, you may observe images d First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilities. Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless. -Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test. +Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerances (`SnapshotOptions::threshold` and, as a last resort, `SnapshotOptions::max_failed_pixels`) for the specific test. See also TODO([#5683](https://github.com/emilk/egui/issues/5683)). ⚠️ **WARNING** ⚠️ Picking too high tolerances may mean that you are missing actual test failures. diff --git a/crates/egui_kittest/src/config.rs b/crates/egui_kittest/src/config.rs index a94075973..cdb74e00f 100644 --- a/crates/egui_kittest/src/config.rs +++ b/crates/egui_kittest/src/config.rs @@ -15,15 +15,20 @@ pub struct Config { /// Default is "tests/snapshots" (relative to the working directory / crate root). output_path: PathBuf, - /// The per-pixel threshold. + /// The maximum weighted squared YIQ color distance between two corresponding pixels. + /// + /// Pixels that differ by more than this are counted as failing. + /// This is an absolute, per-pixel value, and does not depend on the image dimensions. /// /// Default is 0.6. threshold: f32, - /// The number of pixels that can differ before the test is considered failed. + /// The number of pixels that may fail the [`Self::threshold`] before the test is + /// considered failed. /// /// Default is 0. - failed_pixel_count_threshold: usize, + #[serde(alias = "failed_pixel_count_threshold")] + max_failed_pixels: usize, windows: OsConfig, mac: OsConfig, @@ -35,7 +40,7 @@ impl Default for Config { Self { output_path: PathBuf::from("tests/snapshots"), threshold: 0.6, - failed_pixel_count_threshold: 0, + max_failed_pixels: 0, windows: Default::default(), mac: Default::default(), linux: Default::default(), @@ -48,8 +53,9 @@ pub struct OsConfig { /// Override the per-pixel threshold for this OS. threshold: Option, - /// Override the failed pixel count threshold for this OS. - failed_pixel_count_threshold: Option, + /// Override the maximum number of failing pixels for this OS. + #[serde(alias = "failed_pixel_count_threshold")] + max_failed_pixels: Option, } fn find_kittest_toml() -> io::Result { @@ -72,13 +78,44 @@ fn find_kittest_toml() -> io::Result { } } +/// The old name of `max_failed_pixels` is still accepted, but warned about. +fn warn_about_deprecated_keys(config_str: &str) { + let Ok(config) = toml::from_str::(config_str) else { + return; + }; + + let mut sections = vec![("", &config)]; + for name in ["windows", "mac", "linux"] { + if let Some(table) = config.get(name).and_then(toml::Value::as_table) { + sections.push((name, table)); + } + } + + for (section, table) in sections { + if table.contains_key("failed_pixel_count_threshold") { + let prefix = if section.is_empty() { + String::new() + } else { + format!("{section}.") + }; + log::warn!( + "`{prefix}failed_pixel_count_threshold` in kittest.toml is deprecated; \ + use `{prefix}max_failed_pixels` instead." + ); + } + } +} + fn load_config() -> Config { if let Ok(config_path) = find_kittest_toml() { match std::fs::read_to_string(&config_path) { - Ok(config_str) => match toml::from_str(&config_str) { - Ok(config) => config, - Err(e) => panic!("Failed to parse {}: {e}", config_path.display()), - }, + Ok(config_str) => { + warn_about_deprecated_keys(&config_str); + match toml::from_str(&config_str) { + Ok(config) => config, + Err(err) => panic!("Failed to parse {}: {err}", config_path.display()), + } + } Err(err) => { panic!("Failed to read {}: {}", config_path.display(), err); } @@ -127,30 +164,59 @@ impl Config { } } - pub fn os_failed_pixel_count_threshold(&self) -> crate::OsThreshold { - let fallback = self.failed_pixel_count_threshold; + pub fn os_max_failed_pixels(&self) -> crate::OsThreshold { + let fallback = self.max_failed_pixels; crate::OsThreshold { - windows: self - .windows - .failed_pixel_count_threshold - .unwrap_or(fallback), - macos: self.mac.failed_pixel_count_threshold.unwrap_or(fallback), - linux: self.linux.failed_pixel_count_threshold.unwrap_or(fallback), + windows: self.windows.max_failed_pixels.unwrap_or(fallback), + macos: self.mac.max_failed_pixels.unwrap_or(fallback), + linux: self.linux.max_failed_pixels.unwrap_or(fallback), fallback, } } - /// The threshold. + /// The maximum weighted squared YIQ color distance between two corresponding pixels. /// - /// Default is 1.0. + /// This is an absolute, per-pixel value, and does not depend on the image dimensions. + /// + /// Default is 0.6. pub fn threshold(&self) -> f32 { self.os_threshold().threshold() } - /// The number of pixels that can differ before the test is considered failed. + /// The number of pixels that may fail the [`Self::threshold`] before the test is + /// considered failed. /// /// Default is 0. - pub fn failed_pixel_count_threshold(&self) -> usize { - self.os_failed_pixel_count_threshold().threshold() + pub fn max_failed_pixels(&self) -> usize { + self.os_max_failed_pixels().threshold() + } +} + +#[cfg(test)] +mod tests { + use super::Config; + + #[test] + fn deprecated_failed_pixel_count_threshold_key_is_accepted() { + let config: Config = toml::from_str( + r" + failed_pixel_count_threshold = 1 + + [windows] + failed_pixel_count_threshold = 2 + + [mac] + failed_pixel_count_threshold = 3 + + [linux] + failed_pixel_count_threshold = 4 + ", + ) + .unwrap_or_else(|err| panic!("Failed to parse config: {err}")); + + assert_eq!(config.max_failed_pixels, 1); + assert_eq!(config.windows.max_failed_pixels, Some(2)); + assert_eq!(config.mac.max_failed_pixels, Some(3)); + assert_eq!(config.linux.max_failed_pixels, Some(4)); } } diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 268e0749f..09ce986c8 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -11,18 +11,35 @@ pub type SnapshotResult = Result<(), SnapshotError>; #[non_exhaustive] #[derive(Clone, Debug)] pub struct SnapshotOptions { - /// The threshold for the image comparison. + /// How much a single pixel may differ before it is counted as failing: + /// the maximum weighted squared YIQ color distance between two corresponding pixels. + /// + /// This is a color tolerance, not an error budget for the image as a whole: + /// it is applied to each pixel pair on its own, and raising it makes every pixel + /// more forgiving. Use [`Self::max_failed_pixels`] to allow a number of pixels + /// to exceed it. /// /// Can be configured via kittest.toml. The fallback is `0.6` (which is enough for most egui /// tests to pass across different wgpu backends). pub threshold: f32, - /// The number of pixels that can differ before the snapshot is considered a failure. + /// The number of pixels that may fail the [`Self::threshold`] before the snapshot is + /// considered a failure. /// - /// Preferably, you should use `threshold` to control the sensitivity of the image comparison. + /// This is an absolute pixel count, not a fraction of the image, so the same value is + /// stricter for a large snapshot than for a small one. + /// + /// Preferably, you should use [`Self::threshold`] to control the sensitivity of the image + /// comparison. /// As a last resort, you can use this to allow a certain number of pixels to differ. + /// + /// Raise this only very carefully: a high value (more than ~10) is enough to hide a real + /// change, such as a moved separator, a shifted one-pixel border, or a small icon rendering + /// incorrectly. Prefer the smallest value that makes the test pass, and re-check it whenever + /// you update the snapshot. + /// /// Can be configured via kittest.toml. The fallback is `0` (meaning no pixels can differ). - pub failed_pixel_count_threshold: usize, + pub max_failed_pixels: usize, /// The path where the snapshots will be saved. /// @@ -33,12 +50,12 @@ pub struct SnapshotOptions { pub output_path: PathBuf, } -/// Helper struct to define the number of pixels that can differ before the snapshot is considered a failure. +/// Helper struct to define a per-OS comparison tolerance. /// -/// This is useful if you want to set different thresholds for different operating systems. +/// This is useful if you want to set different tolerances for different operating systems. /// /// [`OsThreshold::default`] gets the default from the config file (`kittest.toml`). -/// For `usize`, it's the `failed_pixel_count_threshold` value. +/// For `usize`, it's the `max_failed_pixels` value. /// For `f32`, it's the `threshold` value. /// /// Example usage: @@ -51,7 +68,7 @@ pub struct SnapshotOptions { /// "os_threshold_example", /// &SnapshotOptions::new() /// .threshold(OsThreshold::new(0.0).windows(10.0)) -/// .failed_pixel_count_threshold(OsThreshold::new(0).windows(10).macos(53) +/// .max_failed_pixels(OsThreshold::new(0).windows(10).macos(53) /// )) /// ``` #[derive(Debug, Clone, Copy)] @@ -63,11 +80,11 @@ pub struct OsThreshold { } impl Default for OsThreshold { - /// Returns the default `failed_pixel_count_threshold` as configured in `kittest.toml` + /// Returns the default `max_failed_pixels` as configured in `kittest.toml` /// /// The fallback is `0`. fn default() -> Self { - config().os_failed_pixel_count_threshold() + config().os_max_failed_pixels() } } @@ -158,7 +175,7 @@ impl Default for SnapshotOptions { Self { threshold: config().threshold(), output_path: config().output_path(), - failed_pixel_count_threshold: config().failed_pixel_count_threshold(), + max_failed_pixels: config().max_failed_pixels(), } } } @@ -169,7 +186,14 @@ impl SnapshotOptions { Default::default() } - /// Change the threshold for the image comparison. + /// Change how much a single pixel may differ before it is counted as failing: + /// the maximum weighted squared YIQ color distance between two corresponding pixels. + /// + /// This is a color tolerance, not an error budget for the image as a whole: + /// it is applied to each pixel pair on its own, and raising it makes every pixel + /// more forgiving. Use [`Self::max_failed_pixels`] to allow a number of pixels + /// to exceed it. + /// /// The default is `0.6` (which is enough for most egui tests to pass across different /// wgpu backends). #[inline] @@ -187,18 +211,33 @@ impl SnapshotOptions { self } - /// Change the number of pixels that can differ before the snapshot is considered a failure. + /// Change the number of pixels that may fail the [`Self::threshold`] before the snapshot is + /// considered a failure. + /// + /// This is an absolute pixel count, not a fraction of the image, so the same value is + /// stricter for a large snapshot than for a small one. /// /// Preferably, you should use [`Self::threshold`] to control the sensitivity of the image comparison. /// As a last resort, you can use this to allow a certain number of pixels to differ. + /// + /// Raise this only very carefully: a high value (more than ~10) is enough to hide a real + /// change, such as a moved separator, a shifted one-pixel border, or a small icon rendering + /// incorrectly. Prefer the smallest value that makes the test pass, and re-check it whenever + /// you update the snapshot. + #[inline] + pub fn max_failed_pixels(mut self, max_failed_pixels: impl Into>) -> Self { + self.max_failed_pixels = max_failed_pixels.into().threshold(); + self + } + + /// Renamed to [`Self::max_failed_pixels`]. + #[deprecated(since = "0.36.0", note = "Renamed to max_failed_pixels")] #[inline] pub fn failed_pixel_count_threshold( - mut self, - failed_pixel_count_threshold: impl Into>, + self, + max_failed_pixels: impl Into>, ) -> Self { - let failed_pixel_count_threshold = failed_pixel_count_threshold.into().threshold(); - self.failed_pixel_count_threshold = failed_pixel_count_threshold; - self + self.max_failed_pixels(max_failed_pixels) } } @@ -219,7 +258,7 @@ pub enum SnapshotError { /// /// Measured at [`THRESHOLD_SWEEP`], lowest threshold first. /// Use this to pick a [`SnapshotOptions::threshold`] and a - /// [`SnapshotOptions::failed_pixel_count_threshold`] from measurements, + /// [`SnapshotOptions::max_failed_pixels`] from measurements, /// instead of by trial and error. failing_pixels_by_threshold: Vec<(f32, i32)>, }, @@ -441,7 +480,7 @@ fn try_image_snapshot_options_impl( let SnapshotOptions { threshold, output_path, - failed_pixel_count_threshold, + max_failed_pixels, } = options; let parent_path = if let Some(parent) = PathBuf::from(&name).parent() { @@ -544,7 +583,7 @@ fn try_image_snapshot_options_impl( return Ok(()); // Difference below threshold }; - let below_threshold = num_wrong_pixels as i64 <= *failed_pixel_count_threshold as i64; + let below_threshold = num_wrong_pixels as i64 <= *max_failed_pixels as i64; if !below_threshold { diff_image From 5c0b690dab5023139957c144fe282db4c6e9713e Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:06:47 +0900 Subject: [PATCH 25/49] Add `extra_text_line_spacing` to control vertical spacing between text lines (#8040) Add `extra_text_line_spacing` to control vertical spacing between text lines **Description** This PR adds a new `Spacing::extra_text_line_spacing` field to control additional vertical spacing between lines of text. The spacing is applied to text layout by adjusting `TextFormat::line_height` based on the font row height plus the configured extra spacing. This improves text readability and allows consistent line spacing customization for widgets such as `TextEdit` and `Label`. --------- Co-authored-by: Emil Ernerfeldt --- crates/egui/src/style.rs | 9 +++++++++ crates/egui/src/widget_text.rs | 11 ++++++++--- crates/egui/src/widgets/text_edit/builder.rs | 9 ++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index faa65b49e..953cd4b9a 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -419,6 +419,9 @@ pub struct Spacing { /// Default width of a [`crate::TextEdit`]. pub text_edit_width: f32, + /// Additional vertical spacing between lines of text. + pub extra_text_line_spacing: f32, + /// Checkboxes, radio button and collapsing headers have an icon at the start. /// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox). pub icon_width: f32, @@ -1458,6 +1461,7 @@ impl Default for Spacing { slider_rail_height: 8.0, combo_width: 100.0, text_edit_width: 280.0, + extra_text_line_spacing: 0.0, icon_width: 14.0, icon_width_inner: 8.0, icon_spacing: 4.0, @@ -1948,6 +1952,7 @@ impl Spacing { slider_rail_height, combo_width, text_edit_width, + extra_text_line_spacing, icon_width, icon_width_inner, icon_spacing, @@ -2014,6 +2019,10 @@ impl Spacing { ui.add(DragValue::new(text_edit_width).range(0.0..=1000.0)); ui.end_row(); + ui.label("Extra text line spacing"); + ui.add(DragValue::new(extra_text_line_spacing).range(0.0..=20.0)); + ui.end_row(); + ui.label("Tooltip wrap width"); ui.add(DragValue::new(tooltip_width).range(0.0..=1000.0)); ui.end_row(); diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index 37018c0b2..c8e803cbc 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -769,14 +769,19 @@ impl WidgetText { .visuals .override_text_color .unwrap_or(crate::Color32::PLACEHOLDER); + + // We want the style overrides to take precedence over the fallback font + let font_id = FontSelection::default().resolve_with_fallback(style, fallback_font); + let line_height = ctx + .fonts_mut(|f| f.row_height(&font_id) + style.spacing.extra_text_line_spacing); + let mut layout_job = LayoutJob::simple_format( text, TextFormat { - // We want the style overrides to take precedence over the fallback font - font_id: FontSelection::default() - .resolve_with_fallback(style, fallback_font), + font_id, color, valign: default_valign, + line_height: Some(line_height), ..Default::default() }, ); diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index aa4f18d8a..96bd3ca01 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -471,6 +471,8 @@ impl TextEdit<'_> { let font_id = font_selection.resolve(ui.style()); let row_height = ui.fonts_mut(|f| f.row_height(&font_id)); + let line_height = row_height + ui.spacing().extra_text_line_spacing; + const MIN_WIDTH: f32 = 24.0; // Never make a [`TextEdit`] more narrow than this. let available_width = ui.available_width().at_least(MIN_WIDTH); let desired_width = desired_width @@ -489,12 +491,17 @@ impl TextEdit<'_> { layout_job.halign = align.x(); // We want to keep the trailing whitespace, since hiding it feels really weird when typing layout_job.keep_trailing_whitespace = true; + + for section in &mut layout_job.sections { + section.format.line_height = Some(line_height); + } + ui.fonts_mut(|f| f.layout_job(layout_job)) }; let layouter = layouter.unwrap_or(&mut default_layouter); - let min_inner_height = (desired_height_rows.at_least(1) as f32) * row_height; + let min_inner_height = (desired_height_rows.at_least(1) as f32) * line_height; let id = id.unwrap_or_else(|| { if let Some(id_salt) = id_salt { From 78c0e39d1d7ced1a0d91671a51e398b5338f63a7 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:10:34 +0900 Subject: [PATCH 26/49] Fix ScrollArea failure by handling horizontal and vertical scrolling separately in the missing place (#8275) Fix ScrollArea failure by handling horizontal and vertical scrolling separately in the missing place Everywhere in `ScrollArea`, horizontal and vertical scrolling are handled separately. However, because there is a single place where they are not handled separately, when trying to process horizontal and vertical scrolls independently, one of the dimensions fails to scroll. This Pull Request ensures that horizontal and vertical scrolling are handled separately in this area, just like in the rest of the codebase. * Closes #5289 * Closes #5307 * Closes #8274 --- crates/egui/src/containers/scroll_area.rs | 27 +++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index e59f7e01d..7b1bf87e1 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1081,17 +1081,9 @@ impl Prepared { let content_size = content_ui.min_size(); - let scroll_delta = content_ui - .ctx() - .pass_state_mut(|state| std::mem::take(&mut state.scroll_delta)); - let mut had_explicit_scroll_adjustment = Vec2b::FALSE; for d in 0..2 { - // PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. - let mut delta = -scroll_delta.0[d]; - let mut animation = scroll_delta.1; - // We always take both scroll targets regardless of which scroll axes are enabled. This // is to avoid them leaking to other scroll areas. let scroll_target = content_ui @@ -1099,6 +1091,17 @@ impl Prepared { .pass_state_mut(|state| state.scroll_target[d].take()); if direction_enabled[d] { + let (scroll_delta, scroll_animation) = content_ui.ctx().pass_state_mut(|state| { + ( + std::mem::take(&mut state.scroll_delta.0[d]), + state.scroll_delta.1, + ) + }); + + // PassState::scroll_delta is inverted from the way we apply the delta, so we need to negate it. + let mut delta = -scroll_delta; + let mut animation = scroll_animation; + if let Some(target) = scroll_target { let pass_state::ScrollTarget { range, @@ -1132,8 +1135,8 @@ impl Prepared { 0.0 }; - delta += delta_update; animation = animation_update; + delta += delta_update; } if delta != 0.0 { @@ -1157,10 +1160,10 @@ impl Prepared { } ui.request_repaint(); } - } - if delta != 0.0 { - had_explicit_scroll_adjustment[d] = true; + if delta != 0.0 { + had_explicit_scroll_adjustment[d] = true; + } } } From 98eab505778e1d474ec5d8a727f16a2ee9e6cfb9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Aug 2026 02:09:27 -0700 Subject: [PATCH 27/49] Treat a press that leaves a widget as a drag (#8365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A widget that senses both clicks and drags postpones the click-versus-drag decision until the pointer moves past `max_click_dist` or is held for `max_click_duration`. But a click has to be released *on* the widget — so once the pointer leaves, the gesture can only be a drag, and there is nothing left to wait for. This matters for widgets thinner than `max_click_dist` (6px), such as panel resize handles. The pointer leaves such a widget almost immediately, which hands the hover to whatever is underneath, while `dragged()` was not true yet. So a handle highlighting on `hovered() || dragged()` blinked out mid-gesture. --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/egui/src/interaction.rs | 14 +- crates/egui/src/response.rs | 25 ++- tests/egui_tests/tests/test_click_or_drag.rs | 187 +++++++++++++++++++ 3 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 tests/egui_tests/tests/test_click_or_drag.rs diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 6e01ec8fb..68ec86a50 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -197,7 +197,19 @@ pub(crate) fn interact( // This widget is sensitive to both clicks and drags. // When the mouse first is pressed, it could be either, // so we postpone the decision until we know. - input.pointer.is_decidedly_dragging() + // + // …unless a click is no longer possible at all: a click has to be + // released on the widget, and `hits.click` tells us whether the + // pointer is still somewhere a release would land on this widget. + // Note that this is not the same as being inside `interact_rect`: + // the hit-test also picks up widgets within `interact_radius`, and + // lets a widget on top take the hit. + // + // Deciding here means a thin drag handle (narrower than + // `max_click_dist`) doesn't spend the decision window as neither + // hovered nor dragged, which would make its highlight blink out. + let could_still_be_clicked = hits.click.is_some_and(|hit| hit.id == widget.id); + input.pointer.is_decidedly_dragging() || !could_still_be_clicked } else { // This widget is just sensitive to drags, so we can mark it as dragged right away: widget.sense.senses_drag() diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 81d04f1ef..5ba7b8943 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -309,6 +309,12 @@ impl Response { /// /// In contrast to [`Self::contains_pointer`], this will be `false` whenever some other widget is being dragged. /// `hovered` is always `false` for disabled widgets. + /// + /// While a widget is being clicked or dragged it is the only hovered widget, + /// so this stays `true` even after the pointer moves off it. Together with + /// how [`Self::dragged`] resolves a press that leaves the widget, that means + /// `hovered() || dragged()` holds for a whole press-drag-release gesture, + /// which is what you want for highlighting something like a drag handle. #[inline(always)] pub fn hovered(&self) -> bool { self.flags.contains(Flags::HOVERED) @@ -403,11 +409,21 @@ impl Response { /// To find out which button(s), use [`Self::dragged_by`]. /// /// If the widget is only sensitive to drags, this is `true` as soon as the pointer presses down on it. - /// If the widget also senses clicks, this won't be true until the pointer has moved a bit, - /// or the user has pressed down for long enough. + /// + /// If the widget also senses clicks, the press could be either, so the + /// decision is postponed until whichever of these comes first: + /// * the pointer moves further than [`crate::InputOptions::max_click_dist`], + /// * it is held longer than [`crate::InputOptions::max_click_duration`], + /// * or it leaves the widget — a click has to be released on the widget, so + /// once the pointer is outside, the gesture can only be a drag. This is what + /// keeps a handle thinner than `max_click_dist` from spending the decision + /// window as neither hovered nor dragged. + /// /// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details. /// - /// If you want to avoid the delay, use [`Self::is_pointer_button_down_on`] instead. + /// While the decision is pending the pointer is still on the widget, so + /// [`Self::hovered`] is `true` throughout. If you want neither the delay nor + /// the distinction, use [`Self::is_pointer_button_down_on`]. /// /// If the widget is NOT sensitive to drags, this will always be `false`. /// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]). @@ -571,6 +587,9 @@ impl Response { /// even when dragging outside the widget. /// /// This could also be thought of as "is this widget being interacted with?". + /// + /// Unlike [`Self::dragged`], this is `true` from the press frame onwards, with + /// no click-versus-drag decision window. #[inline(always)] pub fn is_pointer_button_down_on(&self) -> bool { self.flags.contains(Flags::IS_POINTER_BUTTON_DOWN_ON) diff --git a/tests/egui_tests/tests/test_click_or_drag.rs b/tests/egui_tests/tests/test_click_or_drag.rs new file mode 100644 index 000000000..58be44247 --- /dev/null +++ b/tests/egui_tests/tests/test_click_or_drag.rs @@ -0,0 +1,187 @@ +//! Tests for how egui decides whether a press on a click-and-drag widget +//! is a click or a drag. + +use egui::{Id, InputOptions, Pos2, Rect, Sense, Style, Vec2}; +use egui_kittest::Harness; + +/// How far the pointer may move before a press is decidedly a drag. +fn max_click_dist() -> f32 { + InputOptions::default().max_click_dist +} + +/// How far outside its rect a widget can still be hit. +fn interact_radius() -> f32 { + Style::default().interaction.interact_radius +} + +fn widget_id() -> Id { + Id::new("click_and_drag") +} + +/// A harness with one click-and-drag widget of the given size at the top-left. +/// +/// If `with_background`, a second click-and-drag widget covers the whole area +/// _beneath_ it. That one matters: without something under the pointer to take +/// over the hover, the first widget keeps it even after the pointer leaves. +/// +/// Steps at 60Hz. The default `step_dt` of 0.25s would blow past +/// `max_click_duration` within a couple of frames, turning every press into a +/// drag before the distance rules get a chance to matter. +fn harness_with_widget(size: Vec2, with_background: bool) -> Harness<'static, ()> { + Harness::builder() + .with_step_dt(1.0 / 60.0) + .with_size(Vec2::new(300.0, 200.0)) + .build_ui(move |ui| { + if with_background { + // Allocated first, so it ends up _behind_ the widget under test. + ui.interact( + ui.max_rect(), + Id::new("background"), + Sense::click_and_drag(), + ); + } + let rect = Rect::from_min_size(ui.max_rect().min, size); + ui.interact(rect, widget_id(), Sense::click_and_drag()); + }) +} + +/// The widget's `(hovered, dragged)` as of the last completed pass. +fn widget_state(harness: &Harness<'_, ()>) -> (bool, bool) { + harness + .ctx + .read_response(widget_id()) + .map(|r| (r.hovered(), r.dragged())) + .expect("the widget should have been registered") +} + +/// The widget's rect as of the last completed pass. +fn widget_rect(harness: &Harness<'_, ()>) -> Rect { + harness + .ctx + .read_response(widget_id()) + .expect("the widget should have been registered") + .rect +} + +/// Press the primary button at `pos`, without releasing it. +fn press_at(harness: &mut Harness<'_, ()>, pos: Pos2) { + harness.hover_at(pos); + harness.step(); + harness.drag_at(pos); + harness.step(); +} + +/// Once a release can no longer land on the widget, the press can no longer become +/// a click, so it counts as a drag right away — without waiting for `max_click_dist`. +/// +/// This matters for widgets thinner than `max_click_dist` (panel resize handles, +/// say): waiting would leave them neither hovered nor dragged for a few frames, +/// which shows up as a flickering highlight. +#[test] +fn press_that_leaves_a_thin_widget_becomes_a_drag_immediately() { + let width = max_click_dist() / 2.0; // thinner than `max_click_dist` + let mut harness = harness_with_widget(Vec2::new(width, 100.0), true); + harness.step(); + + let grab = widget_rect(&harness).center(); + press_at(&mut harness, grab); + + let (hovered, dragged) = widget_state(&harness); + assert!(hovered && !dragged, "the press starts out undecided"); + + // Creep outward in 1px steps, never reaching `max_click_dist` — + // if we did, `is_decidedly_dragging` would explain the drag on its own + // and the test would prove nothing. + let mut saw_drag = false; + for step in 1..max_click_dist().ceil() as i32 { + let offset = step as f32; + harness.hover_at(Pos2::new(grab.x + offset, grab.y)); + harness.step(); + + let (hovered, dragged) = widget_state(&harness); + saw_drag |= dragged; + assert!( + hovered || dragged, + "at +{offset}px the widget was neither hovered nor dragged, \ + so anything highlighting on `hovered || dragged` would blink out" + ); + } + + assert!( + saw_drag, + "leaving the widget should have started a drag, even within max_click_dist" + ); +} + +/// While the pointer is still on the widget, a press stays undecided: hovered, +/// but not yet dragged, so it can still become a click. +#[test] +fn press_inside_a_wide_widget_stays_undecided() { + let mut harness = harness_with_widget(Vec2::new(100.0, 100.0), true); + harness.step(); + + let grab = widget_rect(&harness).center(); + press_at(&mut harness, grab); + + // A small twitch: inside the widget, and inside `max_click_dist`. + harness.hover_at(Pos2::new(grab.x + max_click_dist() / 2.0, grab.y)); + harness.step(); + + let (hovered, dragged) = widget_state(&harness); + assert!(hovered, "the pointer is still over the widget"); + assert!( + !dragged, + "a small twitch inside the widget should still be able to become a click" + ); +} + +/// A press just _outside_ the widget still hits it, thanks to `interact_radius`. +/// The pointer hasn't moved at all, so this must not count as leaving the widget. +#[test] +fn press_just_outside_a_widget_stays_undecided() { + // No background: we want the widget to win the hit-test from a distance. + let mut harness = harness_with_widget(Vec2::new(100.0, 100.0), false); + harness.step(); + + let rect = widget_rect(&harness); + let offset = interact_radius() - 1.0; + let grab = Pos2::new(rect.right() + offset, rect.center().y); + press_at(&mut harness, grab); + + let (hovered, dragged) = widget_state(&harness); + assert!( + hovered, + "a press within interact_radius still hits the widget" + ); + assert!( + !dragged, + "the pointer never moved, so this press must still be able to become a click" + ); +} + +/// A press and release inside the widget is still a click, not a drag. +#[test] +fn click_inside_a_widget_still_clicks() { + let mut harness = harness_with_widget(Vec2::new(100.0, 100.0), true); + harness.step(); + + let grab = widget_rect(&harness).center(); + press_at(&mut harness, grab); + // Release without `drop_at`, which would also fire `PointerGone` and so + // discard the click. + harness.event(egui::Event::PointerButton { + pos: grab, + button: egui::PointerButton::Primary, + pressed: false, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + + assert!( + harness + .ctx + .read_response(widget_id()) + .is_some_and(|r| r.clicked()), + "press and release without moving should be a click" + ); +} From 622bbbeccc0b8c72fe796b23b6efea757354a8d4 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Aug 2026 02:20:25 -0700 Subject: [PATCH 28/49] Add drag-to-open for collapsible panels (#8363) A fully collapsed `show_collapsible` panel now leaves a thin grab handle at its fixed edge, invisible until hovered. Dragging it out past `min_size` (or double-clicking it) reopens the panel. Opt out with `panel.drag_to_open(false)`. --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/egui/src/containers/panel.rs | 225 ++++++++---- crates/egui_demo_app/src/wrap_app.rs | 3 +- crates/egui_demo_lib/src/demo/panels.rs | 4 +- crates/emath/src/range.rs | 8 + .../panel_drag/collapsed_handle_hovered.png | 3 + .../panel_drag/collapsed_handle_idle.png | 3 + .../panel_drag/collapsed_handle_reopened.png | 3 + tests/egui_tests/tests/test_panel_drag.rs | 342 +++++++++++++++++- 8 files changed, 520 insertions(+), 71 deletions(-) create mode 100644 tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_hovered.png create mode 100644 tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_idle.png create mode 100644 tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_reopened.png diff --git a/crates/egui/src/containers/panel.rs b/crates/egui/src/containers/panel.rs index e4954b6cd..cc732007a 100644 --- a/crates/egui/src/containers/panel.rs +++ b/crates/egui/src/containers/panel.rs @@ -18,14 +18,24 @@ use emath::GuiRounding as _; use crate::{ - Align, Context, CursorIcon, Frame, Id, InnerResponse, Layout, Margin, NumExt as _, Rangef, - Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, + Align, Context, CursorIcon, Frame, Id, InnerResponse, LayerId, Layout, Margin, NumExt as _, + Order, Rangef, Rect, Response, Sense, Stroke, Ui, UiBuilder, UiKind, UiStackInfo, Vec2, lerp, }; fn animate_expansion(ctx: &Context, id: Id, is_expanded: bool) -> f32 { ctx.animate_bool_responsive(id, is_expanded) } +/// [`Id`] of a panel's resize-handle widget. +/// +/// A panel registers its handle under this same id whether it is open, +/// mid-slide, or fully collapsed — that is what lets one uninterrupted drag +/// collapse the panel and pull it back open. [`Panel::show_switched`] points +/// both of its panels at one shared handle the same way. +fn resize_widget_id(id_source: Id) -> Id { + id_source.with("__resize") +} + /// State regarding panels. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -198,6 +208,7 @@ pub struct Panel { id: Id, frame: Option, resizable: bool, + drag_to_open: bool, show_separator_line: bool, /// _Outer_ size (including [`Frame`] margin & border): @@ -283,6 +294,7 @@ impl Panel { id: id.into(), frame: None, resizable: true, + drag_to_open: true, show_separator_line: true, default_outer_size, outer_size_range, @@ -312,6 +324,24 @@ impl Panel { self } + /// Can a fully collapsed panel be dragged back open? + /// + /// Default: `true`. + /// + /// When enabled, a panel that [`Self::show_collapsible`] has collapsed all + /// the way still leaves a thin grab handle at its fixed edge. The handle is + /// invisible until hovered, at which point it lights up like a normal resize + /// handle. Dragging it outward past [`Self::min_size`] — or double-clicking + /// it — reopens the panel. + /// + /// This is the counterpart to drag-to-collapse, and like it requires + /// [`Self::resizable`] to be `true`. + #[inline] + pub fn drag_to_open(mut self, drag_to_open: bool) -> Self { + self.drag_to_open = drag_to_open; + self + } + /// Show a separator line, even when not interacting with it? /// /// The separator line sits on the panel's inner edge, i.e. the edge facing the rest of the ui. @@ -415,6 +445,9 @@ impl Panel { /// to `true` if the user drags the handle outward while the panel is closed. /// When [`Self::resizable`] is `true`, double-clicking the resize edge also /// flips `*is_expanded`. + /// + /// A fully collapsed panel keeps a thin grab handle at its fixed edge, so the + /// user can drag it back open. See [`Self::drag_to_open`] to opt out. pub fn show_collapsible( self, ui: &mut Ui, @@ -424,10 +457,11 @@ impl Panel { let how_expanded = animate_expansion(ui, self.id.with("animation"), *is_expanded); if how_expanded == 0.0 { - // Panel is fully closed. If the user is still dragging the resize handle - // from a previous frame, keep its widget id alive so they can drag the - // panel back out without releasing. - self.keep_drag_alive_for_reopen(ui, is_expanded); + // Panel is fully closed, but we still leave a grab handle at its fixed + // edge so the user can drag it back open. + if self.resizable && self.drag_to_open { + self.collapsed_resize_handle(ui, is_expanded); + } // Make sure the ids of the next widgets are the same whether we show the panel or not: ui.skip_ahead_auto_ids(1); @@ -436,7 +470,7 @@ impl Panel { // Don't lose the drag during the slide-back-open animation: let drag_in_progress = ui - .read_response(self.id.with("__resize")) + .read_response(self.resize_id()) .is_some_and(|r| r.dragged()); let panel = if how_expanded < 1.0 { @@ -549,20 +583,11 @@ impl Panel { // Is the resize handle currently being dragged? let drag_in_progress = ui - .read_response(resize_id_source.with("__resize")) + .read_response(resize_widget_id(resize_id_source)) .is_some_and(|r| r.dragged()); let animation_id = expanded_panel.id.with("animation"); - // While the user is dragging, snap the animation to the target so the - // drag (which sets `outer_size` directly from the pointer) doesn't fight - // a simultaneous slide. Without this, drag-to-expand visibly jumps as - // the slide animation tries to grow from 0 while the pointer is already - // at the expanded size. - let how_expanded = if drag_in_progress { - ui.animate_bool_with_time(animation_id, *is_expanded, 0.0) - } else { - animate_expansion(ui, animation_id, *is_expanded) - }; + let how_expanded = animate_expansion(ui, animation_id, *is_expanded); // When expanding, the user sees the expanded content the moment animation starts. // When collapsing, keep showing the expanded content until past the midpoint, @@ -585,7 +610,19 @@ impl Panel { let panel = if how_expanded < 1.0 { // Animate the visible size from collapsed_size to expanded_size, // so the slide picks up where the collapsed panel left off. - let expanded_size = expanded_panel.outer_size(ui); + let expanded_size = if drag_in_progress { + // During a drag the pointer sets the size, clamped to `min_size` + // — so that, not the (stale) persisted size, is where the slide + // meets the collapsed panel, whether opening or closing. Get it + // wrong and the panel jumps the gap between the two sizes in one + // frame. + expanded_panel + .outer_size_range + .min + .at_least(collapse_threshold) + } else { + expanded_panel.outer_size(ui) + }; let visible_size = lerp(collapse_threshold..=expanded_size, how_expanded); let slide_fraction = if 0.0 < expanded_size { visible_size / expanded_size @@ -702,7 +739,7 @@ impl Panel { // released size gets persisted into [`PanelState`] — without this the // store-skipped-during-drag rule would leave the stored size at the // pre-drag value. - let resize_id = self.resize_id_source.unwrap_or(id).with("__resize"); + let resize_id = self.resize_id(); let resize_response = parent_ui.read_response(resize_id); // Double-click on the resize edge toggles `*is_expanded` for the @@ -860,19 +897,25 @@ impl Panel { .store(parent_ui, id); } - // Hide the separator once the panel is mostly slid off — at that point - // the line would just be a stray dash hovering near the parent edge. - if 0.01 < self.slide_fraction { - let stroke = if is_resizing { - parent_ui.style().visuals.widgets.active.fg_stroke // highly visible - } else if resize_hover { - parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible - } else if show_separator_line { - // TODO(emilk): distinguish resizable from non-resizable - parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim - } else { - Stroke::NONE - }; + // The highlight follows the pointer all the way down to zero size, where + // `collapsed_resize_handle` picks it straight up again — so the user never + // loses sight of the edge they are dragging. The dim idle separator does + // get hidden once the panel is mostly slid off, since there it would just + // be a stray dash hovering near the parent edge. + let stroke = if is_resizing { + parent_ui.style().visuals.widgets.active.fg_stroke // highly visible + } else if resize_hover { + parent_ui.style().visuals.widgets.hovered.fg_stroke // highly visible + } else if show_separator_line && 0.01 < self.slide_fraction { + // TODO(emilk): distinguish resizable from non-resizable + parent_ui.style().visuals.widgets.noninteractive.bg_stroke // dim + } else { + Stroke::NONE + }; + if 0.0 < stroke.width { + // Nudged inward, to keep the line inside the panel's own (shifted) + // rect: `parent_ui`'s painter sits below the panels that come after + // this one, so anything drawn past the fixed edge is covered by them. // TODO(emilk): draw line on top of all panels in this ui when https://github.com/emilk/egui/issues/1516 is done // The line goes just _outside_ the frame's outline, in the room `resolve_frame` @@ -893,6 +936,13 @@ impl Panel { inner_response } + /// [`Id`] of this panel's resize-handle widget. + /// + /// See [`resize_widget_id`] for why open and collapsed panels must share it. + fn resize_id(&self) -> Id { + resize_widget_id(self.resize_id_source.unwrap_or(self.id)) + } + /// The configured [`Frame`], or the default side/top panel frame for this [`Ui`]. fn resolve_frame(&self, ui: &Ui) -> Frame { let mut frame = self @@ -918,48 +968,91 @@ impl Panel { frame } - /// Panel is fully closed. If the user is still dragging the resize handle - /// from the frame the panel closed on, keep its widget id registered so the - /// drag survives, and reopen if they drag back past the minimum size. - fn keep_drag_alive_for_reopen(&self, ui: &Ui, is_expanded: &mut bool) { - let resize_id = self.id.with("__resize"); - let Some(resize_response) = ui.read_response(resize_id) else { - return; - }; - if !resize_response.dragged() { - return; - } - let Some(pointer) = resize_response.interact_pointer_pos() else { - return; - }; + /// The grab handle of a fully collapsed panel: a thin strip along the panel's + /// fixed edge, invisible until hovered. + /// + /// Dragging it outward past the minimum size — or double-clicking it — + /// reopens the panel. Registering it under the same id as the expanded + /// panel's resize handle also keeps an in-progress drag-to-collapse gesture + /// alive, so the user can drag the panel straight back out without releasing. + fn collapsed_resize_handle(&self, ui: &Ui, is_expanded: &mut bool) { + let side = self.side; + let axis = side.axis(); - // Re-register the resize widget at the (now collapsed) fixed edge so its - // id stays alive in egui's interaction state. let available_rect = ui.available_rect_before_wrap(); - let fixed_edge_pos = self.side.fixed_pos(available_rect); - let cross_range = available_rect.range_along(self.side.cross_axis()); - let resize_rect = if self.side.axis() == 0 { + let fixed_edge_pos = side.fixed_pos(available_rect); + let cross_range = available_rect.range_along(side.cross_axis()); + + // The strip lies just _inside_ the fixed edge, so it never reaches + // outside the area the panel is allowed to occupy. + let mut resize_rect = if axis == 0 { Rect::from_x_y_ranges(Rangef::point(fixed_edge_pos), cross_range) } else { Rect::from_x_y_ranges(cross_range, Rangef::point(fixed_edge_pos)) }; - let grab = ui.style().interaction.resize_grab_radius_side; - let resize_rect = resize_rect.expand2(grab * self.side.axis_unit()); - ui.interact(resize_rect, resize_id, Sense::drag()); + side.set_rect_size( + &mut resize_rect, + ui.style().interaction.resize_grab_radius_side, + ); - // Keep the resize cursor while the user is still holding the drag. - // Otherwise the cursor would snap back to the default the moment the - // panel closed, even though the gesture is still ongoing. - ui.set_cursor_icon(self.cursor_icon(0.0)); + let resize_id = self.resize_id(); + let response = ui.interact(resize_rect, resize_id, Sense::click_and_drag()); - // Signed distance from the fixed edge to the pointer along the panel's - // axis. Only counts as "pulled outward" while positive — going past the - // fixed edge gives a negative value, NOT a mirrored positive one (no - // `.abs()`), so dragging past the screen edge can't spuriously reopen. - let dragged_size = -self.side.sign() * (pointer[self.side.axis()] - fixed_edge_pos); - if self.outer_size_range.min < dragged_size { + if response.double_clicked() { *is_expanded = true; } + + if response.hovered() || response.dragged() { + // Advertise that the panel can be pulled out. Also keeps the resize + // cursor for a drag that started before the panel closed, instead of + // snapping back to the default mid-gesture. + ui.set_cursor_icon(self.cursor_icon(0.0)); + } + + if response.dragged() + && let Some(pointer) = response.interact_pointer_pos() + { + // Signed distance from the fixed edge to the pointer along the panel's + // axis. Only counts as "pulled outward" while positive — going past the + // fixed edge gives a negative value, NOT a mirrored positive one (no + // `.abs()`), so dragging past the screen edge can't spuriously reopen. + // + // We require the full minimum size, so the panel never jumps ahead of + // the pointer: it opens exactly when the drag reaches the size it will + // open at, and follows the pointer from there. + let dragged_size = -side.sign() * (pointer[axis] - fixed_edge_pos); + if self.outer_size_range.min < dragged_size { + *is_expanded = true; + } + } + + // Invisible until hovered, so the handle doesn't read as a stray line at + // the edge of the screen. + let stroke = if response.dragged() { + ui.style().visuals.widgets.active.fg_stroke + } else if response.hovered() { + ui.style().visuals.widgets.hovered.fg_stroke + } else { + Stroke::NONE + }; + if 0.0 < stroke.width { + // The collapsed panel occupies no space of its own, so the line has to + // go _inside_ the area the following panels use — which means painting + // in a layer above them, or they would cover it. + // TODO(emilk): use the panel's own layer once https://github.com/emilk/egui/issues/1516 is done + let painter = ui + .ctx() + .layer_painter(LayerId::new(Order::Middle, resize_id)) + .with_clip_rect(resize_rect); + + // Nudge the line inward so it isn't half-clipped by the edge. + let line_pos = fixed_edge_pos - 0.5 * side.sign() * stroke.width; + if axis == 0 { + painter.vline(line_pos, cross_range, stroke); + } else { + painter.hline(cross_range, line_pos, stroke); + } + } } /// Get the current _outer_ width or height of the panel (from previous frame), @@ -994,7 +1087,7 @@ impl Panel { // Use `resize_id_source` so collapsed/expanded panels in // `show_switched` share one resize widget. - let resize_id = self.resize_id_source.unwrap_or(self.id).with("__resize"); + let resize_id = self.resize_id(); let resize_rect = Rect::from_x_y_ranges(resize_x, resize_y).expand2(amount); ui.interact(resize_rect, resize_id, Sense::click_and_drag()) } diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 540f05ca8..629ad6a97 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -361,7 +361,8 @@ impl WrapApp { let mut cmd = Command::Nothing; egui::Panel::left("backend_panel") - .resizable(false) + .resizable(true) + .size_range(280..=400) .show_collapsible(ui, &mut is_open, |ui| { ui.add_space(4.0); ui.vertical_centered(|ui| { diff --git a/crates/egui_demo_lib/src/demo/panels.rs b/crates/egui_demo_lib/src/demo/panels.rs index 8f2e811fa..00d19efdd 100644 --- a/crates/egui_demo_lib/src/demo/panels.rs +++ b/crates/egui_demo_lib/src/demo/panels.rs @@ -94,10 +94,10 @@ impl crate::View for Panels { bottom, egui::Panel::bottom("bottom_panel_collapsed") .resizable(true) - .default_size(20.0), + .exact_size(20.0), egui::Panel::bottom("bottom_panel_expanded") .resizable(true) - .max_size(128.0), + .size_range(64.0..=128.0), |ui, expanded| { if expanded { ui.vertical_centered(|ui| { diff --git a/crates/emath/src/range.rs b/crates/emath/src/range.rs index ffd34dc20..26d5f6f2c 100644 --- a/crates/emath/src/range.rs +++ b/crates/emath/src/range.rs @@ -170,6 +170,14 @@ impl From<&RangeInclusive> for Rangef { } } +/// Makes specifying size ranges slightly more convenient (no need for the extra `.0` suffixes) +impl From> for Rangef { + #[inline] + fn from(range: RangeInclusive) -> Self { + Self::new(*range.start() as _, *range.end() as _) + } +} + impl From> for Rangef { #[inline] fn from(range: RangeFrom) -> Self { diff --git a/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_hovered.png b/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_hovered.png new file mode 100644 index 000000000..27e751fa8 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_hovered.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71c6c7299aad708fd16b945bcc9c82f529a276486eb1b2042f2ebaa932935a54 +size 3516 diff --git a/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_idle.png b/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_idle.png new file mode 100644 index 000000000..fced7caae --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_idle.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc0393c0b85df389314d15161b14803fde138eb66ef2b055335aef275ac03d1c +size 3538 diff --git a/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_reopened.png b/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_reopened.png new file mode 100644 index 000000000..ba81d4095 --- /dev/null +++ b/tests/egui_tests/tests/snapshots/panel_drag/collapsed_handle_reopened.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bbf9c826403a00937f9e37cccc76b16dc67db8aef3536bf076922fdd2d564d5 +size 4881 diff --git a/tests/egui_tests/tests/test_panel_drag.rs b/tests/egui_tests/tests/test_panel_drag.rs index 7068ba49e..0909753cc 100644 --- a/tests/egui_tests/tests/test_panel_drag.rs +++ b/tests/egui_tests/tests/test_panel_drag.rs @@ -2,6 +2,8 @@ //! //! Covers: //! * [`Panel::show_collapsible`] — drag-to-close on a `Left` panel. +//! * [`Panel::show_collapsible`] — drag-to-open via the grab handle a fully +//! collapsed panel leaves behind, plus [`Panel::drag_to_open`] opting out of it. //! * [`Panel::show_switched`] — drag-to-close on the expanded panel //! followed by drag-to-expand on the collapsed panel, both via the shared //! resize handle. @@ -13,6 +15,15 @@ use egui_kittest::{Harness, SnapshotResults}; #[derive(Default)] struct State { is_expanded: bool, + + /// The panel's live _outer_ width, recorded each pass. + /// + /// `None` while the panel is fully collapsed. + /// + /// We can't read this back from [`egui::PanelState`], because a panel + /// deliberately doesn't persist its size while its resize handle is being + /// dragged — which is exactly when these tests need to observe it. + panel_width: Option, } #[test] @@ -34,7 +45,10 @@ fn drag_to_close_animated_inside() { ui.label("Central"); }); }, - State { is_expanded: true }, + State { + is_expanded: true, + ..Default::default() + }, ); harness.run(); @@ -65,6 +79,156 @@ fn drag_to_close_animated_inside() { results.add(harness.try_snapshot("panel_drag/inside_closed")); } +/// The size range of the collapsible left panel used by the drag-to-open tests. +const MIN_SIZE: f32 = 60.0; +const DEFAULT_SIZE: f32 = 80.0; + +/// A harness with a single collapsible, resizable left panel. +/// +/// `drag_to_open` is passed straight through to [`Panel::drag_to_open`]. +fn collapsible_left_panel_harness(drag_to_open: bool) -> Harness<'static, State> { + Harness::builder() + .with_size(Vec2::new(400.0, 200.0)) + .build_ui_state( + move |ui, state: &mut State| { + let response = Panel::left("test_left_panel") + .resizable(true) + .drag_to_open(drag_to_open) + .default_size(DEFAULT_SIZE) + .min_size(MIN_SIZE) + .show_collapsible(ui, &mut state.is_expanded, |ui| { + ui.label("Left panel content"); + // Without this the frame shrinks to fit the label, and the + // panel's rect would report the content width instead of + // the width the panel was resized to. + ui.take_available_space(); + }); + state.panel_width = response.map(|response| response.response.rect.width()); + egui::CentralPanel::default().show(ui, |ui| { + ui.label("Central"); + }); + }, + State { + is_expanded: true, + panel_width: None, + }, + ) +} + +/// The panel's live _outer_ width, as of the last completed pass. +fn panel_width(harness: &Harness<'_, State>) -> f32 { + harness + .state() + .panel_width + .expect("the panel should be showing") +} + +/// Collapse the panel by dragging its resize edge past `min_size`, and return the +/// panel's fixed (left) edge — where the grab handle it leaves behind sits. +fn collapse_by_drag(harness: &mut Harness<'_, State>) -> Pos2 { + harness.run(); + assert!(harness.state().is_expanded, "should start expanded"); + + // Query the actual resize edge from PanelState (avoids assumptions about + // Frame margins and the harness's ui padding). + let panel_state = egui::PanelState::load(&harness.ctx, egui::Id::new("test_left_panel")) + .expect("PanelState should be persisted after the first frame"); + let fixed_edge = Pos2::new( + panel_state.outer_rect.left(), + panel_state.outer_rect.center().y, + ); + + let drag_start = Pos2::new(panel_state.outer_rect.right(), fixed_edge.y); + let drag_end = Pos2::new(drag_start.x - 200.0, fixed_edge.y); + + harness.drag_at(drag_start); + harness.run(); + harness.hover_at(drag_end); + harness.run(); + harness.drop_at(drag_end); + harness.run(); + + assert!( + !harness.state().is_expanded, + "drag past min_size should have closed the panel" + ); + + // Move the pointer away so the handle isn't left hovered. + harness.hover_at(Pos2::new(300.0, fixed_edge.y)); + harness.run(); + + fixed_edge +} + +#[test] +fn drag_to_open_collapsed_panel() { + let mut results = SnapshotResults::new(); + + let mut harness = collapsible_left_panel_harness(true); + let fixed_edge = collapse_by_drag(&mut harness); + // Grab just inside the fixed edge, where the handle is. + let handle_pos = fixed_edge + Vec2::new(1.0, 0.0); + + // The handle is invisible until hovered: + results.add(harness.try_snapshot("panel_drag/collapsed_handle_idle")); + + harness.hover_at(handle_pos); + harness.run(); + results.add(harness.try_snapshot("panel_drag/collapsed_handle_hovered")); + + // Dragging out but not as far as `min_size` must not reopen the panel. + harness.drag_at(handle_pos); + harness.run(); + let short_of_min = Pos2::new(fixed_edge.x + MIN_SIZE - 10.0, fixed_edge.y); + harness.hover_at(short_of_min); + harness.run(); + assert!( + !harness.state().is_expanded, + "dragging out less than min_size should not reopen the panel" + ); + + // …but continuing past `min_size` should, without releasing the drag. The + // panel opens at the size the pointer is already at, so it never jumps ahead. + let past_min = Pos2::new(fixed_edge.x + MIN_SIZE + 20.0, fixed_edge.y); + harness.hover_at(past_min); + harness.run(); + assert!( + harness.state().is_expanded, + "dragging out past min_size should have reopened the panel" + ); + assert_eq!( + panel_width(&harness), + past_min.x - fixed_edge.x, + "the reopened panel's edge should sit under the pointer" + ); + + harness.drop_at(past_min); + harness.run(); + assert!( + harness.state().is_expanded, + "the panel should stay open after the drag is released" + ); + results.add(harness.try_snapshot("panel_drag/collapsed_handle_reopened")); +} + +#[test] +fn drag_to_open_can_be_opted_out_of() { + let mut harness = collapsible_left_panel_harness(false); + let handle_pos = collapse_by_drag(&mut harness) + Vec2::new(1.0, 0.0); + + harness.drag_at(handle_pos); + harness.run(); + harness.hover_at(Pos2::new(handle_pos.x + 150.0, handle_pos.y)); + harness.run(); + harness.drop_at(Pos2::new(handle_pos.x + 150.0, handle_pos.y)); + harness.run(); + + assert!( + !harness.state().is_expanded, + "with `drag_to_open(false)` there should be no grab handle to reopen the panel with" + ); +} + #[test] fn drag_to_close_and_reopen_animated_between() { let mut results = SnapshotResults::new(); @@ -108,7 +272,10 @@ fn drag_to_close_and_reopen_animated_between() { ui.label("Central"); }); }, - State { is_expanded: true }, + State { + is_expanded: true, + ..Default::default() + }, ); harness.run(); @@ -155,3 +322,174 @@ fn drag_to_close_and_reopen_animated_between() { ); results.add(harness.try_snapshot("panel_drag/between_reopened")); } + +/// State for the animated-close test: records the panel's live top edge. +#[derive(Default)] +struct SwitchedState { + is_expanded: bool, + + /// Bottom of whatever space is left after the panel — i.e. the top edge of + /// the panel that is currently showing. + /// + /// Read from the ui rather than [`egui::PanelState`], which a panel doesn't + /// persist while its resize handle is held. + panel_top: f32, +} + +/// The sizes a `show_switched` bottom panel moves between in these tests. +/// +/// The expanded minimum sits well above the collapsed size, so the gap between +/// the two shows up in the panel's edge. +const SWITCHED_COLLAPSED_SIZE: f32 = 20.0; +const SWITCHED_EXPANDED_MIN: f32 = 80.0; + +fn switched_bottom_panel_harness(start_expanded: bool) -> Harness<'static, SwitchedState> { + let mut harness = Harness::builder() + .with_size(Vec2::new(400.0, 300.0)) + .with_step_dt(1.0 / 60.0) + .build_ui_state( + move |ui, state: &mut SwitchedState| { + Panel::show_switched( + ui, + &mut state.is_expanded, + Panel::bottom("switched_collapsed") + .resizable(true) + .exact_size(SWITCHED_COLLAPSED_SIZE), + Panel::bottom("switched_expanded") + .resizable(true) + .default_size(160.0) + .min_size(SWITCHED_EXPANDED_MIN) + .max_size(250.0), + |ui, _expanded| ui.take_available_space(), + ); + state.panel_top = ui.available_rect_before_wrap().bottom(); + egui::CentralPanel::default().show(ui, |_ui| {}); + }, + SwitchedState { + is_expanded: start_expanded, + ..Default::default() + }, + ); + // kittest disables animations by default, and these tests are about one. + harness + .ctx + .all_styles_mut(|style| style.animation_time = 0.25); + for _ in 0..4 { + harness.step(); + } + harness +} + +/// Assert that the panel edge crossed `gap` gradually, rather than in one frame. +fn assert_crossed_gradually(tops: &[f32], gap: std::ops::Range) { + let frames_in_gap = tops.iter().filter(|top| gap.contains(top)).count(); + assert!( + 3 <= frames_in_gap, + "expected the panel to be animated across the gap between the collapsed \ + size and the expanded min_size, but only {frames_in_gap} frame(s) landed \ + inside {gap:?}: {tops:?}" + ); +} + +/// Dragging the expanded panel shut animates it the rest of the way, rather than +/// snapping, even while the drag is still held. +/// +/// The expanded panel can't shrink past its own `min_size`, so a drag that goes +/// below that leaves a gap between where the panel is stuck and the collapsed +/// panel's size. That gap has to be animated, or the panel jumps. +#[test] +fn drag_to_close_switched_animates_while_held() { + let collapsed_size = SWITCHED_COLLAPSED_SIZE; + let expanded_min = SWITCHED_EXPANDED_MIN; + + let mut harness = switched_bottom_panel_harness(true); + + let expanded = egui::PanelState::load(&harness.ctx, egui::Id::new("switched_expanded")) + .expect("PanelState should be persisted after the first frame"); + let (x, bottom) = (expanded.outer_rect.center().x, expanded.outer_rect.bottom()); + let collapsed_top = bottom - collapsed_size; + + // Drag the top edge down well past the collapsed size, and keep holding. + harness.drag_at(Pos2::new(x, expanded.outer_rect.top())); + harness.step(); + harness.hover_at(Pos2::new(x, bottom - 10.0)); + harness.step(); + + assert!( + !harness.state().is_expanded, + "dragging past the collapsed size should have collapsed the panel" + ); + let top_at_collapse = harness.state().panel_top; + assert_eq!( + top_at_collapse, + bottom - expanded_min, + "the expanded panel should be stuck at its min_size when the collapse fires" + ); + + // Follow the close, still holding the drag. + let mut tops = vec![top_at_collapse]; + for _ in 0..40 { + harness.step(); + tops.push(harness.state().panel_top); + } + + assert!( + tops.windows(2).all(|w| w[0] <= w[1]), + "the panel should only ever move towards being shut, never jump back open: {tops:?}" + ); + assert!( + (tops.last().copied().unwrap_or_default() - collapsed_top).abs() < 1.0, + "the close should end at the collapsed panel's size, got {:?}", + tops.last() + ); + + // The gap between min_size and the collapsed size must be crossed over + // several frames, not in one jump. + assert_crossed_gradually(&tops, (top_at_collapse + 1.0)..(collapsed_top - 1.0)); +} + +/// The mirror image: dragging the collapsed panel open animates across the same +/// gap, instead of snapping straight out to the expanded panel's `min_size`. +#[test] +fn drag_to_open_switched_animates_while_held() { + let mut harness = switched_bottom_panel_harness(false); + + let collapsed = egui::PanelState::load(&harness.ctx, egui::Id::new("switched_collapsed")) + .expect("PanelState should be persisted after the first frame"); + let (x, collapsed_top, bottom) = ( + collapsed.outer_rect.center().x, + collapsed.outer_rect.top(), + collapsed.outer_rect.bottom(), + ); + let expanded_min_top = bottom - SWITCHED_EXPANDED_MIN; + + // Nudge the collapsed panel's top edge out past its `exact_size` cap, and keep + // holding. The pointer stays far short of the expanded panel's `min_size`. + harness.drag_at(Pos2::new(x, collapsed_top)); + harness.step(); + harness.hover_at(Pos2::new(x, collapsed_top - 10.0)); + harness.step(); + + assert!( + harness.state().is_expanded, + "a small outward drag past the collapsed panel's cap should expand it" + ); + + let mut tops = vec![harness.state().panel_top]; + for _ in 0..40 { + harness.step(); + tops.push(harness.state().panel_top); + } + + assert!( + tops.windows(2).all(|w| w[1] <= w[0]), + "the panel should only ever grow, never jump back shut: {tops:?}" + ); + assert!( + (tops.last().copied().unwrap_or_default() - expanded_min_top).abs() < 1.0, + "the panel should settle at the expanded min_size (top {expanded_min_top}), \ + since the pointer never got further out than that, got {:?}", + tops.last() + ); + assert_crossed_gradually(&tops, (expanded_min_top + 1.0)..(collapsed_top - 1.0)); +} From 5347b0a4ac89c1ff8d59019f40e83652867d7392 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Aug 2026 07:54:06 -0700 Subject: [PATCH 29/49] Fix window with a `Grid` being widenable but not shrinkable again (#8386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Related * Fixes a regression from #8152 * Part of #2921 Reported symptom: you can widen the Widget Gallery window, but it won't shrink again. I'm not sure this fix is the best one, but it does work. # Claude says ## Cause A `Grid` gives its **last** column all the available width, so a width-filling widget in it (`Separator`, `TextEdit`, `ProgressBar`, …) makes the `Grid` remember a `col_width` that is really just "however wide we happened to be". At the start of a resize drag, `Resize` runs a one-frame sizing pass (#8152) to measure the minimum content width and clamps the drag against it. But `GridLayout::next_cell` inflated every cell to `prev_state.col_width`, so the `Grid` reported its previous width as its minimum — even though it was only offered `min_size.x`. The clamp is a lower bound, so widening kept working while shrinking was blocked at the widened width. ## Fix During an enclosing sizing pass, don't inflate the stretchy last column to its remembered width, and don't store the measured (narrow) widths. Minimal repro (fails before, passes after — added as a regression test): ```rust Window::new("x").default_width(280.0).show(ctx, |ui| { egui::Grid::new("grid").num_columns(2).show(ui, |ui| { ui.label("Separator"); ui.separator(); // fills the last column ui.end_row(); }); }); ``` `Panel` is unaffected — it clamps only against the user's `min_size`, with no content-min sizing pass. * [x] I have followed the instructions in the PR template 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Lucas Meurer --- crates/egui/src/grid.rs | 16 +++++- crates/egui_kittest/tests/regression_tests.rs | 55 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index e5c20f05f..981025713 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -75,6 +75,11 @@ pub(crate) struct GridLayout { curr_state: State, initial_available: Rect, + /// Are we inside an enclosing sizing pass (e.g. [`crate::Resize`] measuring + /// the minimum content width)? If so we must not remember the (narrow) sizes + /// we measure during it. + sizing_pass: bool, + // Options: num_columns: Option, spacing: Vec2, @@ -90,6 +95,10 @@ pub(crate) struct GridLayout { impl GridLayout { pub(crate) fn new(ui: &Ui, id: Id, prev_state: Option) -> Self { let is_first_frame = prev_state.is_none(); + + // An outer sizing pass, we should render as small as possible. + let sizing_pass = ui.is_sizing_pass(); + let prev_state = prev_state.unwrap_or_default(); // TODO(emilk): respect current layout @@ -110,6 +119,7 @@ impl GridLayout { prev_state, curr_state: State::default(), initial_available, + sizing_pass, num_columns: None, spacing: ui.spacing().item_spacing, @@ -180,7 +190,11 @@ impl GridLayout { } pub(crate) fn next_cell(&self, cursor: Rect, child_size: Vec2) -> Rect { - let width = self.prev_state.col_width(self.col).unwrap_or(0.0); + let width = if self.sizing_pass { + 0.0 + } else { + self.prev_state.col_width(self.col).unwrap_or(0.0) + }; let height = self.prev_row_height(self.row); let size = child_size.max(vec2(width, height)); Rect::from_min_size(cursor.min, size).round_ui() diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 459e2f024..8aad4dea1 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -513,6 +513,61 @@ fn window_resize_wraps_to_content_min_width() { ); } +/// A `Grid` gives its last column all the available width, so a width-filling widget in it +/// (here a `Separator`) makes the grid remember a column width that is really just +/// "however wide the window happened to be". +/// +/// When `Resize` then measures the minimum content width in a sizing pass, that remembered +/// width must not be reported as the minimum — otherwise the window can be widened but +/// never shrunk again. +#[test] +fn window_with_grid_can_shrink_after_being_widened() { + let window_title = "grid_shrink_regression"; + let mut harness = Harness::builder() + .with_size(Vec2::new(800.0, 600.0)) + .build_ui(move |ui| { + Window::new(window_title) + .default_pos([20.0, 20.0]) + .default_width(280.0) + .show(ui.ctx(), |ui| { + egui::Grid::new("grid").num_columns(2).show(ui, |ui| { + ui.label("Separator"); + ui.separator(); // Fills the available width + ui.end_row(); + }); + }); + }); + harness.run(); + + let drag_right_edge = |harness: &mut Harness<'_>, dx: f32| { + let rect = harness + .get_by_role_and_label(Role::Window, window_title) + .rect(); + let grab = Pos2::new(rect.right(), rect.center().y); + harness.hover_at(grab); + harness.run(); + harness.drag_at(grab); + harness.run(); + harness.hover_at(grab + Vec2::new(dx, 0.0)); + harness.run(); + harness.drop_at(grab + Vec2::new(dx, 0.0)); + harness.run(); + harness + .get_by_role_and_label(Role::Window, window_title) + .rect() + .width() + }; + + let widened = drag_right_edge(&mut harness, 300.0); + let shrunk = drag_right_edge(&mut harness, -300.0); + + assert!( + shrunk < widened - 200.0, + "window could not be shrunk again after being widened: \ + widened to {widened}, then only shrunk to {shrunk}" + ); +} + /// Ensure that the size passed to window is actually treated as outer size (including /// margins and borders). #[test] From 90e03028f7c298ff854a34c7e8c0c8c022bdeacf Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 4 Aug 2026 12:40:56 -0700 Subject: [PATCH 30/49] Fix a few nightly clippy lints (#8388) --- crates/egui/src/layout.rs | 6 +++--- crates/egui_demo_lib/src/demo/misc_demo_window.rs | 4 +--- crates/emath/src/easing.rs | 4 +++- crates/emath/src/range.rs | 4 +++- crates/epaint/src/shapes/bezier_shape.rs | 6 +++--- crates/epaint/src/tessellator.rs | 6 ++++-- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/layout.rs b/crates/egui/src/layout.rs index c97652e8d..15152f47b 100644 --- a/crates/egui/src/layout.rs +++ b/crates/egui/src/layout.rs @@ -1,4 +1,4 @@ -use emath::GuiRounding as _; +use emath::{GuiRounding as _, fast_midpoint}; use crate::{ Align, Direction, @@ -477,12 +477,12 @@ impl Layout { // Make sure it isn't negative: if avail.max.x < avail.min.x { - let x = 0.5 * (avail.min.x + avail.max.x); + let x = fast_midpoint(avail.min.x, avail.max.x); avail.min.x = x; avail.max.x = x; } if avail.max.y < avail.min.y { - let y = 0.5 * (avail.min.y + avail.max.y); + let y = fast_midpoint(avail.min.y, avail.max.y); avail.min.y = y; avail.max.y = y; } diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index d035f5186..6b9584dd2 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -143,9 +143,7 @@ impl View for MiscDemoWindow { ) .changed() { - for check in &mut self.checklist { - *check = all_checked; - } + self.checklist.fill(all_checked); } for (i, checked) in self.checklist.iter_mut().enumerate() { ui.checkbox(checked, format!("Item {}", i + 1)); diff --git a/crates/emath/src/easing.rs b/crates/emath/src/easing.rs index 95fc7250d..6a98cad80 100644 --- a/crates/emath/src/easing.rs +++ b/crates/emath/src/easing.rs @@ -7,6 +7,8 @@ //! Derived from . use std::f32::consts::PI; +use crate::fast_midpoint; + #[inline] fn powf(base: f32, exp: f32) -> f32 { base.powf(exp) @@ -116,7 +118,7 @@ pub fn circular_in_out(t: f32) -> f32 { if t < 0.5 { 0.5 * (1. - (1. - 4. * t * t).sqrt()) } else { - 0.5 * ((-(2. * t - 3.) * (2. * t - 1.)).sqrt() + 1.) + fast_midpoint((-(2. * t - 3.) * (2. * t - 1.)).sqrt(), 1.) } } diff --git a/crates/emath/src/range.rs b/crates/emath/src/range.rs index 26d5f6f2c..be6072c71 100644 --- a/crates/emath/src/range.rs +++ b/crates/emath/src/range.rs @@ -1,5 +1,7 @@ use std::ops::{RangeFrom, RangeFull, RangeInclusive, RangeToInclusive}; +use crate::fast_midpoint; + /// Inclusive range of floats, i.e. `min..=max`, but more ergonomic than [`RangeInclusive`]. #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq)] @@ -52,7 +54,7 @@ impl Rangef { /// The center of the range #[inline] pub fn center(self) -> f32 { - 0.5 * (self.min + self.max) + fast_midpoint(self.min, self.max) } #[inline] diff --git a/crates/epaint/src/shapes/bezier_shape.rs b/crates/epaint/src/shapes/bezier_shape.rs index dfeaae5ec..b8c78f273 100644 --- a/crates/epaint/src/shapes/bezier_shape.rs +++ b/crates/epaint/src/shapes/bezier_shape.rs @@ -3,7 +3,7 @@ use std::ops::Range; use crate::{Color32, PathShape, PathStroke, Shape}; -use emath::{Pos2, Rect, RectTransform}; +use emath::{Pos2, Rect, RectTransform, fast_midpoint}; // ---------------------------------------------------------------------------- @@ -689,8 +689,8 @@ fn single_curve_approximation(curve: &CubicBezierShape) -> QuadraticBezierShape let c2_x = (curve.points[2].x * 3.0 - curve.points[3].x) * 0.5; let c2_y = (curve.points[2].y * 3.0 - curve.points[3].y) * 0.5; let c = Pos2 { - x: (c1_x + c2_x) * 0.5, - y: (c1_y + c2_y) * 0.5, + x: fast_midpoint(c1_x, c2_x), + y: fast_midpoint(c1_y, c2_y), }; QuadraticBezierShape { points: [curve.points[0], c, curve.points[3]], diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 9a90636a4..716fe5424 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -5,7 +5,9 @@ #![expect(clippy::identity_op)] -use emath::{GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2, pos2, remap, vec2}; +use emath::{ + GuiRounding as _, NumExt as _, Pos2, Rect, Rot2, Vec2, fast_midpoint, pos2, remap, vec2, +}; use crate::{ CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, @@ -1074,7 +1076,7 @@ fn stroke_and_fill_path( */ let inner_rad = 0.5 * (stroke.width - feathering); - let outer_rad = 0.5 * (stroke.width + feathering); + let outer_rad = fast_midpoint(stroke.width, feathering); match path_type { PathType::Closed => { From e37d44ad8a61ace5c8462e20e826bf254f42fc3e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 5 Aug 2026 00:14:35 -0700 Subject: [PATCH 31/49] Never run an egui pass when nothing will be shown (#8387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes * Alternative to #8385 Not the most simple or beautiful code, but it works, and makes sense. What makes it complex: `app.logic` should still see some input (e.g. what viewports are visible) and emit some output (e.g. "open this link", or "focus and repaint"). ## TODO * [x] test multiple viewports ## Clanker says Instead of teaching egui to skip book-keeping during a pass where no ui is shown, we simply run no pass at all. Then there is nothing to special-case: all ui state is left untouched, and the app finds everything where it left it when the window is shown again. * New `Context::run_logic(&raw_input, f)`: ticks app logic without a pass, returning the `LogicOutput` (platform output + viewport commands) that a pass would otherwise have carried, so e.g. `ViewportCommand::Focus` still reaches the integration. * All three eframe backends (glow, wgpu, web) call `run_logic` instead of `run_ui` when the viewport is minimized/occluded (and has no visible descendant viewport) or, on web, when the tab is hidden. * `App::logic` is still called from inside the pass when the window is visible, so it sees the current frame's input. While hidden, `run_logic` fills in only the window state (`RawInput::viewports` / `focused`), so the app can tell that it is hidden. The ui input (events, time, …) is not interpreted, and is instead given to the next real pass. --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/eframe/src/epi.rs | 6 + crates/eframe/src/native/epi_integration.rs | 102 +++++++++++--- crates/eframe/src/native/glow_integration.rs | 125 +++++++++++----- crates/eframe/src/native/wgpu_integration.rs | 133 +++++++++++++----- crates/eframe/src/native/winit_integration.rs | 12 ++ crates/eframe/src/web/app_runner.rs | 86 ++++++----- crates/egui/src/context.rs | 53 ++++++- crates/egui/src/data/output.rs | 16 +++ crates/egui/src/lib.rs | 2 +- tests/egui_tests/tests/regression_tests.rs | 116 +++++++++++++++ 10 files changed, 519 insertions(+), 132 deletions(-) diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 7de55736a..c10645bea 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -155,6 +155,12 @@ pub trait App { /// /// You may NOT show any ui or do any painting during the call to [`Self::logic`]. /// + /// While the window is hidden, `eframe` runs no egui pass at all (so that no ui state is + /// disturbed), and calls this via [`egui::Context::run_logic`] instead. + /// You can then still tell that the window is hidden with + /// [`egui::InputState::viewport`], but the rest of [`egui::Context::input`] + /// (events, time, …) is that of the last shown frame. + /// /// The [`egui::Context`] can be cloned and saved if you like. /// /// To force another call to [`Self::logic`], call [`egui::Context::request_repaint`] at any time (e.g. from another thread). diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 270c38388..10d64932d 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -156,6 +156,11 @@ pub struct EpiIntegration { pub beginning: Instant, is_first_frame: bool, pub egui_ctx: egui::Context, + + /// Input that we have received, but not yet given to egui, + /// because we haven't run any pass since (see [`Self::update_logic_only`]). + pending_raw_input: egui::RawInput, + pending_full_output: egui::FullOutput, /// When set, it is time to close the native window. @@ -215,6 +220,7 @@ impl EpiIntegration { Self { frame, last_auto_save: Instant::now(), + pending_raw_input: Default::default(), pending_full_output: Default::default(), close: false, can_drag_window: false, @@ -262,59 +268,111 @@ impl EpiIntegration { /// Run user code - this can create immediate viewports, so hold no locks over this! /// - /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::ui`]. + /// If `viewport_ui_cb` is None, we are in the root viewport and will call + /// [`crate::App::logic`] and [`crate::App::ui`]. + /// + /// Only call this when the ui will actually be shown; + /// use [`Self::update_logic_only`] otherwise. pub fn update( &mut self, app: &mut dyn epi::App, viewport_ui_cb: Option<&DeferredViewportUiCallback>, - mut raw_input: egui::RawInput, - is_visible: bool, + raw_input: egui::RawInput, ) -> egui::FullOutput { - raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); + let raw_input = self.prepare_raw_input(app, raw_input); let close_requested = raw_input.viewport().close_requested(); - app.raw_input_hook(&self.egui_ctx, &mut raw_input); + let is_root_viewport = viewport_ui_cb.is_none(); let full_output = self.egui_ctx.run_ui(raw_input, |ui| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport - if is_visible { - profiling::scope!("viewport_callback"); - viewport_ui_cb(ui); - } + profiling::scope!("viewport_callback"); + viewport_ui_cb(ui); } else { { profiling::scope!("App::logic"); app.logic(ui.ctx(), &mut self.frame); } - - if is_visible { - { - profiling::scope!("App::ui"); - app.ui(ui, &mut self.frame); - } + { + profiling::scope!("App::ui"); + app.ui(ui, &mut self.frame); } } }); - let is_root_viewport = viewport_ui_cb.is_none(); if is_root_viewport && close_requested { let canceled = full_output.viewport_output[&ViewportId::ROOT] .commands .contains(&egui::ViewportCommand::CancelClose); - if canceled { - log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose"); - } else { - log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)"); - self.close = true; - } + self.handle_close_request(canceled); } self.pending_full_output.append(full_output); std::mem::take(&mut self.pending_full_output) } + /// Let the app tick its logic without showing any ui, + /// because the window is minimized or occluded. + /// + /// No egui pass is run, so all ui state is left untouched: + /// the app will find everything where it left it once the window is visible again. + /// + /// Only call this for the root viewport: only it has [`crate::App::logic`]. + pub fn update_logic_only( + &mut self, + app: &mut dyn epi::App, + raw_input: egui::RawInput, + ) -> egui::LogicOutput { + let raw_input = self.prepare_raw_input(app, raw_input); + + let close_requested = raw_input.viewport().close_requested(); + + let logic_output = self.egui_ctx.run_logic(&raw_input, |ctx| { + profiling::scope!("App::logic"); + app.logic(ctx, &mut self.frame); + }); + + // No pass consumed the input, so save it for the next one: + self.pending_raw_input = raw_input; + + if close_requested { + let canceled = logic_output + .viewport_commands + .get(&ViewportId::ROOT) + .is_some_and(|commands| commands.contains(&egui::ViewportCommand::CancelClose)); + self.handle_close_request(canceled); + } + + logic_output + } + + /// Prepend any input we couldn't give to egui earlier, set the time, and run the app hook. + fn prepare_raw_input( + &mut self, + app: &mut dyn epi::App, + new_input: egui::RawInput, + ) -> egui::RawInput { + let mut raw_input = std::mem::take(&mut self.pending_raw_input); + raw_input.append(new_input); // The new input wins where they overlap + + raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); + + app.raw_input_hook(&self.egui_ctx, &mut raw_input); + + raw_input + } + + fn handle_close_request(&mut self, canceled: bool) { + if canceled { + log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose"); + } else { + log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)"); + self.close = true; + } + } + pub fn report_frame_time(&mut self, seconds: f32) { self.frame.info.cpu_usage = Some(seconds); } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 451aa0d82..908cc8c26 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -40,7 +40,7 @@ use super::{ use crate::epaint::textures::TexturesDelta; use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, - native::{epi_integration::EpiIntegration, winit_integration::is_invisible_or_minimized}, + native::{epi_integration::EpiIntegration, winit_integration::sleep_if_invisible_or_minimized}, }; // ---------------------------------------------------------------------------- @@ -139,6 +139,27 @@ struct Viewport { egui_winit: Option, } +impl Viewport { + /// Apply the commands, or defer them until we have a window. + fn process_commands( + &mut self, + egui_ctx: &egui::Context, + mut commands: Vec, + ) { + self.deferred_commands.append(&mut commands); + + if let Some(window) = &self.window { + egui_winit::process_viewport_commands( + egui_ctx, + &mut self.info, + std::mem::take(&mut self.deferred_commands), + window, + &mut self.actions_requested, + ); + } + } +} + impl Drop for Viewport { fn drop(&mut self) { // Avoid debug panic when dropping unapplied deltas on teardown @@ -579,7 +600,7 @@ impl GlowWinitRunning<'_> { } } - let (raw_input, viewport_ui_cb, is_visible, run_ui) = { + let (raw_input, viewport_ui_cb, is_visible, show_ui) = { let mut glutin = self.glutin.borrow_mut(); let egui_ctx = glutin.egui_ctx.clone(); let Some(viewport) = glutin.viewports.get_mut(&viewport_id) else { @@ -598,7 +619,7 @@ impl GlowWinitRunning<'_> { let mut raw_input = egui_winit.take_egui_input(window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); - let run_ui = + let show_ui = is_visible || is_viewport_or_descendant_visible(&glutin.viewports, viewport_id); self.integration.pre_update(); @@ -610,9 +631,58 @@ impl GlowWinitRunning<'_> { .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); - (raw_input, viewport_ui_cb, is_visible, run_ui) + (raw_input, viewport_ui_cb, is_visible, show_ui) }; + if !show_ui { + // Nothing will be shown, so we run no egui pass at all. + // That way all ui state is left untouched, and is still there + // when this viewport becomes visible again. + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport { + // The app logic keeps ticking, so it can e.g. ask to be shown again: + let egui::LogicOutput { + platform_output, + viewport_commands, + } = self + .integration + .update_logic_only(self.app.as_mut(), raw_input); + + let mut glutin = self.glutin.borrow_mut(); + if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { + viewport.info.events.clear(); // they should have been processed + if let Some(window) = viewport.window.clone() + && let Some(egui_winit) = viewport.egui_winit.as_mut() + { + egui_winit.handle_platform_output_with_event_loop( + &window, + event_loop, + platform_output, + ); + } + } + for (id, commands) in viewport_commands { + if let Some(viewport) = glutin.viewports.get_mut(&id) { + viewport.process_commands(&self.integration.egui_ctx, commands); + } + } + } + + sleep_if_invisible_or_minimized( + self.glutin + .borrow() + .viewports + .get(&viewport_id) + .and_then(|viewport| viewport.window.as_deref()), + ); + + return Ok(if self.integration.should_close() { + EventResult::CloseRequested + } else { + EventResult::Wait + }); + } + // HACK: In order to get the right clear_color, the system theme needs to be set, which // usually only happens in the `update` call. So we call Options::begin_pass early // to set the right theme. Without this there would be a black flash on the first frame. @@ -661,12 +731,9 @@ impl GlowWinitRunning<'_> { // The update function, which could call immediate viewports, // so make sure we don't hold any locks here required by the immediate viewports rendeer. - let full_output = self.integration.update( - self.app.as_mut(), - viewport_ui_cb.as_deref(), - raw_input, - run_ui, - ); + let full_output = + self.integration + .update(self.app.as_mut(), viewport_ui_cb.as_deref(), raw_input); // ------------------------------------------------------------ @@ -800,14 +867,7 @@ impl GlowWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), Some(&window)); - if is_invisible_or_minimized(&window) { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - // On Windows, an invisible window also uses up all CPU: - // https://github.com/emilk/egui/issues/7776 - profiling::scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); - } + sleep_if_invisible_or_minimized(Some(&window)); if integration.should_close() { Ok(EventResult::CloseRequested) @@ -1380,7 +1440,7 @@ impl GlutinWindowContext { class, builder, viewport_ui_cb, - mut commands, + commands, repaint_delay: _, // ignored - we listened to the repaint callback instead }, ) in viewport_output.clone() @@ -1395,25 +1455,18 @@ impl GlutinWindowContext { viewport_ui_cb, ); - if let Some(window) = &viewport.window { - let old_inner_size = window.inner_size(); + let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size()); - viewport.deferred_commands.append(&mut commands); + viewport.process_commands(egui_ctx, commands); - egui_winit::process_viewport_commands( - egui_ctx, - &mut viewport.info, - std::mem::take(&mut viewport.deferred_commands), - window, - &mut viewport.actions_requested, - ); - - // For Wayland : https://github.com/emilk/egui/issues/4196 - if cfg!(target_os = "linux") { - let new_inner_size = window.inner_size(); - if new_inner_size != old_inner_size { - self.resize(viewport_id, new_inner_size); - } + // For Wayland : https://github.com/emilk/egui/issues/4196 + if cfg!(target_os = "linux") + && let Some(window) = &viewport.window + && let Some(old_inner_size) = old_inner_size + { + let new_inner_size = window.inner_size(); + if new_inner_size != old_inner_size { + self.resize(viewport_id, new_inner_size); } } } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index e01b4d9a3..343c2234a 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -30,7 +30,7 @@ use crate::{ App, AppCreator, CreationContext, NativeOptions, Result, Storage, native::{ epi_integration::EpiIntegration, - winit_integration::{EventResult, is_invisible_or_minimized}, + winit_integration::{EventResult, sleep_if_invisible_or_minimized}, }, }; @@ -624,7 +624,7 @@ impl WgpuWinitRunning<'_> { let mut frame_timer = crate::stopwatch::Stopwatch::new(); frame_timer.start(); - let (viewport_ui_cb, raw_input, is_visible, run_ui) = { + let (viewport_ui_cb, raw_input, is_visible, show_ui) = { profiling::scope!("Prepare"); let mut shared_lock = shared.borrow_mut(); @@ -680,7 +680,7 @@ impl WgpuWinitRunning<'_> { }; let mut raw_input = egui_winit.take_egui_input(window); - let run_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id); + let show_ui = is_visible || is_viewport_or_descendant_visible(viewports, viewport_id); integration.pre_update(); @@ -692,15 +692,67 @@ impl WgpuWinitRunning<'_> { painter.handle_screenshots(&mut raw_input.events); - (viewport_ui_cb, raw_input, is_visible, run_ui) + (viewport_ui_cb, raw_input, is_visible, show_ui) }; + if !show_ui { + // Nothing will be shown, so we run no egui pass at all. + // That way all ui state is left untouched, and is still there + // when this viewport becomes visible again. + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport { + // The app logic keeps ticking, so it can e.g. ask to be shown again: + let egui::LogicOutput { + platform_output, + viewport_commands, + } = integration.update_logic_only(app.as_mut(), raw_input); + + let mut shared_mut = shared.borrow_mut(); + let SharedState { viewports, .. } = &mut *shared_mut; + + if let Some(viewport) = viewports.get_mut(&viewport_id) { + viewport.info.events.clear(); // they should have been processed + if let Viewport { + window: Some(window), + egui_winit: Some(egui_winit), + .. + } = viewport + { + egui_winit.handle_platform_output_with_event_loop( + window, + event_loop, + platform_output, + ); + } + } + + for (id, commands) in viewport_commands { + if let Some(viewport) = viewports.get_mut(&id) { + viewport.process_commands(&integration.egui_ctx, commands); + } + } + } + + sleep_if_invisible_or_minimized( + shared + .borrow() + .viewports + .get(&viewport_id) + .and_then(|viewport| viewport.window.as_deref()), + ); + + return Ok(if integration.should_close() { + EventResult::CloseRequested + } else { + EventResult::Wait + }); + } + // ------------------------------------------------------------ // Runs the update, which could call immediate viewports, // so make sure we hold no locks here! - let full_output = - integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input, run_ui); + let full_output = integration.update(app.as_mut(), viewport_ui_cb.as_deref(), raw_input); // ------------------------------------------------------------ @@ -821,16 +873,7 @@ impl WgpuWinitRunning<'_> { integration.maybe_autosave(app.as_mut(), window.map(|w| w.as_ref())); - if let Some(window) = window - && is_invisible_or_minimized(window) - { - // On Mac, a minimized Window uses up all CPU: - // https://github.com/emilk/egui/issues/325 - // On Windows, an invisible window also uses up all CPU: - // https://github.com/emilk/egui/issues/7776 - profiling::scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); - } + sleep_if_invisible_or_minimized(window.map(|window| window.as_ref())); if integration.should_close() { Ok(EventResult::CloseRequested) @@ -985,6 +1028,25 @@ impl WgpuWinitRunning<'_> { } impl Viewport { + /// Apply the commands, or defer them until we have a window. + fn process_commands( + &mut self, + egui_ctx: &egui::Context, + mut commands: Vec, + ) { + self.deferred_commands.append(&mut commands); + + if let Some(window) = self.window.as_ref() { + egui_winit::process_viewport_commands( + egui_ctx, + &mut self.info, + std::mem::take(&mut self.deferred_commands), + window, + &mut self.actions_requested, + ); + } + } + /// Create winit window, if needed. fn initialize_window( &mut self, @@ -1222,7 +1284,7 @@ fn handle_viewport_output( class, builder, viewport_ui_cb, - mut commands, + commands, repaint_delay: _, // ignored - we listened to the repaint callback instead }, ) in viewport_output.clone() @@ -1232,30 +1294,23 @@ fn handle_viewport_output( let viewport = initialize_or_update_viewport(viewports, ids, class, builder, viewport_ui_cb, painter); - if let Some(window) = viewport.window.as_ref() { - let old_inner_size = window.inner_size(); + let old_inner_size = viewport.window.as_ref().map(|window| window.inner_size()); - viewport.deferred_commands.append(&mut commands); + viewport.process_commands(egui_ctx, commands); - egui_winit::process_viewport_commands( - egui_ctx, - &mut viewport.info, - std::mem::take(&mut viewport.deferred_commands), - window, - &mut viewport.actions_requested, - ); - - // For Wayland : https://github.com/emilk/egui/issues/4196 - if cfg!(target_os = "linux") { - let new_inner_size = window.inner_size(); - if new_inner_size != old_inner_size - && let (Some(width), Some(height)) = ( - NonZeroU32::new(new_inner_size.width), - NonZeroU32::new(new_inner_size.height), - ) - { - painter.on_window_resized(viewport_id, width, height); - } + // For Wayland : https://github.com/emilk/egui/issues/4196 + if cfg!(target_os = "linux") + && let Some(window) = viewport.window.as_ref() + && let Some(old_inner_size) = old_inner_size + { + let new_inner_size = window.inner_size(); + if new_inner_size != old_inner_size + && let (Some(width), Some(height)) = ( + NonZeroU32::new(new_inner_size.width), + NonZeroU32::new(new_inner_size.height), + ) + { + painter.on_window_resized(viewport_id, width, height); } } } diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index b4ec62c09..9aa356de2 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -17,6 +17,18 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool { window.is_visible() == Some(false) || window.is_minimized() == Some(true) } +/// On Mac, a minimized window uses up all CPU: +/// +/// +/// On Windows, an invisible window also uses up all CPU: +/// +pub fn sleep_if_invisible_or_minimized(window: Option<&Window>) { + if window.is_some_and(is_invisible_or_minimized) { + profiling::scope!("minimized_sleep"); + std::thread::sleep(std::time::Duration::from_millis(10)); + } +} + /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { profiling::function_scope!(); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 3364d83ce..b774bcb1f 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -280,45 +280,65 @@ impl AppRunner { .and_then(|v| v.visible()) .unwrap_or(true); - let full_output = self.egui_ctx.run_ui(raw_input, |ui| { - self.app.logic(ui.ctx(), &mut self.frame); - - if is_visible { + if is_visible { + let full_output = self.egui_ctx.run_ui(raw_input, |ui| { + self.app.logic(ui.ctx(), &mut self.frame); self.app.ui(ui, &mut self.frame); - } - }); - let egui::FullOutput { - platform_output, - textures_delta, - shapes, - pixels_per_point, - viewport_output, - } = full_output; + }); + let egui::FullOutput { + platform_output, + textures_delta, + shapes, + pixels_per_point, + viewport_output, + } = full_output; - if viewport_output.len() > 1 { - log::warn!("Multiple viewports not yet supported on the web"); - } - for (_viewport_id, viewport_output) in viewport_output { - for command in viewport_output.commands { - match command { - ViewportCommand::Screenshot(user_data) => { - self.screenshot_commands_with_frame_delay - .push((user_data, 1)); - } - _ => { - // TODO(emilk): handle some of the commands - log::warn!( - "Unhandled egui viewport command: {command:?} - not implemented in web backend" - ); - } - } + if viewport_output.len() > 1 { + log::warn!("Multiple viewports not yet supported on the web"); } - } + self.handle_viewport_commands( + viewport_output + .into_values() + .flat_map(|viewport_output| viewport_output.commands), + ); - self.handle_platform_output(platform_output); - if is_visible || !textures_delta.is_empty() { + self.handle_platform_output(platform_output); self.textures_delta.append(textures_delta); self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); + } else { + // The tab is hidden, so we run no egui pass at all. + // That way all ui state is left untouched, and is still there + // when the tab is shown again. + + let egui::LogicOutput { + platform_output, + viewport_commands, + } = self.egui_ctx.run_logic(&raw_input, |ctx| { + self.app.logic(ctx, &mut self.frame); + }); + + // No pass consumed the input, so save it for the next one: + self.input.raw.append(raw_input); + + self.handle_viewport_commands(viewport_commands.into_values().flatten()); + self.handle_platform_output(platform_output); + } + } + + fn handle_viewport_commands(&mut self, commands: impl Iterator) { + for command in commands { + match command { + ViewportCommand::Screenshot(user_data) => { + self.screenshot_commands_with_frame_delay + .push((user_data, 1)); + } + _ => { + // TODO(emilk): handle some of the commands + log::warn!( + "Unhandled egui viewport command: {command:?} - not implemented in web backend" + ); + } + } } } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index dbb714bed..50f324588 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -32,7 +32,7 @@ use crate::{ load::{self, Bytes, Loaders, SizedTexture}, memory::{Options, Theme}, os::OperatingSystem, - output::FullOutput, + output::{FullOutput, LogicOutput}, pass_state::PassState, plugin::{self, TypedPluginHandle}, resize, response, scroll_area, @@ -888,6 +888,57 @@ impl Context { output } + /// Run app logic without showing any ui. + /// + /// Use this instead of [`Self::run_ui`] when nothing will be shown, + /// e.g. because the window is minimized or occluded, + /// but you still want to let the app tick its logic + /// (so that it can e.g. ask to be shown again with [`ViewportCommand::Focus`]). + /// + /// No pass is run, so `f` must not show any ui. + /// This means everything egui knows about the ui is left untouched: + /// no widget state is garbage-collected, no animation advances, + /// and nothing loses focus. + /// + /// Of `new_input`, only the window state ([`RawInput::viewports`] and + /// [`RawInput::focused`]) is used, so that `f` can tell that the window is hidden. + /// The ui input (events, time, …) is _not_ interpreted, and is left for the next + /// call to [`Self::run_ui`]: [`Self::input`] is otherwise still that of the last pass. + /// + /// The returned [`LogicOutput`] is what [`FullOutput`] would have carried: + /// anything `f` asked the integration to do. + /// There is nothing to paint. + #[must_use] + pub fn run_logic(&self, new_input: &RawInput, logic: impl FnOnce(&Self)) -> LogicOutput { + profiling::function_scope!(); + + let viewport_id = new_input.viewport_id; + + self.write(|ctx| { + // Consume any outstanding repaint request, so that a new request from `logic` + // reaches the integration instead of being considered already served: + ctx.begin_pass_repaint_logic(viewport_id); + + // Tell `logic` about the windows, but leave the ui input alone: + let raw = &mut ctx.viewport_for(viewport_id).input.raw; + raw.viewport_id = viewport_id; + raw.viewports = new_input.viewports.clone(); + raw.focused = new_input.focused; + }); + + logic(self); + + self.write(|ctx| LogicOutput { + platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output), + viewport_commands: ctx + .viewports + .iter_mut() + .filter(|(_, viewport)| !viewport.commands.is_empty()) + .map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands))) + .collect(), + }) + } + /// An alternative to calling [`Self::run_ui`]. /// /// It is usually better to use [`Self::run_ui`], because diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index bbd271b71..ae1363641 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -76,6 +76,22 @@ impl FullOutput { } } +/// What egui emits from [`crate::Context::run_logic`], i.e. from a tick where no ui was shown. +/// +/// There is nothing to paint, but the app may still have asked the integration to do things, +/// e.g. to show a hidden window again with [`crate::ViewportCommand::Focus`]. +#[derive(Clone, Default)] +pub struct LogicOutput { + /// Non-rendering related output. + pub platform_output: PlatformOutput, + + /// The commands sent with [`crate::Context::send_viewport_cmd`] and friends. + /// + /// Note that this contains no information about which viewports exist: + /// the integration should leave its viewports as they are. + pub viewport_commands: OrderedViewportIdMap>, +} + /// Information about text being edited. /// /// Useful for IME. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 88de74b49..27ab7035c 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -467,7 +467,7 @@ pub use self::{ Key, UserData, input::*, output::{ - self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand, + self, CursorIcon, CustomCursorImage, FullOutput, LogicOutput, OpenUrl, OutputCommand, PlatformOutput, UserAttentionType, WidgetInfo, }, }, diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 34527f4cc..1a65254a7 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -559,3 +559,119 @@ fn tooltip_should_hand_over_to_neighboring_widget() { "Tooltip A should be hidden when hovering Button B" ); } + +/// When a window is minimized or occluded, the integration runs no pass at all, +/// and instead ticks the app logic with [`egui::Context::run_logic`]. +/// +/// Such a tick must leave all ui state alone. Otherwise areas think they were hidden and +/// replay their fade-in, popups close, focus is lost, and child viewports pop back up. +/// See . +#[test] +fn run_logic_should_not_disturb_ui_state() { + const MENU: &str = "My menu"; + const MENU_ITEM: &str = "Button in my menu"; + const FOCUSED_BUTTON: &str = "Click me"; + + let child_viewport = egui::ViewportId::from_hash_of("My child viewport"); + let area_id = egui::Id::new("My area"); + let area_layer = egui::LayerId::new(egui::Order::Middle, area_id); + + let mut harness = Harness::builder() + .with_size(Vec2::new(400.0, 300.0)) + .build_ui(move |ui| { + // A backend that can open real windows, like eframe: + ui.ctx().set_embed_viewports(false); + + ui.ctx() + .show_viewport_deferred(child_viewport, Default::default(), |_ui, _class| {}); + + ui.menu_button(MENU, |ui| { + _ = ui.button(MENU_ITEM); + }); + + egui::Area::new(area_id) + .fixed_pos((150.0, 120.0)) + .show(ui.ctx(), |ui| { + _ = ui.button(FOCUSED_BUTTON); + }); + }); + + harness.get_by_label(MENU).click(); + harness.run(); + // Nothing asks for focus again, so the test fails if egui ever loses it: + harness.get_by_label(FOCUSED_BUTTON).focus(); + harness.run(); + + let assert_state = |harness: &Harness<'_>| { + assert!( + harness + .get_by_label(FOCUSED_BUTTON) + .accesskit_node() + .is_focused(), + "The button lost focus" + ); + harness.get_by_label(MENU_ITEM); // Panics if the menu closed + assert!( + harness + .ctx + .memory(|m| m.areas().visible_last_frame(&area_layer)), + "Area state was reset" + ); + assert!( + harness + .ctx + .viewport_for(child_viewport, |viewport| viewport.class) + == egui::ViewportClass::Deferred, + "The child viewport was closed" + ); + }; + + assert_state(&harness); + + // The window is now occluded, so the integration runs no pass, + // and only ticks the app logic: + for i in 0..2 { + let time = 100.0 + f64::from(i); + let mut raw_input = egui::RawInput { + time: Some(time), + ..Default::default() + }; + raw_input + .viewports + .entry(egui::ViewportId::ROOT) + .or_default() + .occluded = Some(true); + + let output = harness.ctx.run_logic(&raw_input, |ctx| { + assert_eq!( + ctx.input(|i| i.viewport().occluded), + Some(true), + "App logic should be able to tell that the window is occluded" + ); + assert!( + ctx.input(|i| i.time) != time, + "The ui input should not be interpreted: it is for the next pass" + ); + + // The app asks to be shown again: + ctx.send_viewport_cmd(egui::ViewportCommand::Focus); + }); + + assert_eq!( + output + .viewport_commands + .into_values() + .flatten() + .collect::>(), + vec![egui::ViewportCommand::Focus], + "The integration should receive the command, even though there was no pass" + ); + + assert_state(&harness); + } + + // The window is visible again, and everything should be where we left it: + harness.run(); + + assert_state(&harness); +} From 2397194d5e494b55322d0153a312d1e79936c92e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 5 Aug 2026 12:55:25 +0200 Subject: [PATCH 32/49] Release 0.36.0 - Improved mobile keyboard support (#8390) --- CHANGELOG.md | 56 ++++++++++++++++++++++++ Cargo.lock | 34 +++++++------- Cargo.toml | 28 ++++++------ crates/ecolor/CHANGELOG.md | 3 ++ crates/eframe/CHANGELOG.md | 14 ++++++ crates/egui-wgpu/CHANGELOG.md | 6 +++ crates/egui-winit/CHANGELOG.md | 4 ++ crates/egui_extras/CHANGELOG.md | 4 ++ crates/egui_glow/CHANGELOG.md | 4 ++ crates/egui_inspection/CHANGELOG.md | 12 +++++ crates/egui_kittest/CHANGELOG.md | 6 +++ crates/emath/CHANGELOG.md | 4 ++ crates/epaint/CHANGELOG.md | 5 +++ crates/epaint_default_fonts/CHANGELOG.md | 4 ++ 14 files changed, 153 insertions(+), 31 deletions(-) create mode 100644 crates/egui_inspection/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c309e231..7194ccc5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,62 @@ 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.36.0 - 2026-08-05 + +### Highlights ✨ + +This release drastically improves the mobile keyboard experience (when using eframe web). It also adds drag-to-open +panels, window chrome theme sync and a lot of small bug fixes and improvements! + +#### Improved mobile keyboard support + +Autocomplete, autocorrect and IMEs now work correctly on iOS and android (on eframe web)! + +https://github.com/user-attachments/assets/b0aa1084-0755-4e47-a0b3-0ce6890aca85 + +- via [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho) (and [@rustbasic](https://github.com/rustbasic) who helped a lot with testing) + +#### Drag to reopen panels + +You can now reopen closed panels by dragging the handle: + +https://github.com/user-attachments/assets/e02895c4-248e-4e22-9694-bdfcd9839bfd + +#### Window decoration theme is now synced with app theme + +Previously, when switching themes, the window chrome (OS titlebar) stayed in the OS theme. Now, it syncs with the app theme: + +https://github.com/user-attachments/assets/6b393057-fdfc-431b-b997-744bc183a8ac + + +### ⭐ Added +* Add `BoxedWidget`: dynamically dispatched widgets [#8378](https://github.com/emilk/egui/pull/8378) by [@emilk](https://github.com/emilk) +* Add `WidgetText::size` [#8377](https://github.com/emilk/egui/pull/8377) by [@emilk](https://github.com/emilk) +* Add `LayoutJob::clear` [#8376](https://github.com/emilk/egui/pull/8376) by [@emilk](https://github.com/emilk) +* Sync window theme with egui theme [#8299](https://github.com/emilk/egui/pull/8299) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `egui::Window::title_frame` [#8353](https://github.com/emilk/egui/pull/8353) by [@Its-Just-Nans](https://github.com/Its-Just-Nans) +* Add `extra_text_line_spacing` to control vertical spacing between text lines [#8040](https://github.com/emilk/egui/pull/8040) by [@rustbasic](https://github.com/rustbasic) +* Add drag-to-open for collapsible panels [#8363](https://github.com/emilk/egui/pull/8363) by [@emilk](https://github.com/emilk) + +### 🔧 Changed +* Rerun `sizing_pass` when reopening popup [#8315](https://github.com/emilk/egui/pull/8315) by [@yay](https://github.com/yay) +* Update MSRV from 1.92 to 1.95 [#8348](https://github.com/emilk/egui/pull/8348) by [@emilk](https://github.com/emilk) +* Treat a press that leaves a widget as a drag [#8365](https://github.com/emilk/egui/pull/8365) by [@emilk](https://github.com/emilk) + +### 🔥 Removed +* Remove `Modifiers` from `RawInput` and make it a `egui::Event` [#8336](https://github.com/emilk/egui/pull/8336) by [@lucasmerlin](https://github.com/lucasmerlin) +* Remove `clip_rect_margin` [#8366](https://github.com/emilk/egui/pull/8366) by [@emilk](https://github.com/emilk) + +### 🐛 Fixed +* Improve backtrace trimming for cranelift [#8294](https://github.com/emilk/egui/pull/8294) by [@emilk](https://github.com/emilk) +* Prevent accidentally dropping `TexturesDelta` [#8356](https://github.com/emilk/egui/pull/8356) by [@lucasmerlin](https://github.com/lucasmerlin) +* Make non-interactive tooltips not interactable [#8362](https://github.com/emilk/egui/pull/8362) by [@lucasmerlin](https://github.com/lucasmerlin) +* Panels: Take separator line width into account [#8367](https://github.com/emilk/egui/pull/8367) by [@emilk](https://github.com/emilk) +* Fix TextEdit hint text not following horizontal_align/vertical_align [#8332](https://github.com/emilk/egui/pull/8332) by [@thedavidweng](https://github.com/thedavidweng) +* Fix ScrollArea failure by handling horizontal and vertical scrolling separately in the missing place [#8275](https://github.com/emilk/egui/pull/8275) by [@rustbasic](https://github.com/rustbasic) +* Fix window with a `Grid` being widenable but not shrinkable again [#8386](https://github.com/emilk/egui/pull/8386) by [@emilk](https://github.com/emilk) + + ## 0.35.0 - 2026-06-25 - Inspection, egui_mcp, classes and improved IME ### Highlights diff --git a/Cargo.lock b/Cargo.lock index adf431072..97791c6f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1243,7 +1243,7 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" -version = "0.35.0" +version = "0.36.0" dependencies = [ "bytemuck", "cint", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.35.0" +version = "0.36.0" dependencies = [ "ahash", "bytemuck", @@ -1293,7 +1293,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.35.0" +version = "0.36.0" dependencies = [ "accesskit", "ahash", @@ -1315,7 +1315,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.35.0" +version = "0.36.0" dependencies = [ "ahash", "bytemuck", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.35.0" +version = "0.36.0" dependencies = [ "accesskit_winit", "arboard", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.35.0" +version = "0.36.0" dependencies = [ "accesskit", "accesskit_consumer", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.35.0" +version = "0.36.0" dependencies = [ "criterion", "document-features", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.35.0" +version = "0.36.0" dependencies = [ "ahash", "document-features", @@ -1423,7 +1423,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.35.0" +version = "0.36.0" dependencies = [ "bytemuck", "document-features", @@ -1439,7 +1439,7 @@ dependencies = [ [[package]] name = "egui_inspection" -version = "0.35.0" +version = "0.36.0" dependencies = [ "document-features", "egui", @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.35.0" +version = "0.36.0" dependencies = [ "dify", "document-features", @@ -1473,7 +1473,7 @@ dependencies = [ [[package]] name = "egui_tests" -version = "0.35.0" +version = "0.36.0" dependencies = [ "egui", "egui_extras", @@ -1503,7 +1503,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "emath" -version = "0.35.0" +version = "0.36.0" dependencies = [ "bytemuck", "document-features", @@ -1601,7 +1601,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.35.0" +version = "0.36.0" dependencies = [ "ahash", "bytemuck", @@ -1630,7 +1630,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.35.0" +version = "0.36.0" [[package]] name = "equivalent" @@ -3594,7 +3594,7 @@ dependencies = [ [[package]] name = "popups" -version = "0.35.0" +version = "0.36.0" dependencies = [ "eframe", "env_logger", @@ -5903,7 +5903,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.35.0" +version = "0.36.0" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index f310dd9af..fc8010f24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" -version = "0.35.0" +version = "0.36.0" [profile.release] @@ -56,19 +56,19 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.35.0", path = "crates/emath", default-features = false } -ecolor = { version = "0.35.0", path = "crates/ecolor", default-features = false } -epaint = { version = "0.35.0", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.35.0", path = "crates/epaint_default_fonts" } -egui = { version = "0.35.0", path = "crates/egui", default-features = false } -egui-winit = { version = "0.35.0", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.35.0", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.35.0", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.35.0", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.35.0", path = "crates/egui_glow", default-features = false } -egui_inspection = { version = "0.35.0", path = "crates/egui_inspection", default-features = false } -egui_kittest = { version = "0.35.0", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.35.0", path = "crates/eframe", default-features = false } +emath = { version = "0.36.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.36.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.36.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.36.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.36.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.36.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.36.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.36.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.36.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.36.0", path = "crates/egui_glow", default-features = false } +egui_inspection = { version = "0.36.0", path = "crates/egui_inspection", default-features = false } +egui_kittest = { version = "0.36.0", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.36.0", path = "crates/eframe", default-features = false } accesskit = "0.24.1" accesskit_consumer = "0.35.0" # Can't update to 0.36+: kittest 0.4 pins accesskit_consumer 0.35, so bumping splits it into two versions diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 340a0c4ae..349a6e52f 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,9 @@ 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.36.0 - 2026-08-05 +Nothing new + ## 0.35.0 - 2026-06-25 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index d65e3699a..0e904260f 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,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.36.0 - 2026-08-05 +### 🔧 Changed +* Improve robustness of text input handling for `eframe/web` [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho) +* Eframe: make webbrowser dependency optional [#8372](https://github.com/emilk/egui/pull/8372) by [@wyvernbw](https://github.com/wyvernbw) +* Store `web_sys::File` inside of `DroppedFile` [#8354](https://github.com/emilk/egui/pull/8354) by [@grtlr](https://github.com/grtlr) + +### 🐛 Fixed +* Web: don't scroll host page when text agent or canvas grabs focus [#8296](https://github.com/emilk/egui/pull/8296) by [@emilk](https://github.com/emilk) +* Fix missing modifier events on eframe web, handle physical keys [#8345](https://github.com/emilk/egui/pull/8345) by [@lucasmerlin](https://github.com/lucasmerlin) +* Web: Avoid panic from lost texture updates when loaded on a background tab [#8313](https://github.com/emilk/egui/pull/8313) by [@kevinmehall](https://github.com/kevinmehall) +* Web: anchor the text agent to the canvas [#8297](https://github.com/emilk/egui/pull/8297) by [@emilk](https://github.com/emilk) +* Never run an egui pass when nothing will be shown [#8387](https://github.com/emilk/egui/pull/8387) by [@emilk](https://github.com/emilk) + + ## 0.35.0 - 2026-06-25 ### ⭐ Added * Add Context::set_cursor_image for OS-level custom cursors [#8155](https://github.com/emilk/egui/pull/8155) by [@all3f0r1](https://github.com/all3f0r1) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 6d0d1085d..e45767f52 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,12 @@ 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.36.0 - 2026-08-05 +* Upgrade wgpu to v30 [#8289](https://github.com/emilk/egui/pull/8289) by [@akx](https://github.com/akx) +* Fix: ensure mapped range is dropped before unmapping buffer in capture [#8337](https://github.com/emilk/egui/pull/8337) by [@MagicCrazyMan](https://github.com/MagicCrazyMan) +* Make wgpu Instance public [#8321](https://github.com/emilk/egui/pull/8321) by [@oleflb](https://github.com/oleflb) + + ## 0.35.0 - 2026-06-25 * Call `pre_present_notify` before presenting [#8089](https://github.com/emilk/egui/pull/8089) by [@dimtpap](https://github.com/dimtpap) * Wgpu: Allow configuring VSync and frame latency at runtime [#8114](https://github.com/emilk/egui/pull/8114) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 536294ddc..f9cc12307 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.36.0 - 2026-08-05 +Nothing new + + ## 0.35.0 - 2026-06-25 * Delegate handling of IME interruptions to integrations to fix virtual keyboard flickering on web [#8078](https://github.com/emilk/egui/pull/8078) by [@umajho](https://github.com/umajho) * Always enable windows undecorated shadows [#8169](https://github.com/emilk/egui/pull/8169) by [@Wumpf](https://github.com/Wumpf) diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index a6b2470a4..a42cb4d62 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.36.0 - 2026-08-05 +Nothing new + + ## 0.35.0 - 2026-06-25 * Improve FileLoader file uri to path handling for windows [#8163](https://github.com/emilk/egui/pull/8163) by [@aconbere](https://github.com/aconbere) * Add arbitrary request headers to `EhttpLoader` [#8121](https://github.com/emilk/egui/pull/8121) by [@frnsys](https://github.com/frnsys) diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 988189427..78197938a 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.36.0 - 2026-08-05 +* Add `egui_inspection::Request::Settle` [#8344](https://github.com/emilk/egui/pull/8344) by [@lucasmerlin](https://github.com/lucasmerlin) + + +## 0.35.0 - Initial release diff --git a/crates/egui_kittest/CHANGELOG.md b/crates/egui_kittest/CHANGELOG.md index 2f6483aaf..a588a42a5 100644 --- a/crates/egui_kittest/CHANGELOG.md +++ b/crates/egui_kittest/CHANGELOG.md @@ -6,6 +6,12 @@ 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.36.0 - 2026-08-05 +* Handle `ViewportCommand::InnerSize` in `egui_kittest` [#8350](https://github.com/emilk/egui/pull/8350) by [@lucasmerlin](https://github.com/lucasmerlin) +* Report failing pixels by threshold when a kittest snapshot fails [#8360](https://github.com/emilk/egui/pull/8360) by [@emilk](https://github.com/emilk) +* Rename `failed_pixel_count_threshold` to `max_failed_pixels` [#8383](https://github.com/emilk/egui/pull/8383) by [@emilk](https://github.com/emilk) + + ## 0.35.0 - 2026-06-25 * Add `HarnessBuilder::with_render_options()` (closes #7630) [#8060](https://github.com/emilk/egui/pull/8060) by [@MichaelGrupp](https://github.com/MichaelGrupp) * Add `Harness::spawn_eframe_app` [#8120](https://github.com/emilk/egui/pull/8120) by [@emilk](https://github.com/emilk) diff --git a/crates/emath/CHANGELOG.md b/crates/emath/CHANGELOG.md index 4aae540c5..d0cdae830 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.36.0 - 2026-08-05 +Nothing new + + ## 0.35.0 - 2026-06-25 Nothing new diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index 4bb868bd3..15c27e4b4 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,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.36.0 - 2026-08-05 +* Add `LayoutJob::clear` [#8376](https://github.com/emilk/egui/pull/8376) by [@emilk](https://github.com/emilk) +* Add `extra_text_line_spacing` to control vertical spacing between text lines [#8040](https://github.com/emilk/egui/pull/8040) by [@rustbasic](https://github.com/rustbasic) + + ## 0.35.0 - 2026-06-25 ### ⭐ Added * Make the size of tabs and thin space configurable [#8070](https://github.com/emilk/egui/pull/8070) by [@emilk](https://github.com/emilk) diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 67a0a9234..8ee27c5c0 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.36.0 - 2026-08-05 +Nothing new + + ## 0.35.0 - 2026-06-25 Nothing new From 2a5f3d99b5034416efa0fc5e508c4ecc2e02e1b4 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 6 Aug 2026 04:00:45 -0700 Subject: [PATCH 33/49] Remove redundant lint entries from Cargo.toml (#8393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 30 lints in `[workspace.lints]` were set to `warn` despite already being warn-by-default, or part of a lint group that is already enabled. * `rust`: `elided_lifetimes_in_paths` and `unused_extern_crates` are in `rust_2018_idioms`; `semicolon_in_expressions_from_macros` is in `future_incompatible`; `unexpected_cfgs` and `unsafe_op_in_unsafe_fn` are warn-by-default (the latter since edition 2024); `rust_2021_prelude_collisions` never fires on edition 2021+. * `rustdoc`: `broken_intra_doc_links` and `missing_crate_level_docs` are in `rustdoc::all`. * `clippy`: 22 lints that are in `clippy::all`. The explicit `allow`s are kept, even though they are no-ops today, to record our intent in case we ever enable `pedantic`. No behavior change: `cargo clippy --all-features --all-targets` gives the same output before and after. * [x] I have followed the instructions in the PR template 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Cargo.toml | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fc8010f24..6e3c44b20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -164,16 +164,10 @@ winit = { version = "0.30.13", default-features = false } [workspace.lints.rust] unsafe_code = "deny" -elided_lifetimes_in_paths = "warn" future_incompatible = { level = "warn", priority = -1 } nonstandard_style = { level = "warn", priority = -1 } rust_2018_idioms = { level = "warn", priority = -1 } -rust_2021_prelude_collisions = "warn" -semicolon_in_expressions_from_macros = "warn" trivial_numeric_casts = "warn" -unexpected_cfgs = "warn" -unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 -unused_extern_crates = "warn" unused_import_braces = "warn" unused_lifetimes = "warn" @@ -182,8 +176,6 @@ unused_qualifications = "allow" [workspace.lints.rustdoc] all = "warn" -missing_crate_level_docs = "warn" -broken_intra_doc_links = "warn" # See also clippy.toml [workspace.lints.clippy] @@ -191,10 +183,8 @@ all = { level = "warn", priority = -1 } allow_attributes = "warn" as_ptr_cast_mut = "warn" -await_holding_lock = "warn" bool_to_int_with_if = "warn" branches_sharing_code = "warn" -char_lit_as_u8 = "warn" checked_conversions = "warn" clear_with_drain = "warn" clone_on_ref_ptr = "warn" @@ -205,11 +195,7 @@ debug_assert_with_mut_call = "warn" decimal_bitwise_operands = "warn" default_union_representation = "warn" derive_partial_eq_without_eq = "warn" -disallowed_macros = "warn" # See clippy.toml -disallowed_methods = "warn" # See clippy.toml -disallowed_names = "warn" # See clippy.toml disallowed_script_idents = "warn" # See clippy.toml -disallowed_types = "warn" # See clippy.toml doc_broken_link = "warn" doc_comment_double_space_linebreaks = "warn" doc_include_without_cfg = "warn" @@ -219,7 +205,6 @@ duration_suboptimal_units = "warn" elidable_lifetime_names = "warn" empty_enum_variants_with_brackets = "warn" empty_enums = "warn" -empty_line_after_outer_attr = "warn" enum_glob_use = "warn" equatable_if_let = "warn" exit = "warn" @@ -236,11 +221,9 @@ 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" inconsistent_struct_constructor = "warn" index_refutable_slice = "warn" @@ -262,23 +245,19 @@ large_include_file = "warn" large_stack_arrays = "warn" large_stack_frames = "warn" large_types_passed_by_value = "warn" -let_unit_value = "warn" linkedlist = "warn" literal_string_with_formatting_args = "warn" lossy_float_literal = "warn" macro_use_imports = "warn" manual_assert = "warn" -manual_clamp = "warn" manual_ilog2 = "warn" manual_instant_elapsed = "warn" manual_is_power_of_two = "warn" manual_is_variant_and = "warn" manual_let_else = "warn" manual_midpoint = "warn" # NOTE `midpoint` is often a lot slower for floats, so we have our own `emath::fast_midpoint` function. -manual_ok_or = "warn" manual_string_new = "warn" map_err_ignore = "warn" -map_flatten = "warn" match_bool = "warn" match_same_arms = "warn" match_wild_err_arm = "warn" @@ -286,14 +265,10 @@ match_wildcard_for_single_variants = "warn" mem_forget = "warn" 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" mutex_integer = "warn" -needless_borrow = "warn" needless_continue = "warn" needless_for_each = "warn" needless_pass_by_ref_mut = "warn" @@ -304,7 +279,6 @@ negative_feature_names = "warn" non_std_lazy_statics = "warn" non_zero_suggestions = "warn" nonstandard_macro_braces = "warn" -only_used_in_recursion = "warn" option_as_ref_cloned = "warn" option_option = "warn" or_fun_call = "warn" @@ -318,7 +292,6 @@ ptr_cast_constness = "warn" pub_underscore_fields = "warn" pub_without_shorthand = "warn" rc_mutex = "warn" -readonly_write_lock = "warn" redundant_type_annotations = "warn" ref_as_ptr = "warn" ref_option = "warn" @@ -340,11 +313,9 @@ string_add = "warn" string_add_assign = "warn" string_lit_as_bytes = "warn" string_lit_chars_any = "warn" -suspicious_command_arg_space = "warn" suspicious_xor_used_as_pow = "warn" todo = "warn" too_long_first_doc_paragraph = "warn" -too_many_arguments = "warn" trailing_empty_array = "warn" trait_duplication_in_bounds = "warn" transmute_ptr_to_ptr = "warn" @@ -373,13 +344,14 @@ unused_trait_names = "warn" unwrap_used = "warn" use_self = "warn" useless_let_if_seq = "warn" -useless_transmute = "warn" verbose_file_reads = "warn" wildcard_dependencies = "warn" zero_sized_map_values = "warn" # TODO(emilk): maybe enable more of these lints? +# NOTE: these are all in `pedantic`/`restriction`/`nursery`, so the `allow` is a no-op today. +# We keep them to record our intent in case we ever enable those groups. cast_possible_wrap = "allow" comparison_chain = "allow" should_panic_without_expect = "allow" From 6aea7eff948b46a5676bb327877c1ec44847e0d1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 6 Aug 2026 04:19:13 -0700 Subject: [PATCH 34/49] Enable the `clippy::std_instead_of_core` lint (#8394) Prefer `core::` over `std::` where either work * Part of https://github.com/emilk/egui/issues/5735 --- Cargo.toml | 1 + crates/ecolor/src/color32.rs | 14 ++--- crates/ecolor/src/hex_color_runtime.rs | 6 +-- crates/ecolor/src/rgba.rs | 22 ++++---- crates/eframe/src/epi.rs | 16 +++--- crates/eframe/src/lib.rs | 12 ++--- crates/eframe/src/native/app_icon.rs | 2 +- crates/eframe/src/native/epi_integration.rs | 8 +-- .../eframe/src/native/event_loop_context.rs | 4 +- crates/eframe/src/native/file_storage.rs | 6 +-- crates/eframe/src/native/glow_integration.rs | 9 ++-- crates/eframe/src/native/run.rs | 9 ++-- crates/eframe/src/native/wgpu_integration.rs | 7 +-- crates/eframe/src/native/winit_integration.rs | 2 +- crates/eframe/src/web/app_runner.rs | 2 +- crates/eframe/src/web/dropped_file.rs | 7 +-- crates/eframe/src/web/input.rs | 2 +- crates/eframe/src/web/text_agent.rs | 7 +-- crates/eframe/src/web/web_painter_wgpu.rs | 2 +- crates/eframe/src/web/web_runner.rs | 13 ++--- crates/egui-wgpu/src/capture.rs | 2 +- crates/egui-wgpu/src/lib.rs | 6 +-- crates/egui-wgpu/src/renderer.rs | 20 +++++--- crates/egui-wgpu/src/setup.rs | 12 ++--- crates/egui-wgpu/src/winit.rs | 5 +- crates/egui/src/atomics/atom_kind.rs | 4 +- crates/egui/src/atomics/atom_layout.rs | 4 +- crates/egui/src/atomics/atoms.rs | 8 +-- crates/egui/src/cache/cache_storage.rs | 10 ++-- crates/egui/src/cache/cache_trait.rs | 2 +- crates/egui/src/cache/frame_cache.rs | 2 +- crates/egui/src/cache/frame_publisher.rs | 2 +- crates/egui/src/callstack.rs | 4 +- crates/egui/src/containers/close_tag.rs | 7 +-- .../egui/src/containers/collapsing_header.rs | 2 +- crates/egui/src/containers/frame.rs | 4 +- crates/egui/src/containers/popup.rs | 8 +-- crates/egui/src/containers/scroll_area.rs | 8 +-- crates/egui/src/containers/window.rs | 4 +- crates/egui/src/context.rs | 51 ++++++++++--------- crates/egui/src/data/input/dropped_file.rs | 4 +- crates/egui/src/data/input/ime_event.rs | 2 +- crates/egui/src/data/input/modifiers.rs | 8 +-- crates/egui/src/data/input/raw_input.rs | 6 +-- .../egui/src/data/input/safe_area_insets.rs | 2 +- crates/egui/src/data/input/viewport_info.rs | 4 +- crates/egui/src/data/output.rs | 16 +++--- crates/egui/src/data/user_data.rs | 9 ++-- crates/egui/src/debug_text.rs | 4 +- crates/egui/src/drag_and_drop.rs | 3 +- crates/egui/src/id.rs | 16 +++--- crates/egui/src/id_salt.rs | 10 ++-- crates/egui/src/input_state/mod.rs | 6 +-- crates/egui/src/input_state/touch_state.rs | 5 +- crates/egui/src/interaction.rs | 2 +- crates/egui/src/layers.rs | 4 +- crates/egui/src/load.rs | 17 +++---- crates/egui/src/memory/mod.rs | 22 ++++---- crates/egui/src/painter.rs | 4 +- crates/egui/src/plugin.rs | 24 ++++----- crates/egui/src/response.rs | 9 ++-- crates/egui/src/sense.rs | 4 +- crates/egui/src/style.rs | 21 ++++---- .../egui/src/text_selection/cursor_range.rs | 4 +- .../text_selection/label_text_selection.rs | 6 +-- .../src/text_selection/text_cursor_state.rs | 2 +- crates/egui/src/text_selection/visuals.rs | 6 +-- crates/egui/src/ui.rs | 7 +-- crates/egui/src/ui_stack.rs | 2 +- crates/egui/src/util/fixed_cache.rs | 6 +-- crates/egui/src/util/id_type_map.rs | 17 ++++--- crates/egui/src/util/undoer.rs | 4 +- crates/egui/src/viewport.rs | 14 ++--- crates/egui/src/widget_style.rs | 2 +- crates/egui/src/widget_text.rs | 6 +-- crates/egui/src/widgets/drag_value.rs | 4 +- crates/egui/src/widgets/image.rs | 7 +-- crates/egui/src/widgets/progress_bar.rs | 2 +- crates/egui/src/widgets/slider.rs | 2 +- crates/egui/src/widgets/spinner.rs | 2 +- crates/egui/src/widgets/text_edit/builder.rs | 4 +- crates/egui/src/widgets/text_edit/state.rs | 2 +- .../egui/src/widgets/text_edit/text_buffer.rs | 21 ++++---- .../src/accessibility_inspector.rs | 4 +- .../egui_demo_app/src/apps/custom3d_wgpu.rs | 2 +- .../egui_demo_app/src/apps/fractal_clock.rs | 4 +- crates/egui_demo_app/src/backend_panel.rs | 8 +-- crates/egui_demo_app/src/main.rs | 4 +- crates/egui_demo_app/src/wrap_app.rs | 6 +-- crates/egui_demo_lib/benches/benchmark.rs | 2 +- .../egui_demo_lib/src/demo/dancing_strings.rs | 2 +- .../src/demo/demo_app_windows.rs | 2 +- .../src/demo/misc_demo_window.rs | 14 ++--- crates/egui_demo_lib/src/demo/sliders.rs | 2 +- .../egui_demo_lib/src/demo/tests/grid_test.rs | 2 +- .../src/demo/tests/input_test.rs | 2 +- crates/egui_extras/src/datepicker/button.rs | 2 +- crates/egui_extras/src/datepicker/mod.rs | 2 +- crates/egui_extras/src/datepicker/popup.rs | 2 +- crates/egui_extras/src/loaders/file_loader.rs | 3 +- crates/egui_extras/src/loaders/gif_loader.rs | 3 +- crates/egui_extras/src/loaders/http_loader.rs | 3 +- .../egui_extras/src/loaders/image_loader.rs | 5 +- crates/egui_extras/src/loaders/svg_loader.rs | 3 +- crates/egui_extras/src/loaders/webp_loader.rs | 3 +- crates/egui_extras/src/syntax_highlighting.rs | 6 +-- crates/egui_glow/examples/pure_glow.rs | 10 ++-- crates/egui_glow/src/painter.rs | 8 +-- crates/egui_glow/src/shader_version.rs | 2 +- crates/egui_glow/src/winit.rs | 4 +- crates/egui_inspection/src/plugin.rs | 2 +- crates/egui_inspection/src/protocol.rs | 2 +- crates/egui_kittest/src/builder.rs | 2 +- crates/egui_kittest/src/lib.rs | 10 ++-- crates/egui_kittest/src/node.rs | 4 +- crates/egui_kittest/src/renderer.rs | 2 +- crates/egui_kittest/src/snapshot.rs | 16 +++--- crates/egui_kittest/src/texture_to_image.rs | 4 +- crates/egui_kittest/src/wgpu.rs | 4 +- crates/egui_kittest/tests/regression_tests.rs | 4 +- crates/emath/src/align.rs | 8 +-- crates/emath/src/easing.rs | 2 +- crates/emath/src/history.rs | 16 +++--- crates/emath/src/lib.rs | 8 +-- crates/emath/src/numeric.rs | 12 ++--- crates/emath/src/ordered_float.rs | 8 +-- crates/emath/src/pos2.rs | 6 +-- crates/emath/src/range.rs | 2 +- crates/emath/src/rect.rs | 4 +- crates/emath/src/rect_transform.rs | 4 +- crates/emath/src/rot2.rs | 20 ++++---- crates/emath/src/ts_transform.rs | 6 +-- crates/emath/src/vec2.rs | 10 ++-- crates/emath/src/vec2b.rs | 6 +-- crates/epaint/benches/benchmark.rs | 2 +- crates/epaint/src/color.rs | 5 +- crates/epaint/src/corner_radius.rs | 24 ++++----- crates/epaint/src/corner_radius_f32.rs | 20 ++++---- crates/epaint/src/image.rs | 8 +-- crates/epaint/src/margin.rs | 28 +++++----- crates/epaint/src/margin_f32.rs | 28 +++++----- crates/epaint/src/mesh.rs | 6 +-- crates/epaint/src/mutex.rs | 6 +-- crates/epaint/src/shadow.rs | 2 +- crates/epaint/src/shapes/bezier_shape.rs | 6 +-- crates/epaint/src/shapes/paint_callback.rs | 11 ++-- crates/epaint/src/shapes/rect_shape.rs | 4 +- crates/epaint/src/shapes/shape.rs | 4 +- crates/epaint/src/shapes/text_shape.rs | 2 +- crates/epaint/src/stats.rs | 10 ++-- crates/epaint/src/stroke.rs | 7 +-- crates/epaint/src/tessellator.rs | 2 +- crates/epaint/src/text/cursor.rs | 12 ++--- crates/epaint/src/text/font.rs | 6 +-- crates/epaint/src/text/fonts.rs | 18 +++---- crates/epaint/src/text/index.rs | 20 ++++---- crates/epaint/src/text/text_layout.rs | 8 +-- crates/epaint/src/text/text_layout_types.rs | 30 +++++------ crates/epaint/src/texture_atlas.rs | 2 +- crates/epaint/src/texture_handle.rs | 4 +- crates/epaint/src/textures.rs | 10 ++-- crates/epaint/src/util/mod.rs | 4 +- examples/external_eventloop/src/main.rs | 3 +- examples/external_eventloop_async/src/app.rs | 3 +- examples/file_dialog/src/main.rs | 2 +- examples/hello_world_par/src/main.rs | 4 +- examples/multiple_viewports/src/main.rs | 6 +-- examples/puffin_profiler/src/main.rs | 10 ++-- examples/serial_windows/src/main.rs | 4 +- examples/user_attention/src/main.rs | 3 +- tests/egui_tests/tests/regression_tests.rs | 2 +- tests/egui_tests/tests/test_atoms.rs | 2 +- tests/egui_tests/tests/test_panel_drag.rs | 2 +- tests/test_background_logic/src/main.rs | 4 +- tests/test_inline_glow_paint/src/main.rs | 2 +- xtask/src/main.rs | 2 +- 176 files changed, 633 insertions(+), 615 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6e3c44b20..124719eff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -307,6 +307,7 @@ set_contains_or_insert = "warn" single_char_pattern = "warn" single_match_else = "warn" single_option_map = "warn" +std_instead_of_core = "warn" str_split_at_newline = "warn" str_to_string = "warn" string_add = "warn" diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index f444b860d..68a8fc3d6 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -30,15 +30,15 @@ use crate::{Rgba, fast_round, linear_f32_from_linear_u8}; #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] pub struct Color32(pub(crate) [u8; 4]); -impl std::fmt::Debug for Color32 { +impl core::fmt::Debug for Color32 { /// Prints the contents with premultiplied alpha! - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let [r, g, b, a] = self.0; write!(f, "#{r:02X}_{g:02X}_{b:02X}_{a:02X}") } } -impl std::ops::Index for Color32 { +impl core::ops::Index for Color32 { type Output = u8; #[inline] @@ -47,7 +47,7 @@ impl std::ops::Index for Color32 { } } -impl std::ops::IndexMut for Color32 { +impl core::ops::IndexMut for Color32 { #[inline] fn index_mut(&mut self, index: usize) -> &mut u8 { &mut self.0[index] @@ -378,7 +378,7 @@ impl Color32 { } } -impl std::ops::Mul for Color32 { +impl core::ops::Mul for Color32 { type Output = Self; /// Fast gamma-space multiplication. @@ -393,7 +393,7 @@ impl std::ops::Mul for Color32 { } } -impl std::ops::Add for Color32 { +impl core::ops::Add for Color32 { type Output = Self; #[inline] @@ -489,7 +489,7 @@ mod test { } else { // There will be small rounding errors whenever the alpha is not 0 or 255, // because we multiply and then unmultiply the alpha. - for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) { + for (&a, &b) in core::iter::zip(&in_rgba, &out_rgba) { assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}"); } } diff --git a/crates/ecolor/src/hex_color_runtime.rs b/crates/ecolor/src/hex_color_runtime.rs index 21e07ffc4..5acca2ef7 100644 --- a/crates/ecolor/src/hex_color_runtime.rs +++ b/crates/ecolor/src/hex_color_runtime.rs @@ -3,7 +3,7 @@ //! Supports the 3, 4, 6, and 8-digit formats, according to the specification in //! -use std::{fmt::Display, str::FromStr}; +use core::{fmt::Display, str::FromStr}; use crate::Color32; @@ -31,7 +31,7 @@ pub enum HexColor { pub enum ParseHexColorError { MissingHash, InvalidLength, - InvalidInt(std::num::ParseIntError), + InvalidInt(core::num::ParseIntError), } impl FromStr for HexColor { @@ -45,7 +45,7 @@ impl FromStr for HexColor { } impl Display for HexColor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Hex3(color) => { let [r, g, b, _] = color.to_srgba_unmultiplied().map(|u| u >> 4); diff --git a/crates/ecolor/src/rgba.rs b/crates/ecolor/src/rgba.rs index 98c3ce408..ecb8cb1d0 100644 --- a/crates/ecolor/src/rgba.rs +++ b/crates/ecolor/src/rgba.rs @@ -9,7 +9,7 @@ use crate::Color32; #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] pub struct Rgba(pub(crate) [f32; 4]); -impl std::ops::Index for Rgba { +impl core::ops::Index for Rgba { type Output = f32; #[inline] @@ -18,7 +18,7 @@ impl std::ops::Index for Rgba { } } -impl std::ops::IndexMut for Rgba { +impl core::ops::IndexMut for Rgba { #[inline] fn index_mut(&mut self, index: usize) -> &mut f32 { &mut self.0[index] @@ -27,20 +27,20 @@ impl std::ops::IndexMut for Rgba { /// Deterministically hash an `f32`, treating all NANs as equal, and ignoring the sign of zero. #[inline] -pub(crate) fn f32_hash(state: &mut H, f: f32) { +pub(crate) fn f32_hash(state: &mut H, f: f32) { if f == 0.0 { state.write_u8(0); } else if f.is_nan() { state.write_u8(1); } else { - use std::hash::Hash as _; + use core::hash::Hash as _; f.to_bits().hash(state); } } -impl std::hash::Hash for Rgba { +impl core::hash::Hash for Rgba { #[inline] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { crate::f32_hash(state, self.0[0]); crate::f32_hash(state, self.0[1]); crate::f32_hash(state, self.0[2]); @@ -219,7 +219,7 @@ impl Rgba { } } -impl std::ops::Add for Rgba { +impl core::ops::Add for Rgba { type Output = Self; #[inline] @@ -233,7 +233,7 @@ impl std::ops::Add for Rgba { } } -impl std::ops::Mul for Rgba { +impl core::ops::Mul for Rgba { type Output = Self; #[inline] @@ -247,7 +247,7 @@ impl std::ops::Mul for Rgba { } } -impl std::ops::Mul for Rgba { +impl core::ops::Mul for Rgba { type Output = Self; #[inline] @@ -261,7 +261,7 @@ impl std::ops::Mul for Rgba { } } -impl std::ops::Mul for f32 { +impl core::ops::Mul for f32 { type Output = Rgba; #[inline] @@ -336,7 +336,7 @@ mod test { } else { // There will be small rounding errors whenever the alpha is not 0 or 255, // because we multiply and then unmultiply the alpha. - for (&a, &b) in std::iter::zip(&in_rgba, &out_rgba) { + for (&a, &b) in core::iter::zip(&in_rgba, &out_rgba) { assert!(a.abs_diff(b) <= 3, "{in_rgba:?} != {out_rgba:?}"); } } diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index c10645bea..a971097a0 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -7,7 +7,7 @@ #![warn(missing_docs)] // Let's keep `epi` well-documented. #[cfg(target_arch = "wasm32")] -use std::any::Any; +use core::any::Any; #[cfg(not(target_arch = "wasm32"))] #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] @@ -41,7 +41,7 @@ pub type EventLoopBuilderHook = Box) #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] pub type WindowBuilderHook = Box egui::ViewportBuilder>; -type DynError = Box; +type DynError = Box; /// This is how your app is created. /// @@ -73,7 +73,7 @@ pub struct CreationContext<'s> { /// The `get_proc_address` wrapper of underlying GL context #[cfg(feature = "glow")] pub get_proc_address: - Option *const std::ffi::c_void + Send + Sync>>, + Option *const core::ffi::c_void + Send + Sync>>, /// The underlying WGPU render state. /// @@ -231,8 +231,8 @@ pub trait App { // Settings: /// Time between automatic calls to [`Self::save`] - fn auto_save_interval(&self) -> std::time::Duration { - std::time::Duration::from_secs(30) + fn auto_save_interval(&self) -> core::time::Duration { + core::time::Duration::from_secs(30) } /// Background color values for the app, e.g. what is sent to `gl.clearColor`. @@ -621,8 +621,8 @@ impl Default for Renderer { } #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] -impl std::fmt::Display for Renderer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Renderer { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { #[cfg(feature = "glow")] Self::Glow => "glow".fmt(f), @@ -634,7 +634,7 @@ impl std::fmt::Display for Renderer { } #[cfg(any(feature = "glow", feature = "wgpu_no_default_features"))] -impl std::str::FromStr for Renderer { +impl core::str::FromStr for Renderer { type Err = String; fn from_str(name: &str) -> Result { diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 0e7259177..55caffdd6 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -503,7 +503,7 @@ pub fn run_ui_native( #[derive(Debug)] pub enum Error { /// Something went wrong in user code when creating the app. - AppCreation(Box), + AppCreation(Box), /// An error from [`winit`]. #[cfg(not(target_arch = "wasm32"))] @@ -519,7 +519,7 @@ pub enum Error { /// An error from [`glutin`] when using [`glow`]. #[cfg(all(feature = "glow", not(target_arch = "wasm32")))] - NoGlutinConfigs(glutin::config::ConfigTemplate, Box), + NoGlutinConfigs(glutin::config::ConfigTemplate, Box), /// An error from [`glutin`] when using [`glow`]. #[cfg(feature = "glow")] @@ -530,7 +530,7 @@ pub enum Error { Wgpu(egui_wgpu::WgpuError), } -impl std::error::Error for Error {} +impl core::error::Error for Error {} #[cfg(not(target_arch = "wasm32"))] impl From for Error { @@ -572,8 +572,8 @@ impl From for Error { } } -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::AppCreation(err) => write!(f, "app creation error: {err}"), @@ -614,4 +614,4 @@ impl std::fmt::Display for Error { } /// Short for `Result`. -pub type Result = std::result::Result; +pub type Result = core::result::Result; diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 85be6754b..9fdeb30e0 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -123,7 +123,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { ) .is_err() { - return std::ptr::null_mut(); + return core::ptr::null_mut(); } // SAFETY: Creating an HICON which should be readonly on our data. diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 10d64932d..b81225638 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -83,7 +83,7 @@ pub fn viewport_builder( } } - match std::mem::take(&mut native_options.window_builder) { + match core::mem::take(&mut native_options.window_builder) { Some(hook) => hook(viewport_builder), None => viewport_builder, } @@ -310,7 +310,7 @@ impl EpiIntegration { } self.pending_full_output.append(full_output); - std::mem::take(&mut self.pending_full_output) + core::mem::take(&mut self.pending_full_output) } /// Let the app tick its logic without showing any ui, @@ -354,7 +354,7 @@ impl EpiIntegration { app: &mut dyn epi::App, new_input: egui::RawInput, ) -> egui::RawInput { - let mut raw_input = std::mem::take(&mut self.pending_raw_input); + let mut raw_input = core::mem::take(&mut self.pending_raw_input); raw_input.append(new_input); // The new input wins where they overlap raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); @@ -379,7 +379,7 @@ impl EpiIntegration { pub fn post_rendering(&mut self, window: &winit::window::Window) { profiling::function_scope!(); - if std::mem::take(&mut self.is_first_frame) { + if core::mem::take(&mut self.is_first_frame) { // We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 window.set_visible(true); } diff --git a/crates/eframe/src/native/event_loop_context.rs b/crates/eframe/src/native/event_loop_context.rs index 810db8e1f..d2081543d 100644 --- a/crates/eframe/src/native/event_loop_context.rs +++ b/crates/eframe/src/native/event_loop_context.rs @@ -1,4 +1,4 @@ -use std::cell::Cell; +use core::cell::Cell; use winit::event_loop::ActiveEventLoop; thread_local! { @@ -14,7 +14,7 @@ impl EventLoopGuard { cell.get().is_none(), "Attempted to set a new event loop while one is already set" ); - cell.set(Some(std::ptr::from_ref::(event_loop))); + cell.set(Some(core::ptr::from_ref::(event_loop))); }); Self } diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index 830fdcc24..f6f4ef477 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -44,10 +44,10 @@ pub fn storage_dir(app_id: &str) -> Option { #[cfg(all(windows, not(target_vendor = "uwp")))] #[expect(unsafe_code)] fn roaming_appdata() -> Option { + use core::ptr; + use core::slice; use std::ffi::OsString; use std::os::windows::ffi::OsStringExt as _; - use std::ptr; - use std::slice; use windows_sys::Win32::Foundation::S_OK; use windows_sys::Win32::System::Com::CoTaskMemFree; @@ -66,7 +66,7 @@ fn roaming_appdata() -> Option { SHGetKnownFolderPath( &FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY as u32, - std::ptr::null_mut(), + core::ptr::null_mut(), &mut path_raw, ) }; diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 908cc8c26..1b9469cfd 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -8,7 +8,8 @@ #![expect(clippy::undocumented_unsafe_blocks)] #![expect(clippy::unwrap_used)] -use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use core::{cell::RefCell, num::NonZeroU32}; +use std::{rc::Rc, sync::Arc, time::Instant}; use egui_winit::ActionRequested; use glutin::{ @@ -152,7 +153,7 @@ impl Viewport { egui_winit::process_viewport_commands( egui_ctx, &mut self.info, - std::mem::take(&mut self.deferred_commands), + core::mem::take(&mut self.deferred_commands), window, &mut self.actions_requested, ); @@ -338,7 +339,7 @@ impl<'app> GlowWinitApp<'app> { log::warn!("set_cursor_hittest(false) failed: {err}"); } - let app_creator = std::mem::take(&mut self.app_creator) + let app_creator = core::mem::take(&mut self.app_creator) .expect("Single-use AppCreator has unexpectedly already been taken"); crate::maybe_attach_inspection_plugin(&integration.egui_ctx, Some(self.app_name.clone())); @@ -1408,7 +1409,7 @@ impl GlutinWindowContext { } } - fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { + fn get_proc_address(&self, addr: &core::ffi::CStr) -> *const core::ffi::c_void { self.gl_config.display().get_proc_address(addr) } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 345cc3e2c..53979591d 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -1,4 +1,5 @@ -use std::time::{Duration, Instant}; +use core::time::Duration; +use std::time::Instant; use winit::{ application::ApplicationHandler, @@ -41,7 +42,7 @@ fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result( mut native_options: epi::NativeOptions, f: impl FnOnce(&mut EventLoop, epi::NativeOptions) -> R, ) -> Result { - thread_local!(static EVENT_LOOP: std::cell::RefCell>> = const { std::cell::RefCell::new(None) }); + thread_local!(static EVENT_LOOP: core::cell::RefCell>> = const { core::cell::RefCell::new(None) }); EVENT_LOOP.with(|event_loop| { // Since we want to reference NativeOptions when creating the EventLoop we can't @@ -550,7 +551,7 @@ impl<'a> EframeWinitApplication<'a> { pub fn pump_eframe_app( &mut self, event_loop: &mut EventLoop, - timeout: Option, + timeout: Option, ) -> EframePumpStatus { use winit::platform::pump_events::{EventLoopExtPumpEvents as _, PumpStatus}; diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index 343c2234a..4a94a5d49 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -5,7 +5,8 @@ //! There is a bunch of improvements we could do, //! like removing a bunch of `unwraps`. -use std::{cell::RefCell, num::NonZeroU32, rc::Rc, sync::Arc, time::Instant}; +use core::{cell::RefCell, num::NonZeroU32}; +use std::{rc::Rc, sync::Arc, time::Instant}; use egui_winit::ActionRequested; use parking_lot::Mutex; @@ -309,7 +310,7 @@ impl<'app> WgpuWinitApp<'app> { egui_winit.init_accesskit(event_loop, &window, event_loop_proxy); } - let app_creator = std::mem::take(&mut self.app_creator) + let app_creator = core::mem::take(&mut self.app_creator) .expect("Single-use AppCreator has unexpectedly already been taken"); crate::maybe_attach_inspection_plugin(&egui_ctx, Some(self.app_name.clone())); @@ -1040,7 +1041,7 @@ impl Viewport { egui_winit::process_viewport_commands( egui_ctx, &mut self.info, - std::mem::take(&mut self.deferred_commands), + core::mem::take(&mut self.deferred_commands), window, &mut self.actions_requested, ); diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 9aa356de2..5e767201e 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -25,7 +25,7 @@ pub fn is_invisible_or_minimized(window: &Window) -> bool { pub fn sleep_if_invisible_or_minimized(window: Option<&Window>) { if window.is_some_and(is_invisible_or_minimized) { profiling::scope!("minimized_sleep"); - std::thread::sleep(std::time::Duration::from_millis(10)); + std::thread::sleep(core::time::Duration::from_millis(10)); } } diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index b774bcb1f..548bdf4b5 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -344,7 +344,7 @@ impl AppRunner { /// Paint the results of the last call to [`Self::logic`]. pub fn paint(&mut self) { - let clipped_primitives = std::mem::take(&mut self.clipped_primitives); + let clipped_primitives = core::mem::take(&mut self.clipped_primitives); if let Some(clipped_primitives) = clipped_primitives { let mut screenshot_commands = vec![]; diff --git a/crates/eframe/src/web/dropped_file.rs b/crates/eframe/src/web/dropped_file.rs index 45e454cc5..2924e6d46 100644 --- a/crates/eframe/src/web/dropped_file.rs +++ b/crates/eframe/src/web/dropped_file.rs @@ -1,8 +1,5 @@ -use std::{ - future::Future, - path::{Path, PathBuf}, - pin::Pin, -}; +use core::{future::Future, pin::Pin}; +use std::path::{Path, PathBuf}; #[derive(Debug)] pub(crate) struct WebFile { diff --git a/crates/eframe/src/web/input.rs b/crates/eframe/src/web/input.rs index 62723e8fa..bd0195646 100644 --- a/crates/eframe/src/web/input.rs +++ b/crates/eframe/src/web/input.rs @@ -32,7 +32,7 @@ pub fn primary_touch_pos( event: &web_sys::TouchEvent, ) -> Option<(egui::Pos2, web_sys::Touch)> { // On touchend we don't get anything in `touches`, but we still get `changed_touches`, so include those: - let all_touches: Vec<_> = std::iter::chain( + let all_touches: Vec<_> = core::iter::chain( (0..event.touches().length()).filter_map(|i| event.touches().get(i)), (0..event.changed_touches().length()).filter_map(|i| event.changed_touches().get(i)), ) diff --git a/crates/eframe/src/web/text_agent.rs b/crates/eframe/src/web/text_agent.rs index b80882769..d9a12347b 100644 --- a/crates/eframe/src/web/text_agent.rs +++ b/crates/eframe/src/web/text_agent.rs @@ -1,7 +1,8 @@ //! The text agent is a hidden `` element used to capture //! IME and mobile keyboard input events. -use std::{cell::RefCell, rc::Rc}; +use core::cell::RefCell; +use std::rc::Rc; use wasm_bindgen::prelude::*; @@ -366,7 +367,7 @@ impl InputState { &self, text: &str, prefix_len_chars: usize, - ) -> Option> { + ) -> Option> { let selection_start = self.input.selection_start().unwrap_or(None)? as usize; let selection_end = self.input.selection_end().unwrap_or(None)? as usize; @@ -444,7 +445,7 @@ impl InputState { } fn longest_common_prefix_length(a: &str, b: &str) -> usize { - std::iter::zip(a.chars(), b.chars()) + core::iter::zip(a.chars(), b.chars()) .take_while(|(a, b)| a == b) .count() } diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 273675475..afb4ee2f9 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -368,7 +368,7 @@ impl WebPainter for WebPainterWgpu { // Submit the commands: both the main buffer and user-defined ones. render_state .queue - .submit(std::iter::chain(user_cmd_bufs, [encoder.finish()])); + .submit(core::iter::chain(user_cmd_bufs, [encoder.finish()])); if let Some((frame, capture_buffer)) = frame_and_capture_buffer { if let Some(capture_buffer) = capture_buffer diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 2bf842ab0..528855192 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -1,4 +1,5 @@ -use std::{cell::RefCell, rc::Rc}; +use core::cell::RefCell; +use std::rc::Rc; use wasm_bindgen::prelude::*; @@ -107,7 +108,7 @@ impl WebRunner { fn unsubscribe_from_all_events(&self) { let events_to_unsubscribe: Vec<_> = - std::mem::take(&mut *self.events_to_unsubscribe.borrow_mut()); + core::mem::take(&mut *self.events_to_unsubscribe.borrow_mut()); if !events_to_unsubscribe.is_empty() { log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len()); @@ -139,7 +140,7 @@ impl WebRunner { /// Returns `None` if there has been a panic, or if we have been destroyed. /// In that case, just return to JS. - pub(crate) fn try_lock(&self) -> Option> { + pub(crate) fn try_lock(&self) -> Option> { if self.panic_handler.has_panicked() { // Unsubscribe from all events so that we don't get any more callbacks // that will try to access the poisoned runner. @@ -147,7 +148,7 @@ impl WebRunner { None } else { let lock = self.app_runner.try_borrow_mut().ok()?; - std::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() }) + core::cell::RefMut::filter_map(lock, |lock| -> Option<&mut AppRunner> { lock.as_mut() }) .ok() } } @@ -158,9 +159,9 @@ impl WebRunner { /// and return `None` if this runner has panicked. pub fn app_mut( &self, - ) -> Option> { + ) -> Option> { self.try_lock() - .map(|lock| std::cell::RefMut::map(lock, |runner| runner.app_mut::())) + .map(|lock| core::cell::RefMut::map(lock, |runner| runner.app_mut::())) } /// Convenience function to reduce boilerplate and ensure that all event handlers diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs index c5519e8a6..54ddeb884 100644 --- a/crates/egui-wgpu/src/capture.rs +++ b/crates/egui-wgpu/src/capture.rs @@ -255,7 +255,7 @@ struct BufferPadding { impl BufferPadding { fn new(width: u32) -> Self { - let bytes_per_pixel = std::mem::size_of::() as u32; + let bytes_per_pixel = core::mem::size_of::() as u32; let unpadded_bytes_per_row = width * bytes_per_pixel; let padded_bytes_per_row = wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 195167177..1eb975aee 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -358,8 +358,8 @@ fn wgpu_config_impl_send_sync() { assert_send_sync::(); } -impl std::fmt::Debug for WgpuConfiguration { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for WgpuConfiguration { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { surface, wgpu_setup, @@ -486,7 +486,7 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { // > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: "" // > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: "" - use std::fmt::Write as _; + use core::fmt::Write as _; let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}"); diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index 2ff9f7e4a..de8808de3 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -1,4 +1,5 @@ -use std::{borrow::Cow, num::NonZeroU64, ops::Range}; +use core::{num::NonZeroU64, ops::Range}; +use std::borrow::Cow; use ahash::HashMap; use bytemuck::Zeroable as _; @@ -299,7 +300,9 @@ impl Renderer { visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { has_dynamic_offset: false, - min_binding_size: NonZeroU64::new(std::mem::size_of::() as _), + min_binding_size: NonZeroU64::new( + core::mem::size_of::() as _ + ), ty: wgpu::BufferBindingType::Uniform, }, count: None, @@ -434,9 +437,9 @@ impl Renderer { }; const VERTEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = - (std::mem::size_of::() * 1024) as _; + (core::mem::size_of::() * 1024) as _; const INDEX_BUFFER_START_CAPACITY: wgpu::BufferAddress = - (std::mem::size_of::() * 1024 * 3) as _; + (core::mem::size_of::() * 1024 * 3) as _; Self { pipeline, @@ -962,7 +965,7 @@ impl Renderer { self.index_buffer.slices.clear(); - let required_index_buffer_size = (std::mem::size_of::() * index_count) as u64; + let required_index_buffer_size = (core::mem::size_of::() * index_count) as u64; if self.index_buffer.capacity < required_index_buffer_size { // Resize index buffer if needed. self.index_buffer.capacity = @@ -989,7 +992,7 @@ impl Renderer { for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { match primitive { Primitive::Mesh(mesh) => { - let size = mesh.indices.len() * std::mem::size_of::(); + let size = mesh.indices.len() * core::mem::size_of::(); let slice = index_offset..(size + index_offset); index_buffer_staging .slice(slice.clone()) @@ -1006,7 +1009,8 @@ impl Renderer { self.vertex_buffer.slices.clear(); - let required_vertex_buffer_size = (std::mem::size_of::() * vertex_count) as u64; + let required_vertex_buffer_size = + (core::mem::size_of::() * vertex_count) as u64; if self.vertex_buffer.capacity < required_vertex_buffer_size { // Resize vertex buffer if needed. self.vertex_buffer.capacity = @@ -1034,7 +1038,7 @@ impl Renderer { for epaint::ClippedPrimitive { primitive, .. } in paint_jobs { match primitive { Primitive::Mesh(mesh) => { - let size = mesh.vertices.len() * std::mem::size_of::(); + let size = mesh.vertices.len() * core::mem::size_of::(); let slice = vertex_offset..(size + vertex_offset); vertex_buffer_staging .slice(slice.clone()) diff --git a/crates/egui-wgpu/src/setup.rs b/crates/egui-wgpu/src/setup.rs index f2733a6e7..3909a9b53 100644 --- a/crates/egui-wgpu/src/setup.rs +++ b/crates/egui-wgpu/src/setup.rs @@ -9,7 +9,7 @@ use std::sync::Arc; /// Automatically implemented for all types that satisfy the bounds /// (including [`winit::event_loop::OwnedDisplayHandle`]). pub trait EguiDisplayHandle: - wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static + wgpu::rwh::HasDisplayHandle + core::fmt::Debug + Send + Sync + 'static { /// Clone into a `Box` for [`wgpu::InstanceDescriptor::display`]. fn clone_for_wgpu(&self) -> Box; @@ -27,7 +27,7 @@ impl Clone for Box { impl EguiDisplayHandle for T where - T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static, + T: wgpu::rwh::HasDisplayHandle + Clone + core::fmt::Debug + Send + Sync + 'static, { fn clone_for_wgpu(&self) -> Box { Box::new(self.clone()) @@ -77,8 +77,8 @@ impl WgpuSetup { } } -impl std::fmt::Debug for WgpuSetup { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for WgpuSetup { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::CreateNew(create_new) => f .debug_tuple("WgpuSetup::CreateNew") @@ -295,8 +295,8 @@ impl Clone for WgpuSetupCreateNew { } } -impl std::fmt::Debug for WgpuSetupCreateNew { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for WgpuSetupCreateNew { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { instance_descriptor, display_handle, diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 62b5d9197..84b32927f 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -8,8 +8,9 @@ use crate::{ RendererOptions, capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel}, }; +use core::num::NonZeroU32; use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet}; -use std::{num::NonZeroU32, sync::Arc}; +use std::sync::Arc; struct SurfaceState { surface: wgpu::Surface<'static>, @@ -727,7 +728,7 @@ impl Painter { let start = web_time::Instant::now(); render_state .queue - .submit(std::iter::chain(user_cmd_bufs, [encoded])); + .submit(core::iter::chain(user_cmd_bufs, [encoded])); vsync_sec += start.elapsed().as_secs_f32(); }; diff --git a/crates/egui/src/atomics/atom_kind.rs b/crates/egui/src/atomics/atom_kind.rs index f996c173b..0dfe56069 100644 --- a/crates/egui/src/atomics/atom_kind.rs +++ b/crates/egui/src/atomics/atom_kind.rs @@ -1,7 +1,7 @@ use crate::{AtomLayout, FontSelection, Image, ImageSource, SizedAtomKind, Ui, WidgetText}; +use core::fmt::Debug; use emath::Vec2; use epaint::text::TextWrapMode; -use std::fmt::Debug; /// Args passed when sizing an [`super::Atom`] pub struct IntoSizedArgs { @@ -90,7 +90,7 @@ impl Clone for AtomKind<'_> { } impl Debug for AtomKind<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { AtomKind::Empty => write!(f, "AtomKind::Empty"), AtomKind::Text(text) => write!(f, "AtomKind::Text({text:?})"), diff --git a/crates/egui/src/atomics/atom_layout.rs b/crates/egui/src/atomics/atom_layout.rs index ceb43e048..5c7da5cc7 100644 --- a/crates/egui/src/atomics/atom_layout.rs +++ b/crates/egui/src/atomics/atom_layout.rs @@ -2,11 +2,11 @@ use crate::{ AtomKind, Atoms, Direction, FontSelection, Frame, Id, Image, IntoAtoms, Response, Sense, SizedAtom, SizedAtomKind, Stroke, Ui, Widget, text_selection::LabelSelectionState, }; +use core::ops::{Deref, DerefMut}; use emath::{Align2, GuiRounding as _, NumExt as _, Rect, Vec2}; use epaint::text::TextWrapMode; use epaint::{Color32, Galley}; use smallvec::SmallVec; -use std::ops::{Deref, DerefMut}; use std::sync::Arc; /// The `(main, cross)` axis indices for `direction`, for indexing a [`Vec2`] (0 = x, 1 = y). @@ -557,7 +557,7 @@ impl<'atom> SizedAtomLayout<'atom> { F: FnMut(SizedAtomKind<'atom>) -> SizedAtomKind<'atom>, { for kind in self.iter_kinds_mut() { - *kind = f(std::mem::take(kind)); + *kind = f(core::mem::take(kind)); } } diff --git a/crates/egui/src/atomics/atoms.rs b/crates/egui/src/atomics/atoms.rs index 460d4732c..501d879e0 100644 --- a/crates/egui/src/atomics/atoms.rs +++ b/crates/egui/src/atomics/atoms.rs @@ -1,6 +1,6 @@ use crate::{Atom, AtomKind, Image, WidgetText}; +use core::ops::{Deref, DerefMut}; use std::borrow::Cow; -use std::ops::{Deref, DerefMut}; /// A list of [`Atom`]s. /// @@ -41,7 +41,7 @@ impl<'a> Atoms<'a> { /// /// If you have weird lifetime issues with this, use [`Self::push_left`] in a loop instead. pub fn extend_left(&mut self, mut atoms: Self) { - std::mem::swap(&mut atoms.0, &mut self.0); + core::mem::swap(&mut atoms.0, &mut self.0); self.0.extend(atoms.0); } @@ -128,7 +128,7 @@ impl<'a> Atoms<'a> { pub fn map_atoms(&mut self, mut f: impl FnMut(Atom<'a>) -> Atom<'a>) { self.iter_mut() - .for_each(|atom| *atom = f(std::mem::take(atom))); + .for_each(|atom| *atom = f(core::mem::take(atom))); } pub fn map_kind(&mut self, mut f: F) @@ -136,7 +136,7 @@ impl<'a> Atoms<'a> { F: FnMut(AtomKind<'a>) -> AtomKind<'a>, { for kind in self.iter_kinds_mut() { - *kind = f(std::mem::take(kind)); + *kind = f(core::mem::take(kind)); } } diff --git a/crates/egui/src/cache/cache_storage.rs b/crates/egui/src/cache/cache_storage.rs index e0aa65e8c..b3533f4cf 100644 --- a/crates/egui/src/cache/cache_storage.rs +++ b/crates/egui/src/cache/cache_storage.rs @@ -23,18 +23,18 @@ use super::CacheTrait; /// ``` #[derive(Default)] pub struct CacheStorage { - caches: ahash::HashMap>, + caches: ahash::HashMap>, } impl CacheStorage { pub fn cache(&mut self) -> &mut Cache { let cache = self .caches - .entry(std::any::TypeId::of::()) + .entry(core::any::TypeId::of::()) .or_insert_with(|| Box::::default()); #[expect(clippy::unwrap_used)] - (cache.as_mut() as &mut dyn std::any::Any) + (cache.as_mut() as &mut dyn core::any::Any) .downcast_mut::() .unwrap() } @@ -60,8 +60,8 @@ impl Clone for CacheStorage { } } -impl std::fmt::Debug for CacheStorage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for CacheStorage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!( f, "FrameCacheStorage[{} caches with {} elements]", diff --git a/crates/egui/src/cache/cache_trait.rs b/crates/egui/src/cache/cache_trait.rs index 54144c724..f05713d66 100644 --- a/crates/egui/src/cache/cache_trait.rs +++ b/crates/egui/src/cache/cache_trait.rs @@ -1,6 +1,6 @@ /// A cache, storing some value for some length of time. #[expect(clippy::len_without_is_empty)] -pub trait CacheTrait: 'static + Send + Sync + std::any::Any { +pub trait CacheTrait: 'static + Send + Sync + core::any::Any { /// Call once per frame to evict cache. fn update(&mut self); diff --git a/crates/egui/src/cache/frame_cache.rs b/crates/egui/src/cache/frame_cache.rs index ae39712c7..76a58c461 100644 --- a/crates/egui/src/cache/frame_cache.rs +++ b/crates/egui/src/cache/frame_cache.rs @@ -48,7 +48,7 @@ impl FrameCache { /// or recompute and store in the cache. pub fn get(&mut self, key: Key) -> &Value where - Key: Copy + std::hash::Hash, + Key: Copy + core::hash::Hash, Computer: ComputerMut, { let hash = crate::util::hash(key); diff --git a/crates/egui/src/cache/frame_publisher.rs b/crates/egui/src/cache/frame_publisher.rs index 81ba34df6..9ca576f29 100644 --- a/crates/egui/src/cache/frame_publisher.rs +++ b/crates/egui/src/cache/frame_publisher.rs @@ -1,4 +1,4 @@ -use std::hash::Hash; +use core::hash::Hash; use super::CacheTrait; diff --git a/crates/egui/src/callstack.rs b/crates/egui/src/callstack.rs index 6b0c35380..9ec426604 100644 --- a/crates/egui/src/callstack.rs +++ b/crates/egui/src/callstack.rs @@ -1,4 +1,4 @@ -use std::fmt::Write as _; +use core::fmt::Write as _; #[derive(Clone)] struct Frame { @@ -239,7 +239,7 @@ fn test_shorten_path() { ), ("/weird/path/file.rs", "/weird/path/file.rs"), ] { - use std::str::FromStr as _; + use core::str::FromStr as _; let before = std::path::PathBuf::from_str(before).unwrap(); assert_eq!(shorten_source_file_path(&before), after); } diff --git a/crates/egui/src/containers/close_tag.rs b/crates/egui/src/containers/close_tag.rs index 3e93dbbd2..273d9e251 100644 --- a/crates/egui/src/containers/close_tag.rs +++ b/crates/egui/src/containers/close_tag.rs @@ -1,6 +1,6 @@ #[expect(unused_imports)] use crate::{Ui, UiBuilder}; -use std::sync::atomic::AtomicBool; +use core::sync::atomic::AtomicBool; /// A tag to mark a container as closable. /// @@ -18,11 +18,12 @@ impl ClosableTag { /// Set close to `true` pub fn set_close(&self) { - self.close.store(true, std::sync::atomic::Ordering::Relaxed); + self.close + .store(true, core::sync::atomic::Ordering::Relaxed); } /// Returns `true` if [`ClosableTag::set_close`] has been called. pub fn should_close(&self) -> bool { - self.close.load(std::sync::atomic::Ordering::Relaxed) + self.close.load(core::sync::atomic::Ordering::Relaxed) } } diff --git a/crates/egui/src/containers/collapsing_header.rs b/crates/egui/src/containers/collapsing_header.rs index 3e49a3bb0..498d64a51 100644 --- a/crates/egui/src/containers/collapsing_header.rs +++ b/crates/egui/src/containers/collapsing_header.rs @@ -342,7 +342,7 @@ pub fn paint_default_icon(ui: &mut Ui, openness: f32, response: &Response) { let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75); let rect = rect.expand(visuals.expansion); let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()]; - use std::f32::consts::TAU; + use core::f32::consts::TAU; let rotation = emath::Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); for p in &mut points { *p = rect.center() + rotation * (*p - rect.center()); diff --git a/crates/egui/src/containers/frame.rs b/crates/egui/src/containers/frame.rs index d6f751bc2..ebef299df 100644 --- a/crates/egui/src/containers/frame.rs +++ b/crates/egui/src/containers/frame.rs @@ -143,12 +143,12 @@ pub struct Frame { #[test] fn frame_size() { assert_eq!( - std::mem::size_of::(), + core::mem::size_of::(), 32, "Frame changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( - std::mem::size_of::() <= 64, + core::mem::size_of::() <= 64, "Frame is getting way too big!" ); } diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 587c77e23..8cd545dd0 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -1,4 +1,4 @@ -use std::iter::once; +use core::iter::once; use emath::{Align, Pos2, Rect, RectAlign, Vec2, vec2}; @@ -483,12 +483,12 @@ impl<'a> Popup<'a> { RectAlign::find_best_align( #[expect(clippy::iter_on_empty_collections)] #[expect(clippy::or_fun_call)] - std::iter::chain( + core::iter::chain( once(self.rect_align), self.alternative_aligns // Need the empty slice so the iters have the same type so we can unwrap_or - .map(|a| std::iter::chain(a.iter().copied(), [].iter().copied())) - .unwrap_or(std::iter::chain( + .map(|a| core::iter::chain(a.iter().copied(), [].iter().copied())) + .unwrap_or(core::iter::chain( self.rect_align.symmetries().iter().copied(), RectAlign::MENU_ALIGNS.iter().copied(), )), diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 7b1bf87e1..54d6550ca 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -2,7 +2,7 @@ #![expect(clippy::needless_range_loop)] -use std::ops::{Add, AddAssign, BitOr, BitOrAssign}; +use core::ops::{Add, AddAssign, BitOr, BitOrAssign}; use emath::GuiRounding as _; use epaint::{Color32, Direction, Margin, Shape}; @@ -930,7 +930,7 @@ impl ScrollArea { let saved_scroll_target = content_ui .ctx() - .pass_state_mut(|state| std::mem::take(&mut state.scroll_target)); + .pass_state_mut(|state| core::mem::take(&mut state.scroll_target)); Prepared { id, @@ -985,7 +985,7 @@ impl ScrollArea { ui: &mut Ui, row_height_sans_spacing: f32, total_rows: usize, - add_contents: impl FnOnce(&mut Ui, std::ops::Range) -> R, + add_contents: impl FnOnce(&mut Ui, core::ops::Range) -> R, ) -> ScrollAreaOutput { let spacing = ui.spacing().item_spacing; let row_height_with_spacing = row_height_sans_spacing + spacing.y; @@ -1093,7 +1093,7 @@ impl Prepared { if direction_enabled[d] { let (scroll_delta, scroll_animation) = content_ui.ctx().pass_state_mut(|state| { ( - std::mem::take(&mut state.scroll_delta.0[d]), + core::mem::take(&mut state.scroll_delta.0[d]), state.scroll_delta.1, ) }); diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index b39a6ec3c..69d3f0bf0 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -921,7 +921,7 @@ impl SideResponse { } } -impl std::ops::BitAnd for SideResponse { +impl core::ops::BitAnd for SideResponse { type Output = Self; fn bitand(self, rhs: Self) -> Self::Output { @@ -932,7 +932,7 @@ impl std::ops::BitAnd for SideResponse { } } -impl std::ops::BitOrAssign for SideResponse { +impl core::ops::BitOrAssign for SideResponse { fn bitor_assign(&mut self, rhs: Self) { *self = Self { hover: self.hover || rhs.hover, diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 50f324588..45193e15e 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1,6 +1,7 @@ #![warn(missing_docs)] // Let's keep `Context` well-documented. -use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration}; +use core::{cell::RefCell, panic::Location, time::Duration}; +use std::{borrow::Cow, sync::Arc}; use emath::GuiRounding as _; use epaint::{ @@ -98,7 +99,7 @@ impl ContextImpl { fn begin_pass_repaint_logic(&mut self, viewport_id: ViewportId) { let viewport = self.viewports.entry(viewport_id).or_default(); - std::mem::swap( + core::mem::swap( &mut viewport.repaint.prev_causes, &mut viewport.repaint.causes, ); @@ -264,14 +265,14 @@ pub struct RepaintCause { pub reason: Cow<'static, str>, } -impl std::fmt::Debug for RepaintCause { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for RepaintCause { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}:{} {}", self.file, self.line, self.reason) } } -impl std::fmt::Display for RepaintCause { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for RepaintCause { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}:{} {}", self.file, self.line, self.reason) } } @@ -459,7 +460,7 @@ impl ContextImpl { self.memory.begin_pass(&new_raw_input, &all_viewport_ids); - viewport.input = std::mem::take(&mut viewport.input).begin_pass( + viewport.input = core::mem::take(&mut viewport.input).begin_pass( new_raw_input, viewport.repaint.requested_immediate_repaint_prev_pass(), pixels_per_point, @@ -653,7 +654,7 @@ impl ContextImpl { } fn all_viewport_ids(&self) -> ViewportIdSet { - std::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect() + core::iter::chain(self.viewports.keys().copied(), [ViewportId::ROOT]).collect() } /// The current active viewport @@ -721,13 +722,13 @@ impl ContextImpl { #[derive(Clone)] pub struct Context(Arc>); -impl std::fmt::Debug for Context { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Context { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("Context").finish_non_exhaustive() } } -impl std::cmp::PartialEq for Context { +impl core::cmp::PartialEq for Context { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.0, &other.0) } @@ -737,7 +738,7 @@ impl Default for Context { fn default() -> Self { let ctx_impl = ContextImpl { embed_viewports: true, - viewports: std::iter::once((ViewportId::ROOT, ViewportState::default())).collect(), + viewports: core::iter::once((ViewportId::ROOT, ViewportState::default())).collect(), ..Default::default() }; let ctx = Self(Arc::new(RwLock::new(ctx_impl))); @@ -847,7 +848,7 @@ impl Context { self.write(|ctx| { let viewport = ctx.viewport_for(viewport_id); viewport.output.num_completed_passes = - std::mem::take(&mut output.platform_output.num_completed_passes); + core::mem::take(&mut output.platform_output.num_completed_passes); output.platform_output.request_discard_reasons.clear(); }); @@ -929,12 +930,12 @@ impl Context { logic(self); self.write(|ctx| LogicOutput { - platform_output: std::mem::take(&mut ctx.viewport_for(viewport_id).output), + platform_output: core::mem::take(&mut ctx.viewport_for(viewport_id).output), viewport_commands: ctx .viewports .iter_mut() .filter(|(_, viewport)| !viewport.commands.is_empty()) - .map(|(&id, viewport)| (id, std::mem::take(&mut viewport.commands))) + .map(|(&id, viewport)| (id, core::mem::take(&mut viewport.commands))) .collect(), }) } @@ -1875,7 +1876,7 @@ impl Context { /// See [`Self::request_repaint_after`] for details. #[track_caller] pub fn request_repaint_after_secs(&self, seconds: f32) { - if let Ok(duration) = std::time::Duration::try_from_secs_f32(seconds) { + if let Ok(duration) = core::time::Duration::try_from_secs_f32(seconds) { self.request_repaint_after(duration); } } @@ -2058,7 +2059,7 @@ impl Context { &self, f: impl FnOnce(&mut T) -> R, ) -> Option { - let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::())); + let plugin = self.read(|ctx| ctx.plugins.get(core::any::TypeId::of::())); plugin.map(|plugin| f(plugin.lock().typed_plugin_mut())) } @@ -2070,13 +2071,13 @@ impl Context { if let Some(plugin) = self.plugin_opt() { plugin } else { - panic!("Plugin of type {:?} not found", std::any::type_name::()); + panic!("Plugin of type {:?} not found", core::any::type_name::()); } } /// Get a handle to the plugin of type `T`, if it was registered. pub fn plugin_opt(&self) -> Option> { - let plugin = self.read(|ctx| ctx.plugins.get(std::any::TypeId::of::())); + let plugin = self.read(|ctx| ctx.plugins.get(core::any::TypeId::of::())); plugin.map(TypedPluginHandle::new) } @@ -2499,7 +2500,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 _; + use core::fmt::Write as _; let paint_widget = |widget: &WidgetRect, text: &str, color: Color32| { let rect = widget.interact_rect; @@ -2680,7 +2681,7 @@ impl ContextImpl { // Inform the backend of all textures that have been updated (including font atlas). let textures_delta = self.tex_manager.0.write().take_delta(); - let mut platform_output: PlatformOutput = std::mem::take(&mut viewport.output); + let mut platform_output: PlatformOutput = core::mem::take(&mut viewport.output); if self.memory.should_interrupt_ime() && let Some(ime) = &mut platform_output.ime @@ -2740,7 +2741,7 @@ impl ContextImpl { shapes }; - std::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass); + core::mem::swap(&mut viewport.prev_pass, &mut viewport.this_pass); if repaint_needed { self.request_repaint(ended_viewport_id, RepaintCause::new()); @@ -2802,7 +2803,7 @@ impl ContextImpl { // Let the primary immediate viewport handle the commands of its children too. // This can make things easier for the backend, as otherwise we may get commands // that affect a viewport while its egui logic is running. - std::mem::take(&mut viewport.commands) + core::mem::take(&mut viewport.commands) } else { vec![] }; @@ -4287,13 +4288,13 @@ fn warn_if_rect_changes_id( struct OrderedRect(Rect); impl PartialOrd for OrderedRect { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for OrderedRect { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { let lhs = self.0; let rhs = other.0; lhs.min diff --git a/crates/egui/src/data/input/dropped_file.rs b/crates/egui/src/data/input/dropped_file.rs index 0ca617fa0..e01955b9f 100644 --- a/crates/egui/src/data/input/dropped_file.rs +++ b/crates/egui/src/data/input/dropped_file.rs @@ -1,13 +1,13 @@ use std::{path::Path, sync::Arc}; #[cfg(target_arch = "wasm32")] -use std::{future::Future, pin::Pin}; +use core::{future::Future, pin::Pin}; /// A file dropped into egui. /// /// The integration owns the concrete file handle, letting egui remain independent of windowing /// backends and file APIs. -pub trait DroppedFile: std::fmt::Debug { +pub trait DroppedFile: core::fmt::Debug { /// The path of the dropped file. /// /// This is an absolute path on native platforms. On the web, it is a relative path containing diff --git a/crates/egui/src/data/input/ime_event.rs b/crates/egui/src/data/input/ime_event.rs index b814b51cd..2075b2fb9 100644 --- a/crates/egui/src/data/input/ime_event.rs +++ b/crates/egui/src/data/input/ime_event.rs @@ -14,7 +14,7 @@ pub enum ImeEvent { /// a non-empty preedit string indicates that the IME is active. Preedit { text: String, - active_range_chars: Option>, + active_range_chars: Option>, }, /// IME composition ended with this final result. diff --git a/crates/egui/src/data/input/modifiers.rs b/crates/egui/src/data/input/modifiers.rs index 2478ea343..35f895950 100644 --- a/crates/egui/src/data/input/modifiers.rs +++ b/crates/egui/src/data/input/modifiers.rs @@ -37,8 +37,8 @@ pub struct Modifiers { pub command: bool, } -impl std::fmt::Debug for Modifiers { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Modifiers { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if self.is_none() { return write!(f, "Modifiers::NONE"); } @@ -387,7 +387,7 @@ impl Modifiers { } } -impl std::ops::BitOr for Modifiers { +impl core::ops::BitOr for Modifiers { type Output = Self; #[inline] @@ -396,7 +396,7 @@ impl std::ops::BitOr for Modifiers { } } -impl std::ops::BitOrAssign for Modifiers { +impl core::ops::BitOrAssign for Modifiers { #[inline] fn bitor_assign(&mut self, rhs: Self) { *self = *self | rhs; diff --git a/crates/egui/src/data/input/raw_input.rs b/crates/egui/src/data/input/raw_input.rs index 7135e90e0..04b20bead 100644 --- a/crates/egui/src/data/input/raw_input.rs +++ b/crates/egui/src/data/input/raw_input.rs @@ -95,7 +95,7 @@ impl Default for RawInput { fn default() -> Self { Self { viewport_id: ViewportId::ROOT, - viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), + viewports: core::iter::once((ViewportId::ROOT, Default::default())).collect(), screen_rect: None, max_texture_side: None, time: None, @@ -134,9 +134,9 @@ impl RawInput { max_texture_side: self.max_texture_side.take(), time: self.time, predicted_dt: self.predicted_dt, - events: std::mem::take(&mut self.events), + events: core::mem::take(&mut self.events), hovered_files: self.hovered_files.clone(), - dropped_files: std::mem::take(&mut self.dropped_files), + dropped_files: core::mem::take(&mut self.dropped_files), focused: self.focused, system_theme: self.system_theme, } diff --git a/crates/egui/src/data/input/safe_area_insets.rs b/crates/egui/src/data/input/safe_area_insets.rs index 914d227c2..a170ec820 100644 --- a/crates/egui/src/data/input/safe_area_insets.rs +++ b/crates/egui/src/data/input/safe_area_insets.rs @@ -10,7 +10,7 @@ use crate::emath::Rect; #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct SafeAreaInsets(pub MarginF32); -impl std::ops::Sub for Rect { +impl core::ops::Sub for Rect { type Output = Self; fn sub(self, rhs: SafeAreaInsets) -> Self::Output { diff --git a/crates/egui/src/data/input/viewport_info.rs b/crates/egui/src/data/input/viewport_info.rs index 774ca1e6e..7c5a31a58 100644 --- a/crates/egui/src/data/input/viewport_info.rs +++ b/crates/egui/src/data/input/viewport_info.rs @@ -117,7 +117,7 @@ impl ViewportInfo { Self { parent: self.parent, title: self.title.clone(), - events: std::mem::take(&mut self.events), + events: core::mem::take(&mut self.events), native_pixels_per_point: self.native_pixels_per_point, monitor_size: self.monitor_size, inner_rect: self.inner_rect, @@ -209,7 +209,7 @@ impl ViewportInfo { } #[expect(clippy::ref_option)] - fn opt_as_str(v: &Option) -> String { + fn opt_as_str(v: &Option) -> String { v.as_ref().map_or(String::new(), |v| format!("{v:?}")) } }); diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index ae1363641..330b7a53c 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -1,6 +1,6 @@ //! All the data egui returns to the backend at the end of each frame. -use std::ops::Range; +use core::ops::Range; use epaint::text::CharIndex; @@ -242,7 +242,7 @@ impl PlatformOutput { /// Take everything ephemeral (everything except `cursor_icon` and /// `cursor_image` currently) pub fn take(&mut self) -> Self { - let taken = std::mem::take(self); + let taken = core::mem::take(self); self.cursor_icon = taken.cursor_icon; // sticky between frames self.cursor_image = taken.cursor_image.clone(); // sticky between frames taken @@ -327,8 +327,8 @@ pub struct CustomCursorImage { pub hotspot: [u16; 2], } -impl std::fmt::Debug for CustomCursorImage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for CustomCursorImage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("CustomCursorImage") .field("size", &self.size) .field("hotspot", &self.hotspot) @@ -544,8 +544,8 @@ impl OutputEvent { } } -impl std::fmt::Debug for OutputEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for OutputEvent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Clicked(wi) => write!(f, "Clicked({wi:?})"), Self::DoubleClicked(wi) => write!(f, "DoubleClicked({wi:?})"), @@ -591,8 +591,8 @@ pub struct WidgetInfo { pub hint_text: Option, } -impl std::fmt::Debug for WidgetInfo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for WidgetInfo { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { typ, enabled, diff --git a/crates/egui/src/data/user_data.rs b/crates/egui/src/data/user_data.rs index 12d90adf7..109e47207 100644 --- a/crates/egui/src/data/user_data.rs +++ b/crates/egui/src/data/user_data.rs @@ -1,4 +1,5 @@ -use std::{any::Any, sync::Arc}; +use core::any::Any; +use std::sync::Arc; /// A wrapper around `dyn Any`, used for passing custom user data /// to [`crate::ViewportCommand::Screenshot`]. @@ -30,8 +31,8 @@ impl PartialEq for UserData { impl Eq for UserData {} -impl std::hash::Hash for UserData { - fn hash(&self, state: &mut H) { +impl core::hash::Hash for UserData { + fn hash(&self, state: &mut H) { self.data.as_ref().map(Arc::as_ptr).hash(state); } } @@ -57,7 +58,7 @@ impl<'de> serde::Deserialize<'de> for UserData { impl serde::de::Visitor<'_> for UserDataVisitor { type Value = UserData; - fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { formatter.write_str("a None value") } diff --git a/crates/egui/src/debug_text.rs b/crates/egui/src/debug_text.rs index 64d06ddac..17c3d6990 100644 --- a/crates/egui/src/debug_text.rs +++ b/crates/egui/src/debug_text.rs @@ -26,7 +26,7 @@ pub fn print(ctx: &Context, text: impl Into) { return; } - let location = std::panic::Location::caller(); + let location = core::panic::Location::caller(); let location = format!("{}:{}", location.file(), location.line()); let plugin = ctx.plugin::(); @@ -58,7 +58,7 @@ impl Plugin for DebugTextPlugin { } fn on_end_pass(&mut self, ui: &mut Ui) { - let entries = std::mem::take(&mut self.entries); + let entries = core::mem::take(&mut self.entries); Self::paint_entries(ui, entries); } } diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index 305effec9..5629f260a 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -1,4 +1,5 @@ -use std::{any::Any, sync::Arc}; +use core::any::Any; +use std::sync::Arc; use crate::{Context, CursorIcon, Plugin, Ui}; diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index e7bb4b2e2..c9d05465e 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -1,6 +1,6 @@ // TODO(emilk): have separate types `PositionId` and `UniqueId`. ? -use std::num::NonZeroU64; +use core::num::NonZeroU64; use crate::{AsIdSalt, IdSalt}; @@ -8,9 +8,9 @@ use crate::{AsIdSalt, IdSalt}; /// /// This is all types implementing `Hash` and `Debug`, /// which includes things like string, integers, tuples of those, etc. -pub trait AsId: std::hash::Hash + std::fmt::Debug {} +pub trait AsId: core::hash::Hash + core::fmt::Debug {} -impl AsId for T {} +impl AsId for T {} /// egui tracks widgets frame-to-frame using [`Id`]s. /// @@ -75,7 +75,7 @@ impl Id { /// Generate a child [`Id`] by salting the parent [`Id`] with the given argument. pub fn with(self, salt: impl AsIdSalt) -> Self { - use std::hash::{BuildHasher as _, Hasher as _}; + use core::hash::{BuildHasher as _, Hasher as _}; let mut hasher = ahash::RandomState::with_seeds(1, 2, 3, 4).build_hasher(); hasher.write_u64(self.value()); hasher.write_u64(IdSalt::new(&salt).value()); @@ -124,8 +124,8 @@ impl Id { } } -impl std::fmt::Debug for Id { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Id { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if *self == Self::NULL { return write!(f, "Id::NULL"); } @@ -204,8 +204,8 @@ mod id_source { #[test] fn id_size() { - assert_eq!(std::mem::size_of::(), 8); - assert_eq!(std::mem::size_of::>(), 8); + assert_eq!(core::mem::size_of::(), 8); + assert_eq!(core::mem::size_of::>(), 8); } #[cfg(test)] diff --git a/crates/egui/src/id_salt.rs b/crates/egui/src/id_salt.rs index 486dda239..2540d1e0e 100644 --- a/crates/egui/src/id_salt.rs +++ b/crates/egui/src/id_salt.rs @@ -1,12 +1,12 @@ -use std::num::NonZeroU64; +use core::num::NonZeroU64; /// Types that can be converted to an [`IdSalt`]. /// /// This is all types implementing `Hash` and `Debug`, /// which includes things like string, integers, tuples of those, etc. -pub trait AsIdSalt: std::hash::Hash + std::fmt::Debug {} +pub trait AsIdSalt: core::hash::Hash + core::fmt::Debug {} -impl AsIdSalt for T {} +impl AsIdSalt for T {} /// Uniquely identifies a child widget within a parent widget. /// @@ -57,8 +57,8 @@ impl IdSalt { } } -impl std::fmt::Debug for IdSalt { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for IdSalt { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { #[cfg(debug_assertions)] if let Some(source) = id_salt_source::get(*self) { return write!(f, "IdSalt::new({source})"); diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 36d6f9bc9..854c1fead 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -13,10 +13,8 @@ use crate::{ }, input_state::wheel_state::WheelState, }; -use std::{ - collections::{BTreeMap, HashSet}, - time::Duration, -}; +use core::time::Duration; +use std::collections::{BTreeMap, HashSet}; pub use crate::Key; pub use touch_state::MultiTouchInfo; diff --git a/crates/egui/src/input_state/touch_state.rs b/crates/egui/src/input_state/touch_state.rs index 578e45e69..833de4238 100644 --- a/crates/egui/src/input_state/touch_state.rs +++ b/crates/egui/src/input_state/touch_state.rs @@ -1,4 +1,5 @@ -use std::{collections::BTreeMap, fmt::Debug}; +use core::fmt::Debug; +use std::collections::BTreeMap; use crate::{ Event, RawInput, TouchId, TouchPhase, @@ -305,7 +306,7 @@ impl TouchState { impl Debug for TouchState { // This outputs less clutter than `#[derive(Debug)]`: - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { for (id, touch) in &self.active_touches { f.write_fmt(format_args!("#{id:?}: {touch:#?}\n"))?; } diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 68ec86a50..e625fc298 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -274,7 +274,7 @@ pub(crate) fn interact( let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0); let top_interactive_order = click_order.max(drag_order); - let mut hovered: IdSet = std::iter::chain(&hits.click, &hits.drag) + let mut hovered: IdSet = core::iter::chain(&hits.click, &hits.drag) .map(|w| w.id) .collect(); diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 6fa9274ee..462bb0142 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -96,8 +96,8 @@ impl LayerId { } } -impl std::fmt::Debug for LayerId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for LayerId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { order, id } = self; write!(f, "LayerId {{ {order:?} {id:?} }}") } diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index c02f37c5c..f3acbf6af 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -55,12 +55,11 @@ mod bytes_loader; mod texture_loader; -use std::{ - borrow::Cow, +use core::{ fmt::{Debug, Display}, ops::Deref, - sync::Arc, }; +use std::{borrow::Cow, sync::Arc}; use ahash::HashMap; @@ -108,13 +107,13 @@ impl LoadError { detected_format.as_ref().map_or(0, |s| s.len()) } Self::Loading(message) => message.len(), - _ => std::mem::size_of::(), + _ => core::mem::size_of::(), } } } impl Display for LoadError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::NoImageLoaders => f.write_str( "No image loaders are installed. If you're trying to load some images \ @@ -136,9 +135,9 @@ impl Display for LoadError { } } -impl std::error::Error for LoadError {} +impl core::error::Error for LoadError {} -pub type Result = std::result::Result; +pub type Result = core::result::Result; /// Given as a hint for image loading requests. /// @@ -209,7 +208,7 @@ pub enum Bytes { } impl Debug for Bytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Static(arg0) => f.debug_tuple("Static").field(&arg0.len()).finish(), Self::Shared(arg0) => f.debug_tuple("Shared").field(&arg0.len()).finish(), @@ -387,7 +386,7 @@ pub type ImageLoadResult = Result; /// An `ImageLoader` decodes raw bytes into a [`ColorImage`]. /// /// Implementations are expected to cache at least each `URI`. -pub trait ImageLoader: std::any::Any { +pub trait ImageLoader: core::any::Any { /// Unique ID of this loader. /// /// To reduce the chance of collisions, include `module_path!()` as part of this ID. diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index d963373b4..4910d81da 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1,6 +1,6 @@ #![warn(missing_docs)] // Let's keep this file well-documented.` to memory.rs -use std::num::NonZeroUsize; +use core::num::NonZeroUsize; use ahash::{HashMap, HashSet}; use epaint::emath::TSTransform; @@ -942,7 +942,7 @@ impl Memory { if let Some(modal_layer) = self.focus().and_then(|f| f.top_modal_layer) { matches!( self.areas().compare_order(layer_id, modal_layer), - std::cmp::Ordering::Equal | std::cmp::Ordering::Greater + core::cmp::Ordering::Equal | core::cmp::Ordering::Greater ) } else { true @@ -982,7 +982,7 @@ impl Memory { if let Some(current) = self.focus().and_then(|f| f.top_modal_layer_current_frame) && matches!( self.areas().compare_order(layer_id, current), - std::cmp::Ordering::Less + core::cmp::Ordering::Less ) { return; @@ -1223,12 +1223,12 @@ impl Areas { /// Compare the order of two layers, based on the order list from last frame. /// /// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list. - pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering { + pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> core::cmp::Ordering { // Sort by layer `order` first and use `order_map` to resolve disputes. // If `order_map` only contains one layer ID, then the other one will be // lower because `None < Some(x)`. match a.order.cmp(&b.order) { - std::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)), + core::cmp::Ordering::Equal => self.order_map.get(&a).cmp(&self.order_map.get(&b)), cmp => cmp, } } @@ -1276,7 +1276,7 @@ impl Areas { } pub fn visible_layer_ids(&self) -> ahash::HashSet { - std::iter::chain( + core::iter::chain( &self.visible_areas_last_frame, &self.visible_areas_current_frame, ) @@ -1365,7 +1365,7 @@ impl Areas { .. } = self; - std::mem::swap(visible_areas_last_frame, visible_areas_current_frame); + core::mem::swap(visible_areas_last_frame, visible_areas_current_frame); visible_areas_current_frame.clear(); order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer))); @@ -1374,7 +1374,7 @@ impl Areas { // For all layers with sublayers, put the sublayers directly after the parent layer: // (it doesn't matter in which order we replace parents with their children) #[expect(clippy::iter_over_hash_type)] - for (parent, children) in std::mem::take(sublayers) { + for (parent, children) in core::mem::take(sublayers) { let mut moved_layers = vec![parent]; // parent first… order.retain(|l| { @@ -1483,14 +1483,14 @@ fn order_map_total_ordering() { let mut i = 0; for &[a, b] in layers.array_windows() { assert!(a.order <= b.order, "does not follow LayerId.order"); - if areas.compare_order(a, b) != std::cmp::Ordering::Equal { + if areas.compare_order(a, b) != core::cmp::Ordering::Equal { i += 1; } equivalence_classes.push(i); } assert_eq!(layers.len(), equivalence_classes.len()); - for (&l1, c1) in std::iter::zip(&layers, &equivalence_classes) { - for (&l2, c2) in std::iter::zip(&layers, &equivalence_classes) { + for (&l1, c1) in core::iter::zip(&layers, &equivalence_classes) { + for (&l2, c2) in core::iter::zip(&layers, &equivalence_classes) { assert_eq!( c1.cmp(c2), areas.compare_order(l1, l2), diff --git a/crates/egui/src/painter.rs b/crates/egui/src/painter.rs index 4b81e98cb..ab64db864 100644 --- a/crates/egui/src/painter.rs +++ b/crates/egui/src/painter.rs @@ -280,7 +280,7 @@ impl Painter { ); } - pub fn error(&self, pos: Pos2, text: impl std::fmt::Display) -> Rect { + pub fn error(&self, pos: Pos2, text: impl core::fmt::Display) -> Rect { let color = self.ctx.global_style().visuals.error_fg_color; self.debug_text(pos, Align2::LEFT_TOP, color, format!("🔥 {text}")) } @@ -416,7 +416,7 @@ impl Painter { /// Show an arrow starting at `origin` and going in the direction of `vec`, with the length `vec.length()`. pub fn arrow(&self, origin: Pos2, vec: Vec2, stroke: impl Into) { use crate::emath::Rot2; - let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); + let rot = Rot2::from_angle(core::f32::consts::TAU / 10.0); let tip_length = vec.length() / 4.0; let tip = origin + vec; let dir = vec.normalized(); diff --git a/crates/egui/src/plugin.rs b/crates/egui/src/plugin.rs index f8f1a4468..76df0a641 100644 --- a/crates/egui/src/plugin.rs +++ b/crates/egui/src/plugin.rs @@ -10,7 +10,7 @@ use std::sync::Arc; /// Plugins should not hold a reference to the [`Context`], since this would create a cycle /// (which would prevent the [`Context`] from being dropped). #[expect(unused_variables)] -pub trait Plugin: Send + Sync + std::any::Any + 'static { +pub trait Plugin: Send + Sync + core::any::Any + 'static { /// Plugin name. /// /// Used when profiling. @@ -60,14 +60,14 @@ pub(crate) struct PluginHandle { /// Use [`Self::lock`] to access the plugin. pub struct TypedPluginHandle { handle: Arc>, - _type: std::marker::PhantomData

, + _type: core::marker::PhantomData

, } impl TypedPluginHandle

{ pub(crate) fn new(handle: Arc>) -> Self { Self { handle, - _type: std::marker::PhantomData, + _type: core::marker::PhantomData, } } @@ -77,7 +77,7 @@ impl TypedPluginHandle

{ pub fn lock(&self) -> TypedPluginGuard<'_, P> { TypedPluginGuard { guard: self.handle.lock(), - _type: std::marker::PhantomData, + _type: core::marker::PhantomData, } } } @@ -85,12 +85,12 @@ impl TypedPluginHandle

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

, + _type: core::marker::PhantomData

, } impl TypedPluginGuard<'_, P> {} -impl std::ops::Deref for TypedPluginGuard<'_, P> { +impl core::ops::Deref for TypedPluginGuard<'_, P> { type Target = P; fn deref(&self) -> &Self::Target { @@ -98,7 +98,7 @@ impl std::ops::Deref for TypedPluginGuard<'_, P> { } } -impl std::ops::DerefMut for TypedPluginGuard<'_, P> { +impl core::ops::DerefMut for TypedPluginGuard<'_, P> { fn deref_mut(&mut self) -> &mut Self::Target { self.guard.typed_plugin_mut() } @@ -111,7 +111,7 @@ impl PluginHandle { })) } - fn plugin_type_id(&self) -> std::any::TypeId { + fn plugin_type_id(&self) -> core::any::TypeId { (*self.plugin).type_id() } @@ -120,13 +120,13 @@ impl PluginHandle { } fn typed_plugin(&self) -> &P { - (self.plugin.as_ref() as &dyn std::any::Any) + (self.plugin.as_ref() as &dyn core::any::Any) .downcast_ref::

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

() .expect("PluginHandle: plugin is not of the expected type") } @@ -135,7 +135,7 @@ impl PluginHandle { /// User-registered plugins. #[derive(Clone, Default)] pub(crate) struct Plugins { - plugins: HashMap>>, + plugins: HashMap>>, plugins_ordered: PluginsOrdered, } @@ -215,7 +215,7 @@ impl Plugins { true } - pub fn get(&self, type_id: std::any::TypeId) -> Option>> { + pub fn get(&self, type_id: core::any::TypeId) -> Option>> { self.plugins.get(&type_id).cloned() } } diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 5ba7b8943..e206594ad 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -1,4 +1,5 @@ -use std::{any::Any, sync::Arc}; +use core::any::Any; +use std::sync::Arc; use crate::{ Context, CursorIcon, Id, LayerId, PointerButton, Popup, PopupKind, Sense, Tooltip, Ui, @@ -77,7 +78,7 @@ pub struct Response { #[test] fn test_response_size() { assert_eq!( - std::mem::size_of::(), + core::mem::size_of::(), 88, "Keep Response small, because we create them often, and we want to keep it lean and fast" ); @@ -1112,7 +1113,7 @@ impl Response { /// ``` /// /// Now `draw_vec2(ui, foo).hovered` is true if either [`DragValue`](crate::DragValue) were hovered. -impl std::ops::BitOr for Response { +impl core::ops::BitOr for Response { type Output = Self; fn bitor(self, rhs: Self) -> Self { @@ -1133,7 +1134,7 @@ impl std::ops::BitOr for Response { /// if response.hovered() { ui.label("You hovered at least one of the widgets"); } /// # }); /// ``` -impl std::ops::BitOrAssign for Response { +impl core::ops::BitOrAssign for Response { fn bitor_assign(&mut self, rhs: Self) { *self = self.union(rhs); } diff --git a/crates/egui/src/sense.rs b/crates/egui/src/sense.rs index c3b3af7f2..1283f320e 100644 --- a/crates/egui/src/sense.rs +++ b/crates/egui/src/sense.rs @@ -22,8 +22,8 @@ bitflags::bitflags! { } } -impl std::fmt::Debug for Sense { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Sense { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Sense {{")?; if self.senses_click() { write!(f, " click")?; diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index 953cd4b9a..f6df21f09 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -1,11 +1,12 @@ //! egui theme (spacing, colors, etc). +use core::ops::RangeInclusive; use emath::Align; use epaint::{ CornerRadius, FontColorTransferFunction, Shadow, Stroke, TextOptions, text::{FontTweak, FontVariationAxis, HintingTarget, SmoothHinting}, }; -use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; +use std::{collections::BTreeMap, sync::Arc}; use crate::{ ComboBox, CursorIcon, FontFamily, FontId, Grid, Margin, Response, RichText, TextWrapMode, @@ -47,8 +48,8 @@ impl NumberFormatter { } } -impl std::fmt::Debug for NumberFormatter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for NumberFormatter { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str("NumberFormatter") } } @@ -93,8 +94,8 @@ pub enum TextStyle { Name(std::sync::Arc), } -impl std::fmt::Display for TextStyle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for TextStyle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Small => "Small".fmt(f), Self::Body => "Body".fmt(f), @@ -192,8 +193,8 @@ impl From for FontSelection { #[derive(Clone, Default)] pub struct StyleModifier(Option>); -impl std::fmt::Debug for StyleModifier { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for StyleModifier { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str("StyleModifier") } } @@ -2695,7 +2696,7 @@ impl DebugOptions { } // TODO(emilk): improve and standardize -fn two_drag_values(value: &mut Vec2, range: std::ops::RangeInclusive) -> impl Widget + '_ { +fn two_drag_values(value: &mut Vec2, range: core::ops::RangeInclusive) -> impl Widget + '_ { move |ui: &mut crate::Ui| { ui.horizontal(|ui| { ui.add( @@ -2764,8 +2765,8 @@ impl NumericColorSpace { } } -impl std::fmt::Display for NumericColorSpace { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for NumericColorSpace { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::GammaByte => write!(f, "U8"), Self::Linear => write!(f, "F"), diff --git a/crates/egui/src/text_selection/cursor_range.rs b/crates/egui/src/text_selection/cursor_range.rs index 4229756db..f811b2e5e 100644 --- a/crates/egui/src/text_selection/cursor_range.rs +++ b/crates/egui/src/text_selection/cursor_range.rs @@ -49,9 +49,9 @@ impl CCursorRange { } /// The range of selected character indices. - pub fn as_sorted_char_range(&self) -> std::ops::Range { + pub fn as_sorted_char_range(&self) -> core::ops::Range { let [start, end] = self.sorted_cursors(); - std::ops::Range { + core::ops::Range { start: start.index, end: end.index, } diff --git a/crates/egui/src/text_selection/label_text_selection.rs b/crates/egui/src/text_selection/label_text_selection.rs index 80cc90c8a..4adcf59ab 100644 --- a/crates/egui/src/text_selection/label_text_selection.rs +++ b/crates/egui/src/text_selection/label_text_selection.rs @@ -47,8 +47,8 @@ fn pos_in_galley(galley: &Galley, ccursor: CCursor) -> Pos2 { galley.pos_from_cursor(ccursor).center() } -impl std::fmt::Debug for WidgetTextCursor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for WidgetTextCursor { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { widget_id, ccursor, @@ -271,7 +271,7 @@ impl ViewportLabelSelectionState { self.is_dragging = false; } - let text_to_copy = std::mem::take(&mut self.text_to_copy); + let text_to_copy = core::mem::take(&mut self.text_to_copy); if !text_to_copy.is_empty() { ui.copy_text(text_to_copy); } diff --git a/crates/egui/src/text_selection/text_cursor_state.rs b/crates/egui/src/text_selection/text_cursor_state.rs index f88368f22..7ab88ec97 100644 --- a/crates/egui/src/text_selection/text_cursor_state.rs +++ b/crates/egui/src/text_selection/text_cursor_state.rs @@ -294,7 +294,7 @@ pub fn char_index_from_byte_index(input: &str, byte_index: ByteIndex) -> CharInd CharIndex(input.chars().count()) } -pub fn slice_char_range(s: &str, char_range: std::ops::Range) -> &str { +pub fn slice_char_range(s: &str, char_range: core::ops::Range) -> &str { assert!( char_range.start <= char_range.end, "Invalid range, start must be less than end, but start = {}, end = {}", diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index 5b41fd902..79e38dccb 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -139,8 +139,8 @@ pub(crate) fn paint_ime_preedit_text_visuals( painter: &Painter, galley: &Arc, row_height: f32, - preedit_range: std::ops::Range, - mut relative_active_range: Option>, + preedit_range: core::ops::Range, + mut relative_active_range: Option>, time_since_last_interaction: f64, ) { /// Instead of implementing [`PartialOrd`] and [`Ord`] for [`CCursor`] to @@ -150,7 +150,7 @@ pub(crate) fn paint_ime_preedit_text_visuals( /// These traits are intentionally not implemented because /// [`CCursor::prefer_next_row`] makes it difficult to define a clear /// ordering between two [`CCursor`]s. - fn is_cursor_range_empty(range: &std::ops::Range) -> bool { + fn is_cursor_range_empty(range: &core::ops::Range) -> bool { range.start.index == range.end.index } diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index d6a390377..9021d2d03 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -1,7 +1,8 @@ #![warn(missing_docs)] // Let's keep `Ui` well-documented. #![expect(clippy::use_self)] -use std::{any::Any, ops::Deref, sync::Arc}; +use core::{any::Any, ops::Deref}; +use std::sync::Arc; use crate::containers::menu; use crate::widget_style::{HasClasses as _, ROOT_CLASS}; @@ -1984,7 +1985,7 @@ impl Ui { /// but is shown to the user in fractions of one Tau (i.e. fractions of one turn). /// The angle is NOT wrapped, so the user may select, for instance 2𝞃 (720°) pub fn drag_angle_tau(&mut self, radians: &mut f32) -> Response { - use std::f32::consts::TAU; + use core::f32::consts::TAU; let mut taus = *radians / TAU; let mut response = self.add(DragValue::new(&mut taus).speed(0.01).suffix("τ")); @@ -2599,7 +2600,7 @@ impl Ui { let column_width = (self.available_width() - total_spacing) / (NUM_COL as f32); let top_left = self.cursor().min; - let mut columns = std::array::from_fn(|col_idx| { + let mut columns = core::array::from_fn(|col_idx| { let pos = top_left + vec2((col_idx as f32) * (column_width + spacing), 0.0); let child_rect = Rect::from_min_max( pos, diff --git a/crates/egui/src/ui_stack.rs b/crates/egui/src/ui_stack.rs index f43834dba..f6b4865be 100644 --- a/crates/egui/src/ui_stack.rs +++ b/crates/egui/src/ui_stack.rs @@ -1,5 +1,5 @@ +use core::{any::Any, iter::FusedIterator}; use std::sync::Arc; -use std::{any::Any, iter::FusedIterator}; use crate::widget_style::Classes; use epaint::Color32; diff --git a/crates/egui/src/util/fixed_cache.rs b/crates/egui/src/util/fixed_cache.rs index c0f8662a2..b2d2e45dd 100644 --- a/crates/egui/src/util/fixed_cache.rs +++ b/crates/egui/src/util/fixed_cache.rs @@ -16,15 +16,15 @@ where } } -impl std::fmt::Debug for FixedCache { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for FixedCache { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Cache") } } impl FixedCache where - K: std::hash::Hash + PartialEq, + K: core::hash::Hash + PartialEq, { pub fn get(&self, key: &K) -> Option<&V> { let bucket = (hash(key) % (FIXED_CACHE_SIZE as u64)) as usize; diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index 76f0da5d7..d384a883b 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -3,7 +3,8 @@ // For non-serializable types, these simply return `None`. // This will also allow users to pick their own serialization format per type. -use std::{any::Any, sync::Arc}; +use core::any::Any; +use std::sync::Arc; // ----------------------------------------------------------------------------------------------- /// Like [`std::any::TypeId`], but can be serialized and deserialized. @@ -14,7 +15,7 @@ pub struct TypeId(u64); impl TypeId { #[inline] pub fn of() -> Self { - std::any::TypeId::of::().into() + core::any::TypeId::of::().into() } #[inline(always)] @@ -23,9 +24,9 @@ impl TypeId { } } -impl From for TypeId { +impl From for TypeId { #[inline] - fn from(id: std::any::TypeId) -> Self { + fn from(id: core::any::TypeId) -> Self { Self(epaint::util::hash(id)) } } @@ -113,8 +114,8 @@ impl Clone for Element { } } -impl std::fmt::Debug for Element { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Element { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match &self { Self::Value { value, .. } => f .debug_struct("Element::Value") @@ -314,7 +315,7 @@ fn from_ron_str(ron: &str) -> Option { Err(_err) => { log::warn!( "egui: Failed to deserialize {} from memory: {}, ron error: {:?}", - std::any::type_name::(), + core::any::type_name::(), _err, ron ); @@ -578,7 +579,7 @@ impl IdTypeMap { pub fn remove_temp(&mut self, id: Id) -> Option { let key = RawKey::new::(id); let mut element = self.map.remove(&key)?; - Some(std::mem::take(element.get_mut_temp()?)) + Some(core::mem::take(element.get_mut_temp()?)) } /// Remove a temporary value given a raw key. diff --git a/crates/egui/src/util/undoer.rs b/crates/egui/src/util/undoer.rs index a2eec6599..cbac10d76 100644 --- a/crates/egui/src/util/undoer.rs +++ b/crates/egui/src/util/undoer.rs @@ -67,8 +67,8 @@ pub struct Undoer { flux: Option>, } -impl std::fmt::Debug for Undoer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Undoer { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let Self { undos, redos, .. } = self; f.debug_struct("Undoer") .field("undo count", &undos.len()) diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index 962b065a3..e04f9d505 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -120,13 +120,13 @@ pub struct ViewportId(pub Id); // We implement `PartialOrd` and `Ord` so we can use `ViewportId` in a `BTreeMap`, // which allows predicatable iteration order, frame-to-frame. impl PartialOrd for ViewportId { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for ViewportId { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { self.0.value().cmp(&other.0.value()) } } @@ -138,8 +138,8 @@ impl Default for ViewportId { } } -impl std::fmt::Debug for ViewportId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for ViewportId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.0.short_debug_format().fmt(f) } } @@ -198,8 +198,8 @@ impl IconData { } } -impl std::fmt::Debug for IconData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for IconData { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("IconData") .field("width", &self.width) .field("height", &self.height) @@ -1275,7 +1275,7 @@ pub struct ViewportOutput { /// but if you haven't, you can use this instead. /// /// If the duration is zero, schedule a repaint immediately. - pub repaint_delay: std::time::Duration, + pub repaint_delay: core::time::Duration, } impl ViewportOutput { diff --git a/crates/egui/src/widget_style.rs b/crates/egui/src/widget_style.rs index f3c5e5bd0..f6239cae9 100644 --- a/crates/egui/src/widget_style.rs +++ b/crates/egui/src/widget_style.rs @@ -256,7 +256,7 @@ impl HasClasses for Classes { } } -impl std::fmt::Display for Classes { +impl core::fmt::Display for Classes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.classes.iter().for_each(|class| { let _ = f.write_str(class.as_str()); diff --git a/crates/egui/src/widget_text.rs b/crates/egui/src/widget_text.rs index c8e803cbc..9281253ad 100644 --- a/crates/egui/src/widget_text.rs +++ b/crates/egui/src/widget_text.rs @@ -1,5 +1,5 @@ +use core::fmt::Formatter; use epaint::text::{IntoTag, TextFormat, VariationCoords}; -use std::fmt::Formatter; use std::{borrow::Cow, sync::Arc}; use crate::{ @@ -539,8 +539,8 @@ pub enum WidgetText { Galley(Arc), } -impl std::fmt::Debug for WidgetText { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for WidgetText { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { let text = self.text(); match self { Self::Text(_) => write!(f, "Text({text:?})"), diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 7f1140bd9..81f686fe1 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -3,8 +3,8 @@ use crate::{ Modifiers, NumExt as _, Response, RichText, Sense, TextEdit, TextWrapMode, Ui, Widget, WidgetInfo, emath, text, }; +use core::{cmp::Ordering, ops::RangeInclusive}; use emath::Vec2; -use std::{cmp::Ordering, ops::RangeInclusive}; // ---------------------------------------------------------------------------- @@ -780,7 +780,7 @@ mod tests { macro_rules! total_assert_eq { ($a:expr, $b:expr) => { assert!( - matches!($a.total_cmp(&$b), std::cmp::Ordering::Equal), + matches!($a.total_cmp(&$b), core::cmp::Ordering::Equal), "{} != {}", $a, $b diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 30a5997ec..0618e8661 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -1,4 +1,5 @@ -use std::{borrow::Cow, slice::Iter, sync::Arc, time::Duration}; +use core::{slice::Iter, time::Duration}; +use std::{borrow::Cow, sync::Arc}; use emath::{Align, Float as _, GuiRounding as _, NumExt as _, Rot2}; use epaint::{ @@ -607,8 +608,8 @@ pub enum ImageSource<'a> { }, } -impl std::fmt::Debug for ImageSource<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for ImageSource<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { ImageSource::Bytes { uri, .. } | ImageSource::Uri(uri) => uri.as_ref().fmt(f), ImageSource::Texture(st) => st.id.fmt(f), diff --git a/crates/egui/src/widgets/progress_bar.rs b/crates/egui/src/widgets/progress_bar.rs index 444f6088c..ed697cc4d 100644 --- a/crates/egui/src/widgets/progress_bar.rs +++ b/crates/egui/src/widgets/progress_bar.rs @@ -163,7 +163,7 @@ impl Widget for ProgressBar { if animate && !has_custom_cr { let n_points = 20; let time = ui.input(|i| i.time); - let start_angle = time * std::f64::consts::TAU; + let start_angle = time * core::f64::consts::TAU; let end_angle = start_angle + 240f64.to_radians() * time.sin(); let circle_radius = half_height - 2.0; let points: Vec = (0..n_points) diff --git a/crates/egui/src/widgets/slider.rs b/crates/egui/src/widgets/slider.rs index b5ce4fc55..796489421 100644 --- a/crates/egui/src/widgets/slider.rs +++ b/crates/egui/src/widgets/slider.rs @@ -1,6 +1,6 @@ #![expect(clippy::needless_pass_by_value)] // False positives with `impl ToString` -use std::ops::RangeInclusive; +use core::ops::RangeInclusive; use crate::{ Color32, DragValue, EventFilter, Key, Label, MINUS_CHAR_STR, NumExt as _, Pos2, Rangef, Rect, diff --git a/crates/egui/src/widgets/spinner.rs b/crates/egui/src/widgets/spinner.rs index 25820a06e..d378fd3b1 100644 --- a/crates/egui/src/widgets/spinner.rs +++ b/crates/egui/src/widgets/spinner.rs @@ -45,7 +45,7 @@ impl Spinner { let radius = (rect.height().min(rect.width()) / 2.0) - 2.0; let n_points = (radius.round() as u32).clamp(8, 128); let time = ui.input(|i| i.time); - let start_angle = time * std::f64::consts::TAU; + let start_angle = time * core::f64::consts::TAU; let end_angle = start_angle + 240f64.to_radians() * time.sin(); let points: Vec = (0..n_points) .map(|i| { diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 96bd3ca01..41220ed81 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -1008,7 +1008,7 @@ impl TextEdit<'_> { fn mask_if_password(is_password: bool, text: &str) -> String { fn mask_password(text: &str) -> String { - std::iter::repeat_n( + core::iter::repeat_n( epaint::text::PASSWORD_REPLACEMENT_CHAR, text.chars().count(), ) @@ -1084,7 +1084,7 @@ fn events( Selection(CCursorRange), ImeComposition { cursor_range: CCursorRange, - active_range: Option>, + active_range: Option>, }, ImeCompositionCursorRange(CCursorRange), } diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index 61ffb21ea..d17532753 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -95,7 +95,7 @@ pub(crate) enum TextEditCursorPurpose { /// irrelevant. /// /// When `None`, no active range is displayed. - active_range: Option>, + active_range: Option>, }, } diff --git a/crates/egui/src/widgets/text_edit/text_buffer.rs b/crates/egui/src/widgets/text_edit/text_buffer.rs index 1fada9626..f09843e86 100644 --- a/crates/egui/src/widgets/text_edit/text_buffer.rs +++ b/crates/egui/src/widgets/text_edit/text_buffer.rs @@ -1,4 +1,5 @@ -use std::{borrow::Cow, ops::Range}; +use core::ops::Range; +use std::borrow::Cow; use epaint::{ Galley, @@ -237,7 +238,7 @@ pub trait TextBuffer { /// } /// } /// ``` - fn type_id(&self) -> std::any::TypeId; + fn type_id(&self) -> core::any::TypeId; } impl TextBuffer for String { @@ -282,11 +283,11 @@ impl TextBuffer for String { } fn take(&mut self) -> String { - std::mem::take(self) + core::mem::take(self) } - fn type_id(&self) -> std::any::TypeId { - std::any::TypeId::of::() + fn type_id(&self) -> core::any::TypeId { + core::any::TypeId::of::() } } @@ -316,11 +317,11 @@ impl TextBuffer for Cow<'_, str> { } fn take(&mut self) -> String { - std::mem::take(self).into_owned() + core::mem::take(self).into_owned() } - fn type_id(&self) -> std::any::TypeId { - std::any::TypeId::of::>() + fn type_id(&self) -> core::any::TypeId { + core::any::TypeId::of::>() } } @@ -340,8 +341,8 @@ impl TextBuffer for &str { fn delete_char_range(&mut self, _ch_range: Range) {} - fn type_id(&self) -> std::any::TypeId { - std::any::TypeId::of::<&str>() + fn type_id(&self) -> core::any::TypeId { + core::any::TypeId::of::<&str>() } } diff --git a/crates/egui_demo_app/src/accessibility_inspector.rs b/crates/egui_demo_app/src/accessibility_inspector.rs index 91da6c653..c28bde2fa 100644 --- a/crates/egui_demo_app/src/accessibility_inspector.rs +++ b/crates/egui_demo_app/src/accessibility_inspector.rs @@ -1,4 +1,4 @@ -use std::mem; +use core::mem; use accesskit::{Action, ActionRequest}; use accesskit_consumer::{FilterResult, Node, NodeId, Tree, TreeChangeHandler}; @@ -168,7 +168,7 @@ impl AccessibilityInspectorPlugin { ui.horizontal_wrapped(|ui| { // Iterate through all possible actions via the `Action::n` helper. let mut current_action = 0; - let all_actions = std::iter::from_fn(|| { + let all_actions = core::iter::from_fn(|| { let action = Action::n(current_action); current_action += 1; action diff --git a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs index b6ba60df9..97ba1d3d2 100644 --- a/crates/egui_demo_app/src/apps/custom3d_wgpu.rs +++ b/crates/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -1,6 +1,6 @@ #![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps -use std::num::NonZeroU64; +use core::num::NonZeroU64; use eframe::{ egui_wgpu::wgpu::util::DeviceExt as _, diff --git a/crates/egui_demo_app/src/apps/fractal_clock.rs b/crates/egui_demo_app/src/apps/fractal_clock.rs index 43ed3fb8b..783e8d2c1 100644 --- a/crates/egui_demo_app/src/apps/fractal_clock.rs +++ b/crates/egui_demo_app/src/apps/fractal_clock.rs @@ -1,10 +1,10 @@ +use core::f32::consts::TAU; use egui::{ Color32, Painter, Pos2, Rect, Shape, Stroke, Ui, Vec2, containers::{CollapsingHeader, Frame}, emath, pos2, widgets::Slider, }; -use std::f32::consts::TAU; #[derive(PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -202,7 +202,7 @@ impl FractalClock { } } - std::mem::swap(&mut nodes, &mut new_nodes); + core::mem::swap(&mut nodes, &mut new_nodes); } self.line_count = shapes.len(); painter.extend(shapes); diff --git a/crates/egui_demo_app/src/backend_panel.rs b/crates/egui_demo_app/src/backend_panel.rs index 4d6039a85..0fb7eee0f 100644 --- a/crates/egui_demo_app/src/backend_panel.rs +++ b/crates/egui_demo_app/src/backend_panel.rs @@ -160,9 +160,9 @@ impl BackendPanel { { log::info!("Waiting 2s before requesting repaint…"); let ctx = ui.ctx().clone(); - call_after_delay(std::time::Duration::from_secs(2), move || { + call_after_delay(core::time::Duration::from_secs(2), move || { log::info!("Request a repaint in 3s…"); - ctx.request_repaint_after(std::time::Duration::from_secs(3)); + ctx.request_repaint_after(core::time::Duration::from_secs(3)); }); } @@ -525,7 +525,7 @@ impl EguiWindows { // ---------------------------------------------------------------------------- #[cfg(not(target_arch = "wasm32"))] -fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) { +fn call_after_delay(delay: core::time::Duration, f: impl FnOnce() + Send + 'static) { std::thread::Builder::new() .name("call_after_delay".to_owned()) .spawn(move || { @@ -536,7 +536,7 @@ fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'stati } #[cfg(target_arch = "wasm32")] -fn call_after_delay(delay: std::time::Duration, f: impl FnOnce() + Send + 'static) { +fn call_after_delay(delay: core::time::Duration, f: impl FnOnce() + Send + 'static) { #![expect(clippy::unwrap_used)] use wasm_bindgen::prelude::*; diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 38da5751d..b487ab99d 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -38,7 +38,7 @@ fn main() { }); for loud_crate in ["naga", "wgpu_core", "wgpu_hal"] { if !rust_log.contains(&format!("{loud_crate}=")) { - use std::fmt::Write as _; + use core::fmt::Write as _; write!(rust_log, ",{loud_crate}=warn").ok(); } } @@ -103,7 +103,7 @@ fn start_puffin_server() { // We can store the server if we want, but in this case we just want // it to keep running. Dropping it closes the server, so let's not drop it! #[expect(clippy::mem_forget)] - std::mem::forget(puffin_server); + core::mem::forget(puffin_server); } Err(err) => { log::error!("Failed to start puffin server: {err}"); diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index 629ad6a97..8801bba23 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -127,8 +127,8 @@ impl Anchor { } } -impl std::fmt::Display for Anchor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Anchor { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let mut name = format!("{self:?}"); name.make_ascii_lowercase(); f.write_str(&name) @@ -473,8 +473,8 @@ impl WrapApp { } fn ui_file_drag_and_drop(&mut self, ctx: &egui::Context) { + use core::fmt::Write as _; use egui::{Align2, Color32, Id, LayerId, Order, TextStyle}; - use std::fmt::Write as _; // Preview hovering files: if !ctx.input(|i| i.raw.hovered_files.is_empty()) { diff --git a/crates/egui_demo_lib/benches/benchmark.rs b/crates/egui_demo_lib/benches/benchmark.rs index 36accead3..6b0eb5d4c 100644 --- a/crates/egui_demo_lib/benches/benchmark.rs +++ b/crates/egui_demo_lib/benches/benchmark.rs @@ -1,4 +1,4 @@ -use std::fmt::Write as _; +use core::fmt::Write as _; use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; diff --git a/crates/egui_demo_lib/src/demo/dancing_strings.rs b/crates/egui_demo_lib/src/demo/dancing_strings.rs index 137ecc2b9..7846e0ee4 100644 --- a/crates/egui_demo_lib/src/demo/dancing_strings.rs +++ b/crates/egui_demo_lib/src/demo/dancing_strings.rs @@ -61,7 +61,7 @@ impl crate::View for DancingStrings { .map(|i| { let t = i as f64 / (n as f64); let amp = (time * speed * mode).sin() / mode; - let y = amp * (t * std::f64::consts::TAU / 2.0 * mode).sin(); + let y = amp * (t * core::f64::consts::TAU / 2.0 * mode).sin(); to_screen * pos2(t as f32, y as f32) }) .collect(); 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 a874de5b6..fc15aac16 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -13,7 +13,7 @@ struct DemoGroup { demos: Vec>, } -impl std::ops::Add for DemoGroup { +impl core::ops::Add for DemoGroup { type Output = Self; fn add(self, other: Self) -> Self { diff --git a/crates/egui_demo_lib/src/demo/misc_demo_window.rs b/crates/egui_demo_lib/src/demo/misc_demo_window.rs index 6b9584dd2..8b91fbc64 100644 --- a/crates/egui_demo_lib/src/demo/misc_demo_window.rs +++ b/crates/egui_demo_lib/src/demo/misc_demo_window.rs @@ -41,7 +41,7 @@ impl Default for MiscDemoWindow { dummy_bool: false, dummy_usize: 0, - checklist: std::array::from_fn(|i| i == 0), + checklist: core::array::from_fn(|i| i == 0), } } } @@ -184,7 +184,7 @@ impl View for MiscDemoWindow { .show(ui, |ui| { ui.horizontal(|ui| { ui.label("You can pretty easily paint your own small icons:"); - use std::f32::consts::TAU; + use core::f32::consts::TAU; let size = Vec2::splat(16.0); let (response, painter) = ui.allocate_painter(size, Sense::hover()); let rect = response.rect; @@ -264,7 +264,7 @@ pub struct Widgets { impl Default for Widgets { fn default() -> Self { Self { - angle: std::f32::consts::TAU / 3.0, + angle: core::f32::consts::TAU / 3.0, password: "hunter2".to_owned(), } } @@ -282,7 +282,7 @@ impl Widgets { ui.horizontal(|ui| { ui.label("An angle:"); ui.drag_angle(angle); - ui.label(format!("≈ {:.3}τ", *angle / std::f32::consts::TAU)) + ui.label(format!("≈ {:.3}τ", *angle / core::f32::consts::TAU)) .on_hover_text("Each τ represents one turn (τ = 2π)"); }) .response @@ -421,7 +421,7 @@ impl Repaint { ctx.request_repaint(); } if self.repaint_after_delay { - ctx.request_repaint_after(std::time::Duration::from_secs_f64(self.delay)); + ctx.request_repaint_after(core::time::Duration::from_secs_f64(self.delay)); } } } @@ -623,7 +623,7 @@ impl Tree { return Action::Delete; } - self.0 = std::mem::take(self) + self.0 = core::mem::take(self) .0 .into_iter() .enumerate() @@ -897,7 +897,7 @@ impl Default for TextRotation { impl TextRotation { pub fn ui(&mut self, ui: &mut Ui) { - ui.add(Slider::new(&mut self.angle, 0.0..=2.0 * std::f32::consts::PI).text("angle")); + ui.add(Slider::new(&mut self.angle, 0.0..=2.0 * core::f32::consts::PI).text("angle")); let default_color = if ui.visuals().dark_mode { Color32::LIGHT_GRAY diff --git a/crates/egui_demo_lib/src/demo/sliders.rs b/crates/egui_demo_lib/src/demo/sliders.rs index 7dab0f26c..d2371cafd 100644 --- a/crates/egui_demo_lib/src/demo/sliders.rs +++ b/crates/egui_demo_lib/src/demo/sliders.rs @@ -125,7 +125,7 @@ impl crate::View for Sliders { ); if ui.button("Assign PI").clicked() { - self.value = std::f64::consts::PI; + self.value = core::f64::consts::PI; } } diff --git a/crates/egui_demo_lib/src/demo/tests/grid_test.rs b/crates/egui_demo_lib/src/demo/tests/grid_test.rs index 1806eb431..7befe09e1 100644 --- a/crates/egui_demo_lib/src/demo/tests/grid_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/grid_test.rs @@ -113,7 +113,7 @@ impl crate::View for GridTest { ui.end_row(); let mut dyn_text = String::from("O"); - dyn_text.extend(std::iter::repeat_n('h', self.text_length)); + dyn_text.extend(core::iter::repeat_n('h', self.text_length)); ui.label(dyn_text); ui.label("Fifth row, second column"); ui.end_row(); diff --git a/crates/egui_demo_lib/src/demo/tests/input_test.rs b/crates/egui_demo_lib/src/demo/tests/input_test.rs index e237b3e4b..ca753d650 100644 --- a/crates/egui_demo_lib/src/demo/tests/input_test.rs +++ b/crates/egui_demo_lib/src/demo/tests/input_test.rs @@ -123,7 +123,7 @@ impl crate::View for InputTest { } fn response_summary(response: &egui::Response, show_hovers: bool) -> String { - use std::fmt::Write as _; + use core::fmt::Write as _; let mut new_info = String::new(); diff --git a/crates/egui_extras/src/datepicker/button.rs b/crates/egui_extras/src/datepicker/button.rs index 42e4f900f..94770ca71 100644 --- a/crates/egui_extras/src/datepicker/button.rs +++ b/crates/egui_extras/src/datepicker/button.rs @@ -1,7 +1,7 @@ use super::popup::DatePickerPopup; +use core::ops::RangeInclusive; use egui::{Area, Button, Frame, InnerResponse, Key, Order, RichText, Ui, Widget}; use jiff::civil::Date; -use std::ops::RangeInclusive; #[derive(Default, Clone)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/crates/egui_extras/src/datepicker/mod.rs b/crates/egui_extras/src/datepicker/mod.rs index f1f6e58fa..1eaf37118 100644 --- a/crates/egui_extras/src/datepicker/mod.rs +++ b/crates/egui_extras/src/datepicker/mod.rs @@ -26,7 +26,7 @@ fn month_data(year: i16, month: i8) -> Vec { if start.weekday() == Weekday::Sunday { weeks.push(Week { number: ISOWeekDate::from(start).week() as u8, - days: std::mem::take(&mut week), + days: core::mem::take(&mut week), }); } start = start.tomorrow().unwrap(); diff --git a/crates/egui_extras/src/datepicker/popup.rs b/crates/egui_extras/src/datepicker/popup.rs index 5c0726e5a..52bd71be4 100644 --- a/crates/egui_extras/src/datepicker/popup.rs +++ b/crates/egui_extras/src/datepicker/popup.rs @@ -32,7 +32,7 @@ pub(crate) struct DatePickerPopup<'a> { 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, } diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index bdafb6553..d566628b1 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -1,9 +1,10 @@ use ahash::HashMap; +use core::task::Poll; use egui::{ load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError}, mutex::Mutex, }; -use std::{path::PathBuf, sync::Arc, task::Poll, thread}; +use std::{path::PathBuf, sync::Arc, thread}; #[derive(Clone)] struct File { diff --git a/crates/egui_extras/src/loaders/gif_loader.rs b/crates/egui_extras/src/loaders/gif_loader.rs index ebaf9a6b3..c1242a1f9 100644 --- a/crates/egui_extras/src/loaders/gif_loader.rs +++ b/crates/egui_extras/src/loaders/gif_loader.rs @@ -1,11 +1,12 @@ use ahash::HashMap; +use core::{mem::size_of, time::Duration}; use egui::{ ColorImage, FrameDurations, Id, decode_animated_image_uri, has_gif_magic_header, load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, }; use image::AnimationDecoder as _; -use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration}; +use std::{io::Cursor, sync::Arc}; /// Array of Frames and the duration for how long each frame should be shown #[derive(Debug, Clone)] diff --git a/crates/egui_extras/src/loaders/http_loader.rs b/crates/egui_extras/src/loaders/http_loader.rs index e3b4d350e..6eb1bc5ba 100644 --- a/crates/egui_extras/src/loaders/http_loader.rs +++ b/crates/egui_extras/src/loaders/http_loader.rs @@ -1,9 +1,10 @@ use ahash::HashMap; +use core::task::Poll; use egui::{ load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError}, mutex::Mutex, }; -use std::{sync::Arc, task::Poll}; +use std::sync::Arc; #[derive(Clone)] struct File { diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 969ed5538..b14a57286 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -1,11 +1,12 @@ use ahash::HashMap; +use core::{mem::size_of, task::Poll}; use egui::{ ColorImage, decode_animated_image_uri, load::{Bytes, BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, }; use image::ImageFormat; -use std::{mem::size_of, path::Path, sync::Arc, task::Poll}; +use std::{path::Path, sync::Arc}; #[cfg(not(target_arch = "wasm32"))] use std::thread; @@ -146,7 +147,7 @@ impl ImageLoader for ImageCrateLoader { .map(Arc::new) .map_err(|err| err.to_string()); log::trace!("finished loading {uri:?}"); - cache_lock.insert(uri.into(), std::task::Poll::Ready(result.clone())); + cache_lock.insert(uri.into(), core::task::Poll::Ready(result.clone())); match result { Ok(image) => Ok(ImagePoll::Ready { image }), Err(err) => Err(LoadError::Loading(err)), diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 3bd881fbf..91063f6b4 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -1,4 +1,5 @@ -use std::{mem::size_of, sync::Arc}; +use core::mem::size_of; +use std::sync::Arc; use ahash::HashMap; diff --git a/crates/egui_extras/src/loaders/webp_loader.rs b/crates/egui_extras/src/loaders/webp_loader.rs index 23d778358..3ecd7068a 100644 --- a/crates/egui_extras/src/loaders/webp_loader.rs +++ b/crates/egui_extras/src/loaders/webp_loader.rs @@ -1,11 +1,12 @@ use ahash::HashMap; +use core::{mem::size_of, time::Duration}; use egui::{ ColorImage, FrameDurations, Id, decode_animated_image_uri, has_webp_header, load::{BytesPoll, ImageLoadResult, ImageLoader, ImagePoll, LoadError, SizeHint}, mutex::Mutex, }; use image::{AnimationDecoder as _, ColorType, ImageDecoder as _, Rgba, codecs::webp::WebPDecoder}; -use std::{io::Cursor, mem::size_of, sync::Arc, time::Duration}; +use std::{io::Cursor, sync::Arc}; #[derive(Clone)] enum WebP { diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index c3981b0e2..d09151d21 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -514,9 +514,9 @@ struct HighlightSettings<'a>(&'a SyntectSettings); #[derive(Copy, Clone)] struct HighlightSettings<'a>(&'a ()); -impl std::hash::Hash for HighlightSettings<'_> { - fn hash(&self, state: &mut H) { - std::ptr::hash(self.0, state); +impl core::hash::Hash for HighlightSettings<'_> { + fn hash(&self, state: &mut H) { + core::ptr::hash(self.0, state); } } diff --git a/crates/egui_glow/examples/pure_glow.rs b/crates/egui_glow/examples/pure_glow.rs index c8ce705c8..17fa95b73 100644 --- a/crates/egui_glow/examples/pure_glow.rs +++ b/crates/egui_glow/examples/pure_glow.rs @@ -5,7 +5,7 @@ #![expect(clippy::undocumented_unsafe_blocks)] #![expect(unsafe_code)] -use std::num::NonZeroU32; +use core::num::NonZeroU32; use std::sync::Arc; use egui_winit::winit; @@ -146,7 +146,7 @@ impl GlutinWindowContext { self.gl_surface.swap_buffers(&self.gl_context) } - fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void { + fn get_proc_address(&self, addr: &core::ffi::CStr) -> *const core::ffi::c_void { use glutin::display::GlDisplay as _; self.gl_display.get_proc_address(addr) } @@ -154,7 +154,7 @@ impl GlutinWindowContext { #[derive(Debug)] pub enum UserEvent { - Redraw(std::time::Duration), + Redraw(core::time::Duration), } struct GlowApp { @@ -162,7 +162,7 @@ struct GlowApp { gl_window: Option, gl: Option>, egui_glow: Option, - repaint_delay: std::time::Duration, + repaint_delay: core::time::Duration, clear_color: [f32; 3], } @@ -173,7 +173,7 @@ impl GlowApp { gl_window: None, gl: None, egui_glow: None, - repaint_delay: std::time::Duration::MAX, + repaint_delay: core::time::Duration::MAX, clear_color: [0.1, 0.1, 0.1], } } diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 2b2341da1..8a68de1f8 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -55,10 +55,10 @@ impl TextureWrapModeExt for egui::TextureWrapMode { #[derive(Debug)] pub struct PainterError(String); -impl std::error::Error for PainterError {} +impl core::error::Error for PainterError {} -impl std::fmt::Display for PainterError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for PainterError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "OpenGL: {}", self.0) } } @@ -219,7 +219,7 @@ impl Painter { let a_tc_loc = gl.get_attrib_location(program, "a_tc").unwrap(); let a_srgba_loc = gl.get_attrib_location(program, "a_srgba").unwrap(); - let stride = std::mem::size_of::() as i32; + let stride = core::mem::size_of::() as i32; let buffer_infos = vec![ vao::BufferInfo { location: a_pos_loc, diff --git a/crates/egui_glow/src/shader_version.rs b/crates/egui_glow/src/shader_version.rs index 7d0caf7fa..e34e44c68 100644 --- a/crates/egui_glow/src/shader_version.rs +++ b/crates/egui_glow/src/shader_version.rs @@ -2,7 +2,7 @@ #![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps #![expect(unsafe_code)] -use std::convert::TryInto as _; +use core::convert::TryInto as _; /// Helper for parsing and interpreting the OpenGL shader version. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/crates/egui_glow/src/winit.rs b/crates/egui_glow/src/winit.rs index 9d2b71310..6ec9b8215 100644 --- a/crates/egui_glow/src/winit.rs +++ b/crates/egui_glow/src/winit.rs @@ -104,8 +104,8 @@ impl EguiGlow { /// Paint the results of the last call to [`Self::run`]. pub fn paint(&mut self, window: &winit::window::Window) { - let shapes = std::mem::take(&mut self.shapes); - let mut textures_delta = std::mem::take(&mut self.textures_delta); + let shapes = core::mem::take(&mut self.shapes); + let mut textures_delta = core::mem::take(&mut self.textures_delta); #[expect(clippy::iter_over_hash_type)] // Order doesn't matter here for (id, image_deltas) in textures_delta.set.drain() { diff --git a/crates/egui_inspection/src/plugin.rs b/crates/egui_inspection/src/plugin.rs index 0ae857ab1..76ad8893a 100644 --- a/crates/egui_inspection/src/plugin.rs +++ b/crates/egui_inspection/src/plugin.rs @@ -33,8 +33,8 @@ //! Note that [`serve`]'s threads hold an [`egui::Context`] clone, so the context stays alive //! for as long as the listener runs (the lifetime of the process, for a debug attach). +use core::time::Duration; use std::sync::mpsc; -use std::time::Duration; use egui::{Context, FullOutput, RawInput}; diff --git a/crates/egui_inspection/src/protocol.rs b/crates/egui_inspection/src/protocol.rs index 631e7c7b1..e95ce5cf5 100644 --- a/crates/egui_inspection/src/protocol.rs +++ b/crates/egui_inspection/src/protocol.rs @@ -136,7 +136,7 @@ pub struct EncodedPng { /// Hard cap on a single framed message. Matches the sanity limit enforced by both ends. pub const MAX_MESSAGE_BYTES: usize = 256 * 1024 * 1024; // 256 MiB -fn invalid_data(err: impl std::fmt::Display) -> io::Error { +fn invalid_data(err: impl core::fmt::Display) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, err.to_string()) } diff --git a/crates/egui_kittest/src/builder.rs b/crates/egui_kittest/src/builder.rs index dc4757ee5..23109544b 100644 --- a/crates/egui_kittest/src/builder.rs +++ b/crates/egui_kittest/src/builder.rs @@ -2,8 +2,8 @@ use crate::app_kind::AppKind; #[cfg(feature = "eframe")] use crate::app_kind::AppKindEframe; use crate::{Harness, LazyRenderer, TestRenderer}; +use core::marker::PhantomData; use egui::{Pos2, Rect, Vec2}; -use std::marker::PhantomData; /// Builder for [`Harness`]. #[must_use] diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 2a8575ef1..fa8f26311 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -26,7 +26,7 @@ pub use { kittest, }; -use std::{ +use core::{ fmt::{Debug, Display, Formatter}, time::Duration, }; @@ -47,7 +47,7 @@ pub struct ExceededMaxStepsError { } impl Display for ExceededMaxStepsError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!( f, "Harness::run exceeded max_steps ({}). If your expect your ui to keep repainting \ @@ -90,7 +90,7 @@ pub struct Harness<'a, State = ()> { } impl Debug for Harness<'_, State> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { self.kittest.fmt(f) } } @@ -245,7 +245,7 @@ impl<'a, State> Harness<'a, State> { /// This will call the app closure with each queued event and /// update the Harness. pub fn step(&mut self) { - let events = std::mem::take(&mut *self.queued_events.lock()); + let events = core::mem::take(&mut *self.queued_events.lock()); if events.is_empty() { self._step(false); } @@ -802,7 +802,7 @@ impl<'a, State> Harness<'a, State> { // SAFETY: `pthread_main_np` is a thread-safe libc query with no arguments. let is_main_thread = unsafe { unsafe extern "C" { - fn pthread_main_np() -> std::ffi::c_int; + fn pthread_main_np() -> core::ffi::c_int; } pthread_main_np() != 0 }; diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs index 729fda763..285602a84 100644 --- a/crates/egui_kittest/src/node.rs +++ b/crates/egui_kittest/src/node.rs @@ -1,8 +1,8 @@ +use core::fmt::{Debug, Formatter}; use egui::accesskit::ActionRequest; use egui::mutex::Mutex; use egui::{Modifiers, PointerButton, Pos2, accesskit}; use kittest::{AccessKitNode, NodeT, debug_fmt_node}; -use std::fmt::{Debug, Formatter}; pub type EventQueue = Mutex>; @@ -14,7 +14,7 @@ pub struct Node<'tree> { } impl Debug for Node<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { debug_fmt_node(self, f) } } diff --git a/crates/egui_kittest/src/renderer.rs b/crates/egui_kittest/src/renderer.rs index 4abcf31c4..1f18a7fa3 100644 --- a/crates/egui_kittest/src/renderer.rs +++ b/crates/egui_kittest/src/renderer.rs @@ -1,5 +1,5 @@ +use core::mem; use egui::TexturesDelta; -use std::mem; pub trait TestRenderer { /// We use this to pass the glow / wgpu render state to [`eframe::Frame`]. diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index 09ce986c8..e4472219f 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use core::fmt::Display; use std::io::ErrorKind; use std::path::PathBuf; @@ -305,7 +305,7 @@ const HOW_TO_UPDATE_SCREENSHOTS: &str = "Run `UPDATE_SNAPSHOTS=1 cargo test --all-features` to update the snapshots."; impl Display for SnapshotError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Diff { name, @@ -840,7 +840,7 @@ impl Harness<'_, State> { /// This removes the snapshot results from the harness. Useful if you e.g. want to merge it /// with the results from another harness (using [`SnapshotResults::add`]). pub fn take_snapshot_results(&mut self) -> SnapshotResults { - std::mem::take(&mut self.snapshot_results) + core::mem::take(&mut self.snapshot_results) } } @@ -872,7 +872,7 @@ impl Harness<'_, State> { pub struct SnapshotResults { errors: Vec, handled: bool, - location: std::panic::Location<'static>, + location: core::panic::Location<'static>, } impl Default for SnapshotResults { @@ -881,13 +881,13 @@ impl Default for SnapshotResults { Self { errors: Vec::new(), handled: true, // If no snapshots were added, we should consider this handled. - location: *std::panic::Location::caller(), + location: *core::panic::Location::caller(), } } } impl Display for SnapshotResults { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if self.errors.is_empty() { write!(f, "All snapshots passed") } else { @@ -939,7 +939,7 @@ impl SnapshotResults { /// Consume this and return the list of errors. pub fn into_inner(mut self) -> Vec { self.handled = true; - std::mem::take(&mut self.errors) + core::mem::take(&mut self.errors) } /// Panics if there are any errors, displaying each. @@ -968,7 +968,7 @@ impl Drop for SnapshotResults { } thread_local! { - static UNHANDLED_SNAPSHOT_RESULTS_COUNTER: std::cell::RefCell = const { std::cell::RefCell::new(0) }; + static UNHANDLED_SNAPSHOT_RESULTS_COUNTER: core::cell::RefCell = const { core::cell::RefCell::new(0) }; } if !self.handled { diff --git a/crates/egui_kittest/src/texture_to_image.rs b/crates/egui_kittest/src/texture_to_image.rs index 289033fba..5c292c6cd 100644 --- a/crates/egui_kittest/src/texture_to_image.rs +++ b/crates/egui_kittest/src/texture_to_image.rs @@ -1,8 +1,8 @@ +use core::iter; +use core::mem::size_of; use egui_wgpu::wgpu; use egui_wgpu::wgpu::{Device, Extent3d, Queue, Texture}; use image::RgbaImage; -use std::iter; -use std::mem::size_of; use std::sync::mpsc::channel; use crate::wgpu::WAIT_TIMEOUT; diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index e5266aead..af751fe40 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -1,5 +1,5 @@ +use core::{iter::once, time::Duration}; use std::sync::Arc; -use std::{iter::once, time::Duration}; use egui::TexturesDelta; use egui_wgpu::{RenderState, ScreenDescriptor, WgpuSetup, wgpu}; @@ -230,7 +230,7 @@ impl crate::TestRenderer for WgpuTestRenderer { self.render_state .queue - .submit(std::iter::chain(user_buffers, once(encoder.finish()))); + .submit(core::iter::chain(user_buffers, once(encoder.finish()))); self.render_state .device diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 8aad4dea1..12e66ac56 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -662,7 +662,7 @@ fn window_fixed_size_is_outer_size() { /// allowed size — they used to inherit the overflowing content rect. #[test] fn panel_rect_clamped_when_content_overflows() { - use std::cell::RefCell; + use core::cell::RefCell; let side_panel_width = 100.0_f32; let top_panel_height = 80.0_f32; @@ -723,7 +723,7 @@ fn panel_rect_clamped_when_content_overflows() { /// portion of the panel. #[test] fn collapsing_panel_must_not_grow_enclosing_window() { - use std::cell::RefCell; + use core::cell::RefCell; let window_rect: RefCell> = RefCell::new(None); let is_expanded: RefCell = RefCell::new(true); diff --git a/crates/emath/src/align.rs b/crates/emath/src/align.rs index 395323d4f..4001af4bf 100644 --- a/crates/emath/src/align.rs +++ b/crates/emath/src/align.rs @@ -274,7 +274,7 @@ impl Align2 { } } -impl std::ops::Index for Align2 { +impl core::ops::Index for Align2 { type Output = Align; #[inline(always)] @@ -283,7 +283,7 @@ impl std::ops::Index for Align2 { } } -impl std::ops::IndexMut for Align2 { +impl core::ops::IndexMut for Align2 { #[inline(always)] fn index_mut(&mut self, index: usize) -> &mut Align { &mut self.0[index] @@ -299,8 +299,8 @@ pub fn center_size_in_rect(size: Vec2, frame: Rect) -> Rect { Align2::CENTER_CENTER.align_size_within_rect(size, frame) } -impl std::fmt::Debug for Align2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Align2 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "Align2({:?}, {:?})", self.x(), self.y()) } } diff --git a/crates/emath/src/easing.rs b/crates/emath/src/easing.rs index 6a98cad80..57c439f27 100644 --- a/crates/emath/src/easing.rs +++ b/crates/emath/src/easing.rs @@ -5,7 +5,7 @@ //! All functions take a value in `[0, 1]` and return a value in `[0, 1]`. //! //! Derived from . -use std::f32::consts::PI; +use core::f32::consts::PI; use crate::fast_midpoint; diff --git a/crates/emath/src/history.rs b/crates/emath/src/history.rs index 4a49defa2..3b0d1fb56 100644 --- a/crates/emath/src/history.rs +++ b/crates/emath/src/history.rs @@ -52,7 +52,7 @@ where /// history.add(now(), 44.0_f32); /// assert_eq!(history.average(), Some(42.0)); /// ``` - pub fn new(length_range: std::ops::Range, max_age: f32) -> Self { + pub fn new(length_range: core::ops::Range, max_age: f32) -> Self { Self { min_len: length_range.start, max_len: length_range.end, @@ -175,8 +175,8 @@ where impl History where T: Copy, - T: std::iter::Sum, - T: std::ops::Div, + T: core::iter::Sum, + T: core::ops::Div, { #[inline] pub fn sum(&self) -> T { @@ -196,9 +196,9 @@ where impl History where T: Copy, - T: std::iter::Sum, - T: std::ops::Div, - T: std::ops::Mul, + T: core::iter::Sum, + T: core::ops::Div, + T: core::ops::Mul, { /// Average times rate. /// If you are keeping track of individual sizes of things (e.g. bytes), @@ -211,8 +211,8 @@ where impl History where T: Copy, - T: std::ops::Sub, - Vel: std::ops::Div, + T: core::ops::Sub, + Vel: core::ops::Div, { /// Calculate a smooth velocity (per second) over the entire time span. /// Calculated as the last value minus the first value over the elapsed time between them. diff --git a/crates/emath/src/lib.rs b/crates/emath/src/lib.rs index 92e34620a..1e7b3b807 100644 --- a/crates/emath/src/lib.rs +++ b/crates/emath/src/lib.rs @@ -21,7 +21,7 @@ #![expect(clippy::float_cmp)] -use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; +use core::ops::{Add, Div, Mul, RangeInclusive, Sub}; // ---------------------------------------------------------------------------- @@ -269,7 +269,7 @@ fn test_format() { assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14"); assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140"); assert_eq!( - format_with_minimum_decimals(std::f64::consts::PI, 2), + format_with_minimum_decimals(core::f64::consts::PI, 2), "3.14159" ); } @@ -365,7 +365,7 @@ impl_num_ext!(Pos2); /// Wrap angle to `[-PI, PI]` range. pub fn normalized_angle(mut angle: f32) -> f32 { - use std::f32::consts::{PI, TAU}; + use core::f32::consts::{PI, TAU}; angle %= TAU; if angle > PI { angle -= TAU; @@ -385,7 +385,7 @@ fn test_normalized_angle() { }; } - use std::f32::consts::TAU; + use core::f32::consts::TAU; almost_eq!(normalized_angle(-3.0 * TAU), 0.0); almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU); almost_eq!(normalized_angle(-TAU), 0.0); diff --git a/crates/emath/src/numeric.rs b/crates/emath/src/numeric.rs index b4b6174e2..a94cfbe6d 100644 --- a/crates/emath/src/numeric.rs +++ b/crates/emath/src/numeric.rs @@ -92,9 +92,9 @@ impl_numeric_integer!(i64); impl_numeric_integer!(u64); impl_numeric_integer!(isize); impl_numeric_integer!(usize); -impl_numeric_non_zero_unsigned!(std::num::NonZeroU8); -impl_numeric_non_zero_unsigned!(std::num::NonZeroU16); -impl_numeric_non_zero_unsigned!(std::num::NonZeroU32); -impl_numeric_non_zero_unsigned!(std::num::NonZeroU64); -impl_numeric_non_zero_unsigned!(std::num::NonZeroU128); -impl_numeric_non_zero_unsigned!(std::num::NonZeroUsize); +impl_numeric_non_zero_unsigned!(core::num::NonZeroU8); +impl_numeric_non_zero_unsigned!(core::num::NonZeroU16); +impl_numeric_non_zero_unsigned!(core::num::NonZeroU32); +impl_numeric_non_zero_unsigned!(core::num::NonZeroU64); +impl_numeric_non_zero_unsigned!(core::num::NonZeroU128); +impl_numeric_non_zero_unsigned!(core::num::NonZeroUsize); diff --git a/crates/emath/src/ordered_float.rs b/crates/emath/src/ordered_float.rs index 369541fed..dc86e2472 100644 --- a/crates/emath/src/ordered_float.rs +++ b/crates/emath/src/ordered_float.rs @@ -1,8 +1,8 @@ //! Total order on floating point types. //! Can be used for sorting, min/max computation, and other collection algorithms. -use std::cmp::Ordering; -use std::hash::{Hash, Hasher}; +use core::cmp::Ordering; +use core::hash::{Hash, Hasher}; /// Wraps a floating-point value to add total order and hash. /// Possible types for `T` are `f32` and `f64`. @@ -21,9 +21,9 @@ impl OrderedFloat { } } -impl std::fmt::Debug for OrderedFloat { +impl core::fmt::Debug for OrderedFloat { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.0.fmt(f) } } diff --git a/crates/emath/src/pos2.rs b/crates/emath/src/pos2.rs index f67767e6b..d331769dc 100644 --- a/crates/emath/src/pos2.rs +++ b/crates/emath/src/pos2.rs @@ -1,4 +1,4 @@ -use std::{ +use core::{ fmt, ops::{Add, AddAssign, MulAssign, Sub, SubAssign}, }; @@ -206,7 +206,7 @@ impl Pos2 { } } -impl std::ops::Index for Pos2 { +impl core::ops::Index for Pos2 { type Output = f32; #[inline(always)] @@ -219,7 +219,7 @@ impl std::ops::Index for Pos2 { } } -impl std::ops::IndexMut for Pos2 { +impl core::ops::IndexMut for Pos2 { #[inline(always)] fn index_mut(&mut self, index: usize) -> &mut f32 { match index { diff --git a/crates/emath/src/range.rs b/crates/emath/src/range.rs index be6072c71..991659f5d 100644 --- a/crates/emath/src/range.rs +++ b/crates/emath/src/range.rs @@ -1,4 +1,4 @@ -use std::ops::{RangeFrom, RangeFull, RangeInclusive, RangeToInclusive}; +use core::ops::{RangeFrom, RangeFull, RangeInclusive, RangeToInclusive}; use crate::fast_midpoint; diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index 8fd04b431..f01387c18 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -1,7 +1,7 @@ use std::fmt; use crate::{Div, Mul, NumExt as _, Pos2, Rangef, Rot2, Vec2, fast_midpoint, lerp, pos2, vec2}; -use std::ops::{BitOr, BitOrAssign}; +use core::ops::{BitOr, BitOrAssign}; /// A rectangular region of space. /// @@ -727,7 +727,7 @@ impl Rect { let mut t1 = (self.max[i] - self.center()[i]) * inv_d; if inv_d < 0.0 { - std::mem::swap(&mut t0, &mut t1); + core::mem::swap(&mut t0, &mut t1); } tmin = tmin.max(t0); diff --git a/crates/emath/src/rect_transform.rs b/crates/emath/src/rect_transform.rs index 3539efe75..05355620a 100644 --- a/crates/emath/src/rect_transform.rs +++ b/crates/emath/src/rect_transform.rs @@ -65,7 +65,7 @@ impl RectTransform { } /// Transforms the position. -impl std::ops::Mul for RectTransform { +impl core::ops::Mul for RectTransform { type Output = Pos2; fn mul(self, pos: Pos2) -> Pos2 { @@ -74,7 +74,7 @@ impl std::ops::Mul for RectTransform { } /// Transforms the position. -impl std::ops::Mul for &RectTransform { +impl core::ops::Mul for &RectTransform { type Output = Pos2; fn mul(self, pos: Pos2) -> Pos2 { diff --git a/crates/emath/src/rot2.rs b/crates/emath/src/rot2.rs index 9af0103a0..9aa4b52d7 100644 --- a/crates/emath/src/rot2.rs +++ b/crates/emath/src/rot2.rs @@ -92,8 +92,8 @@ impl Rot2 { } } -impl std::fmt::Debug for Rot2 { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for Rot2 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if let Some(precision) = f.precision() { write!( f, @@ -113,7 +113,7 @@ impl std::fmt::Debug for Rot2 { } } -impl std::ops::Mul for Rot2 { +impl core::ops::Mul for Rot2 { type Output = Self; #[inline] @@ -130,7 +130,7 @@ impl std::ops::Mul for Rot2 { } /// Rotates (and maybe scales) the vector. -impl std::ops::Mul for Rot2 { +impl core::ops::Mul for Rot2 { type Output = Vec2; #[inline] @@ -143,7 +143,7 @@ impl std::ops::Mul for Rot2 { } /// Scales the rotor. -impl std::ops::Mul for f32 { +impl core::ops::Mul for f32 { type Output = Rot2; #[inline] @@ -156,7 +156,7 @@ impl std::ops::Mul for f32 { } /// Scales the rotor. -impl std::ops::Mul for Rot2 { +impl core::ops::Mul for Rot2 { type Output = Self; #[inline] @@ -169,7 +169,7 @@ impl std::ops::Mul for Rot2 { } /// Scales the rotor. -impl std::ops::Div for Rot2 { +impl core::ops::Div for Rot2 { type Output = Self; #[inline] @@ -189,7 +189,7 @@ mod test { #[test] fn test_rotation2() { { - let angle = std::f32::consts::TAU / 6.0; + let angle = core::f32::consts::TAU / 6.0; let rot = Rot2::from_angle(angle); assert!((rot.angle() - angle).abs() < 1e-5); assert!((rot * rot.inverse()).angle().abs() < 1e-5); @@ -197,14 +197,14 @@ mod test { } { - let angle = std::f32::consts::TAU / 4.0; + let angle = core::f32::consts::TAU / 4.0; let rot = Rot2::from_angle(angle); assert!(((rot * vec2(1.0, 0.0)) - vec2(0.0, 1.0)).length() < 1e-5); } { // Test rotation and scaling - let angle = std::f32::consts::TAU / 4.0; + let angle = core::f32::consts::TAU / 4.0; let rot = 3.0 * Rot2::from_angle(angle); let rotated = rot * vec2(1.0, 0.0); let expected = vec2(0.0, 3.0); diff --git a/crates/emath/src/ts_transform.rs b/crates/emath/src/ts_transform.rs index 515f5ecd7..5d36911ed 100644 --- a/crates/emath/src/ts_transform.rs +++ b/crates/emath/src/ts_transform.rs @@ -109,7 +109,7 @@ impl TSTransform { } /// Transforms the position. -impl std::ops::Mul for TSTransform { +impl core::ops::Mul for TSTransform { type Output = Pos2; #[inline] @@ -119,7 +119,7 @@ impl std::ops::Mul for TSTransform { } /// Transforms the rectangle. -impl std::ops::Mul for TSTransform { +impl core::ops::Mul for TSTransform { type Output = Rect; #[inline] @@ -128,7 +128,7 @@ impl std::ops::Mul for TSTransform { } } -impl std::ops::Mul for TSTransform { +impl core::ops::Mul for TSTransform { type Output = Self; #[inline] diff --git a/crates/emath/src/vec2.rs b/crates/emath/src/vec2.rs index f79359df9..5422aa79b 100644 --- a/crates/emath/src/vec2.rs +++ b/crates/emath/src/vec2.rs @@ -1,5 +1,5 @@ +use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use std::fmt; -use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; use crate::Vec2b; @@ -322,7 +322,7 @@ impl Vec2 { } } -impl std::ops::Index for Vec2 { +impl core::ops::Index for Vec2 { type Output = f32; #[inline(always)] @@ -335,7 +335,7 @@ impl std::ops::Index for Vec2 { } } -impl std::ops::IndexMut for Vec2 { +impl core::ops::IndexMut for Vec2 { #[inline(always)] fn index_mut(&mut self, index: usize) -> &mut f32 { match index { @@ -514,7 +514,7 @@ mod test { #[test] fn test_vec2() { - use std::f32::consts::TAU; + use core::f32::consts::TAU; assert_eq!(Vec2::ZERO.angle(), 0.0); assert_eq!(Vec2::angled(0.0).angle(), 0.0); @@ -547,7 +547,7 @@ mod test { #[test] fn test_vec2_normalized() { fn generate_spiral(n: usize, start: Vec2, end: Vec2) -> impl Iterator { - let angle_step = 2.0 * std::f32::consts::PI / n as f32; + let angle_step = 2.0 * core::f32::consts::PI / n as f32; let radius_step = (end.length() - start.length()) / n as f32; (0..n).map(move |i| { diff --git a/crates/emath/src/vec2b.rs b/crates/emath/src/vec2b.rs index 673f2959e..be5c36f78 100644 --- a/crates/emath/src/vec2b.rs +++ b/crates/emath/src/vec2b.rs @@ -67,7 +67,7 @@ impl From<[bool; 2]> for Vec2b { } } -impl std::ops::Index for Vec2b { +impl core::ops::Index for Vec2b { type Output = bool; #[inline(always)] @@ -80,7 +80,7 @@ impl std::ops::Index for Vec2b { } } -impl std::ops::IndexMut for Vec2b { +impl core::ops::IndexMut for Vec2b { #[inline(always)] fn index_mut(&mut self, index: usize) -> &mut bool { match index { @@ -91,7 +91,7 @@ impl std::ops::IndexMut for Vec2b { } } -impl std::ops::Not for Vec2b { +impl core::ops::Not for Vec2b { type Output = Self; #[inline] diff --git a/crates/epaint/benches/benchmark.rs b/crates/epaint/benches/benchmark.rs index 8fbfc65ea..5cafeaf0f 100644 --- a/crates/epaint/benches/benchmark.rs +++ b/crates/epaint/benches/benchmark.rs @@ -5,7 +5,7 @@ use epaint::{ Tessellator, TextureAtlas, Vec2, pos2, tessellator::Path, }; -use std::hint::black_box; +use core::hint::black_box; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; // Much faster allocator diff --git a/crates/epaint/src/color.rs b/crates/epaint/src/color.rs index 54106c10d..8aa4f56df 100644 --- a/crates/epaint/src/color.rs +++ b/crates/epaint/src/color.rs @@ -1,4 +1,5 @@ -use std::{fmt::Debug, sync::Arc}; +use core::fmt::Debug; +use std::sync::Arc; use ecolor::Color32; use emath::{Pos2, Rect}; @@ -25,7 +26,7 @@ impl Default for ColorMode { } impl Debug for ColorMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Solid(arg0) => f.debug_tuple("Solid").field(arg0).finish(), Self::UV(_arg0) => f.debug_tuple("UV").field(&"").finish(), diff --git a/crates/epaint/src/corner_radius.rs b/crates/epaint/src/corner_radius.rs index 07bd56c9e..dd0b432b9 100644 --- a/crates/epaint/src/corner_radius.rs +++ b/crates/epaint/src/corner_radius.rs @@ -99,7 +99,7 @@ impl CornerRadius { } } -impl std::ops::Add for CornerRadius { +impl core::ops::Add for CornerRadius { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self { @@ -112,7 +112,7 @@ impl std::ops::Add for CornerRadius { } } -impl std::ops::Add for CornerRadius { +impl core::ops::Add for CornerRadius { type Output = Self; #[inline] fn add(self, rhs: u8) -> Self { @@ -125,7 +125,7 @@ impl std::ops::Add for CornerRadius { } } -impl std::ops::AddAssign for CornerRadius { +impl core::ops::AddAssign for CornerRadius { #[inline] fn add_assign(&mut self, rhs: Self) { *self = Self { @@ -137,7 +137,7 @@ impl std::ops::AddAssign for CornerRadius { } } -impl std::ops::AddAssign for CornerRadius { +impl core::ops::AddAssign for CornerRadius { #[inline] fn add_assign(&mut self, rhs: u8) { *self = Self { @@ -149,7 +149,7 @@ impl std::ops::AddAssign for CornerRadius { } } -impl std::ops::Sub for CornerRadius { +impl core::ops::Sub for CornerRadius { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self { @@ -162,7 +162,7 @@ impl std::ops::Sub for CornerRadius { } } -impl std::ops::Sub for CornerRadius { +impl core::ops::Sub for CornerRadius { type Output = Self; #[inline] fn sub(self, rhs: u8) -> Self { @@ -175,7 +175,7 @@ impl std::ops::Sub for CornerRadius { } } -impl std::ops::SubAssign for CornerRadius { +impl core::ops::SubAssign for CornerRadius { #[inline] fn sub_assign(&mut self, rhs: Self) { *self = Self { @@ -187,7 +187,7 @@ impl std::ops::SubAssign for CornerRadius { } } -impl std::ops::SubAssign for CornerRadius { +impl core::ops::SubAssign for CornerRadius { #[inline] fn sub_assign(&mut self, rhs: u8) { *self = Self { @@ -199,7 +199,7 @@ impl std::ops::SubAssign for CornerRadius { } } -impl std::ops::Div for CornerRadius { +impl core::ops::Div for CornerRadius { type Output = Self; #[inline] fn div(self, rhs: f32) -> Self { @@ -212,7 +212,7 @@ impl std::ops::Div for CornerRadius { } } -impl std::ops::DivAssign for CornerRadius { +impl core::ops::DivAssign for CornerRadius { #[inline] fn div_assign(&mut self, rhs: f32) { *self = Self { @@ -224,7 +224,7 @@ impl std::ops::DivAssign for CornerRadius { } } -impl std::ops::Mul for CornerRadius { +impl core::ops::Mul for CornerRadius { type Output = Self; #[inline] fn mul(self, rhs: f32) -> Self { @@ -237,7 +237,7 @@ impl std::ops::Mul for CornerRadius { } } -impl std::ops::MulAssign for CornerRadius { +impl core::ops::MulAssign for CornerRadius { #[inline] fn mul_assign(&mut self, rhs: f32) { *self = Self { diff --git a/crates/epaint/src/corner_radius_f32.rs b/crates/epaint/src/corner_radius_f32.rs index 0a88aaac7..ef6e597f5 100644 --- a/crates/epaint/src/corner_radius_f32.rs +++ b/crates/epaint/src/corner_radius_f32.rs @@ -111,7 +111,7 @@ impl CornerRadiusF32 { } } -impl std::ops::Add for CornerRadiusF32 { +impl core::ops::Add for CornerRadiusF32 { type Output = Self; #[inline] fn add(self, rhs: Self) -> Self { @@ -124,7 +124,7 @@ impl std::ops::Add for CornerRadiusF32 { } } -impl std::ops::AddAssign for CornerRadiusF32 { +impl core::ops::AddAssign for CornerRadiusF32 { #[inline] fn add_assign(&mut self, rhs: Self) { *self = Self { @@ -136,7 +136,7 @@ impl std::ops::AddAssign for CornerRadiusF32 { } } -impl std::ops::AddAssign for CornerRadiusF32 { +impl core::ops::AddAssign for CornerRadiusF32 { #[inline] fn add_assign(&mut self, rhs: f32) { *self = Self { @@ -148,7 +148,7 @@ impl std::ops::AddAssign for CornerRadiusF32 { } } -impl std::ops::Sub for CornerRadiusF32 { +impl core::ops::Sub for CornerRadiusF32 { type Output = Self; #[inline] fn sub(self, rhs: Self) -> Self { @@ -161,7 +161,7 @@ impl std::ops::Sub for CornerRadiusF32 { } } -impl std::ops::SubAssign for CornerRadiusF32 { +impl core::ops::SubAssign for CornerRadiusF32 { #[inline] fn sub_assign(&mut self, rhs: Self) { *self = Self { @@ -173,7 +173,7 @@ impl std::ops::SubAssign for CornerRadiusF32 { } } -impl std::ops::SubAssign for CornerRadiusF32 { +impl core::ops::SubAssign for CornerRadiusF32 { #[inline] fn sub_assign(&mut self, rhs: f32) { *self = Self { @@ -185,7 +185,7 @@ impl std::ops::SubAssign for CornerRadiusF32 { } } -impl std::ops::Div for CornerRadiusF32 { +impl core::ops::Div for CornerRadiusF32 { type Output = Self; #[inline] fn div(self, rhs: f32) -> Self { @@ -198,7 +198,7 @@ impl std::ops::Div for CornerRadiusF32 { } } -impl std::ops::DivAssign for CornerRadiusF32 { +impl core::ops::DivAssign for CornerRadiusF32 { #[inline] fn div_assign(&mut self, rhs: f32) { *self = Self { @@ -210,7 +210,7 @@ impl std::ops::DivAssign for CornerRadiusF32 { } } -impl std::ops::Mul for CornerRadiusF32 { +impl core::ops::Mul for CornerRadiusF32 { type Output = Self; #[inline] fn mul(self, rhs: f32) -> Self { @@ -223,7 +223,7 @@ impl std::ops::Mul for CornerRadiusF32 { } } -impl std::ops::MulAssign for CornerRadiusF32 { +impl core::ops::MulAssign for CornerRadiusF32 { #[inline] fn mul_assign(&mut self, rhs: f32) { *self = Self { diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 6fbd2b38f..92890e180 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -301,7 +301,7 @@ impl ColorImage { } } -impl std::ops::Index<(usize, usize)> for ColorImage { +impl core::ops::Index<(usize, usize)> for ColorImage { type Output = Color32; #[inline] @@ -312,7 +312,7 @@ impl std::ops::Index<(usize, usize)> for ColorImage { } } -impl std::ops::IndexMut<(usize, usize)> for ColorImage { +impl core::ops::IndexMut<(usize, usize)> for ColorImage { #[inline] fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 { let [w, h] = self.size; @@ -335,8 +335,8 @@ impl From> for ImageData { } } -impl std::fmt::Debug for ColorImage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for ColorImage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("ColorImage") .field("size", &self.size) .field("pixel-count", &self.pixels.len()) diff --git a/crates/epaint/src/margin.rs b/crates/epaint/src/margin.rs index 0e2063efe..765208ef1 100644 --- a/crates/epaint/src/margin.rs +++ b/crates/epaint/src/margin.rs @@ -120,7 +120,7 @@ impl From for Margin { } /// `Margin + Margin` -impl std::ops::Add for Margin { +impl core::ops::Add for Margin { type Output = Self; #[inline] @@ -135,7 +135,7 @@ impl std::ops::Add for Margin { } /// `Margin + i8` -impl std::ops::Add for Margin { +impl core::ops::Add for Margin { type Output = Self; #[inline] @@ -150,7 +150,7 @@ impl std::ops::Add for Margin { } /// `Margin += i8` -impl std::ops::AddAssign for Margin { +impl core::ops::AddAssign for Margin { #[inline] fn add_assign(&mut self, v: i8) { *self = *self + v; @@ -158,7 +158,7 @@ impl std::ops::AddAssign for Margin { } /// `Margin * f32` -impl std::ops::Mul for Margin { +impl core::ops::Mul for Margin { type Output = Self; #[inline] @@ -173,7 +173,7 @@ impl std::ops::Mul for Margin { } /// `Margin *= f32` -impl std::ops::MulAssign for Margin { +impl core::ops::MulAssign for Margin { #[inline] fn mul_assign(&mut self, v: f32) { *self = *self * v; @@ -181,7 +181,7 @@ impl std::ops::MulAssign for Margin { } /// `Margin / f32` -impl std::ops::Div for Margin { +impl core::ops::Div for Margin { type Output = Self; #[inline] @@ -192,7 +192,7 @@ impl std::ops::Div for Margin { } /// `Margin /= f32` -impl std::ops::DivAssign for Margin { +impl core::ops::DivAssign for Margin { #[inline] fn div_assign(&mut self, v: f32) { *self = *self / v; @@ -200,7 +200,7 @@ impl std::ops::DivAssign for Margin { } /// `Margin - Margin` -impl std::ops::Sub for Margin { +impl core::ops::Sub for Margin { type Output = Self; #[inline] @@ -215,7 +215,7 @@ impl std::ops::Sub for Margin { } /// `Margin - i8` -impl std::ops::Sub for Margin { +impl core::ops::Sub for Margin { type Output = Self; #[inline] @@ -230,7 +230,7 @@ impl std::ops::Sub for Margin { } /// `Margin -= i8` -impl std::ops::SubAssign for Margin { +impl core::ops::SubAssign for Margin { #[inline] fn sub_assign(&mut self, v: i8) { *self = *self - v; @@ -238,7 +238,7 @@ impl std::ops::SubAssign for Margin { } /// `Rect + Margin` -impl std::ops::Add for Rect { +impl core::ops::Add for Rect { type Output = Self; #[inline] @@ -251,7 +251,7 @@ impl std::ops::Add for Rect { } /// `Rect += Margin` -impl std::ops::AddAssign for Rect { +impl core::ops::AddAssign for Rect { #[inline] fn add_assign(&mut self, margin: Margin) { *self = *self + margin; @@ -259,7 +259,7 @@ impl std::ops::AddAssign for Rect { } /// `Rect - Margin` -impl std::ops::Sub for Rect { +impl core::ops::Sub for Rect { type Output = Self; #[inline] @@ -272,7 +272,7 @@ impl std::ops::Sub for Rect { } /// `Rect -= Margin` -impl std::ops::SubAssign for Rect { +impl core::ops::SubAssign for Rect { #[inline] fn sub_assign(&mut self, margin: Margin) { *self = *self - margin; diff --git a/crates/epaint/src/margin_f32.rs b/crates/epaint/src/margin_f32.rs index 2a8c820a1..793e5de47 100644 --- a/crates/epaint/src/margin_f32.rs +++ b/crates/epaint/src/margin_f32.rs @@ -111,7 +111,7 @@ impl From for MarginF32 { } /// `MarginF32 + MarginF32` -impl std::ops::Add for MarginF32 { +impl core::ops::Add for MarginF32 { type Output = Self; #[inline] @@ -126,7 +126,7 @@ impl std::ops::Add for MarginF32 { } /// `MarginF32 + f32` -impl std::ops::Add for MarginF32 { +impl core::ops::Add for MarginF32 { type Output = Self; #[inline] @@ -141,7 +141,7 @@ impl std::ops::Add for MarginF32 { } /// `Margind += f32` -impl std::ops::AddAssign for MarginF32 { +impl core::ops::AddAssign for MarginF32 { #[inline] fn add_assign(&mut self, v: f32) { self.left += v; @@ -152,7 +152,7 @@ impl std::ops::AddAssign for MarginF32 { } /// `MarginF32 * f32` -impl std::ops::Mul for MarginF32 { +impl core::ops::Mul for MarginF32 { type Output = Self; #[inline] @@ -167,7 +167,7 @@ impl std::ops::Mul for MarginF32 { } /// `MarginF32 *= f32` -impl std::ops::MulAssign for MarginF32 { +impl core::ops::MulAssign for MarginF32 { #[inline] fn mul_assign(&mut self, v: f32) { self.left *= v; @@ -178,7 +178,7 @@ impl std::ops::MulAssign for MarginF32 { } /// `MarginF32 / f32` -impl std::ops::Div for MarginF32 { +impl core::ops::Div for MarginF32 { type Output = Self; #[inline] @@ -193,7 +193,7 @@ impl std::ops::Div for MarginF32 { } /// `MarginF32 /= f32` -impl std::ops::DivAssign for MarginF32 { +impl core::ops::DivAssign for MarginF32 { #[inline] fn div_assign(&mut self, v: f32) { self.left /= v; @@ -204,7 +204,7 @@ impl std::ops::DivAssign for MarginF32 { } /// `MarginF32 - MarginF32` -impl std::ops::Sub for MarginF32 { +impl core::ops::Sub for MarginF32 { type Output = Self; #[inline] @@ -219,7 +219,7 @@ impl std::ops::Sub for MarginF32 { } /// `MarginF32 - f32` -impl std::ops::Sub for MarginF32 { +impl core::ops::Sub for MarginF32 { type Output = Self; #[inline] @@ -234,7 +234,7 @@ impl std::ops::Sub for MarginF32 { } /// `MarginF32 -= f32` -impl std::ops::SubAssign for MarginF32 { +impl core::ops::SubAssign for MarginF32 { #[inline] fn sub_assign(&mut self, v: f32) { self.left -= v; @@ -245,7 +245,7 @@ impl std::ops::SubAssign for MarginF32 { } /// `Rect + MarginF32` -impl std::ops::Add for Rect { +impl core::ops::Add for Rect { type Output = Self; #[inline] @@ -258,7 +258,7 @@ impl std::ops::Add for Rect { } /// `Rect += MarginF32` -impl std::ops::AddAssign for Rect { +impl core::ops::AddAssign for Rect { #[inline] fn add_assign(&mut self, margin: MarginF32) { *self = *self + margin; @@ -266,7 +266,7 @@ impl std::ops::AddAssign for Rect { } /// `Rect - MarginF32` -impl std::ops::Sub for Rect { +impl core::ops::Sub for Rect { type Output = Self; #[inline] @@ -279,7 +279,7 @@ impl std::ops::Sub for Rect { } /// `Rect -= MarginF32` -impl std::ops::SubAssign for Rect { +impl core::ops::SubAssign for Rect { #[inline] fn sub_assign(&mut self, margin: MarginF32) { *self = *self - margin; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index d48c98bc2..5ef978ff3 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -90,9 +90,9 @@ impl Mesh { /// Returns the amount of memory used by the vertices and indices. pub fn bytes_used(&self) -> usize { - std::mem::size_of::() - + self.vertices.len() * std::mem::size_of::() - + self.indices.len() * std::mem::size_of::() + core::mem::size_of::() + + self.vertices.len() * core::mem::size_of::() + + self.indices.len() * core::mem::size_of::() } /// Are all indices within the bounds of the contained vertices? diff --git a/crates/epaint/src/mutex.rs b/crates/epaint/src/mutex.rs index 272046823..09e64d30c 100644 --- a/crates/epaint/src/mutex.rs +++ b/crates/epaint/src/mutex.rs @@ -2,7 +2,7 @@ // ---------------------------------------------------------------------------- -const DEADLOCK_DURATION: std::time::Duration = std::time::Duration::from_secs(10); +const DEADLOCK_DURATION: core::time::Duration = core::time::Duration::from_secs(10); /// Provides interior mutability. /// @@ -128,7 +128,7 @@ mod tests { #![expect(clippy::disallowed_methods)] // Ok for tests use crate::mutex::Mutex; - use std::time::Duration; + use core::time::Duration; #[test] fn lock_two_different_mutexes_single_thread() { @@ -161,7 +161,7 @@ mod tests_rwlock { #![expect(clippy::disallowed_methods)] // Ok for tests use crate::mutex::RwLock; - use std::time::Duration; + use core::time::Duration; #[test] fn lock_two_different_rwlocks_single_thread() { diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index ace5ab90a..251b57b7a 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -29,7 +29,7 @@ pub struct Shadow { #[test] fn shadow_size() { assert_eq!( - std::mem::size_of::(), + core::mem::size_of::(), 8, "Shadow changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); diff --git a/crates/epaint/src/shapes/bezier_shape.rs b/crates/epaint/src/shapes/bezier_shape.rs index b8c78f273..caf1094a1 100644 --- a/crates/epaint/src/shapes/bezier_shape.rs +++ b/crates/epaint/src/shapes/bezier_shape.rs @@ -1,6 +1,6 @@ #![expect(clippy::many_single_char_names)] -use std::ops::Range; +use core::ops::Range; use crate::{Color32, PathShape, PathStroke, Shape}; use emath::{Pos2, Rect, RectTransform, fast_midpoint}; @@ -256,8 +256,8 @@ impl CubicBezierShape { let theta = (-q / (2.0 * r)).acos() / 3.0; let t1 = 2.0 * r.cbrt() * theta.cos() + h; - let t2 = 2.0 * r.cbrt() * (theta + 120.0 * std::f32::consts::PI / 180.0).cos() + h; - let t3 = 2.0 * r.cbrt() * (theta + 240.0 * std::f32::consts::PI / 180.0).cos() + h; + let t2 = 2.0 * r.cbrt() * (theta + 120.0 * core::f32::consts::PI / 180.0).cos() + h; + let t3 = 2.0 * r.cbrt() * (theta + 240.0 * core::f32::consts::PI / 180.0).cos() + h; if t1 > epsilon && t1 < 1.0 - epsilon { return Some(t1); diff --git a/crates/epaint/src/shapes/paint_callback.rs b/crates/epaint/src/shapes/paint_callback.rs index 00882f0f2..a45109280 100644 --- a/crates/epaint/src/shapes/paint_callback.rs +++ b/crates/epaint/src/shapes/paint_callback.rs @@ -1,4 +1,5 @@ -use std::{any::Any, sync::Arc}; +use core::any::Any; +use std::sync::Arc; use crate::*; @@ -32,7 +33,7 @@ fn test_viewport_rounding() { let left = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_max_x(x); let right = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)).with_min_x(x); - for pixels_per_point in [0.618, 1.0, std::f32::consts::PI] { + for pixels_per_point in [0.618, 1.0, core::f32::consts::PI] { let left = ViewportInPixels::from_points(&left, pixels_per_point, [100, 100]); let right = ViewportInPixels::from_points(&right, pixels_per_point, [100, 100]); assert_eq!(left.left_px + left.width_px, right.left_px); @@ -81,15 +82,15 @@ pub struct PaintCallback { pub callback: Arc, } -impl std::fmt::Debug for PaintCallback { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Debug for PaintCallback { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("CustomShape") .field("rect", &self.rect) .finish_non_exhaustive() } } -impl std::cmp::PartialEq for PaintCallback { +impl core::cmp::PartialEq for PaintCallback { fn eq(&self, other: &Self) -> bool { self.rect.eq(&other.rect) && Arc::ptr_eq(&self.callback, &other.callback) } diff --git a/crates/epaint/src/shapes/rect_shape.rs b/crates/epaint/src/shapes/rect_shape.rs index e0c528377..159d56af5 100644 --- a/crates/epaint/src/shapes/rect_shape.rs +++ b/crates/epaint/src/shapes/rect_shape.rs @@ -62,12 +62,12 @@ pub struct RectShape { #[test] fn rect_shape_size() { assert_eq!( - std::mem::size_of::(), + core::mem::size_of::(), 56, "RectShape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( - std::mem::size_of::() <= 64, + core::mem::size_of::() <= 64, "RectShape is getting way too big!" ); } diff --git a/crates/epaint/src/shapes/shape.rs b/crates/epaint/src/shapes/shape.rs index ff15708f1..55be52ba5 100644 --- a/crates/epaint/src/shapes/shape.rs +++ b/crates/epaint/src/shapes/shape.rs @@ -73,12 +73,12 @@ pub enum Shape { #[test] fn shape_size() { assert_eq!( - std::mem::size_of::(), + core::mem::size_of::(), 64, "Shape changed size! If it shrank - good! Update this test. If it grew - bad! Try to find a way to avoid it." ); assert!( - std::mem::size_of::() <= 64, + core::mem::size_of::() <= 64, "Shape is getting way too big!" ); } diff --git a/crates/epaint/src/shapes/text_shape.rs b/crates/epaint/src/shapes/text_shape.rs index 3e177db07..8083dcf25 100644 --- a/crates/epaint/src/shapes/text_shape.rs +++ b/crates/epaint/src/shapes/text_shape.rs @@ -201,7 +201,7 @@ mod tests { // 90 degree rotation if let Shape::Text(ts) = &mut t { - ts.angle = std::f32::consts::PI / 2.0; + ts.angle = core::f32::consts::PI / 2.0; } let size_rot = t.visual_bounding_rect().size(); diff --git a/crates/epaint/src/stats.rs b/crates/epaint/src/stats.rs index de8f275cf..8c191dcb1 100644 --- a/crates/epaint/src/stats.rs +++ b/crates/epaint/src/stats.rs @@ -26,7 +26,7 @@ impl From<&[T]> for AllocInfo { } } -impl std::ops::Add for AllocInfo { +impl core::ops::Add for AllocInfo { type Output = Self; fn add(self, rhs: Self) -> Self { @@ -47,13 +47,13 @@ impl std::ops::Add for AllocInfo { } } -impl std::ops::AddAssign for AllocInfo { +impl core::ops::AddAssign for AllocInfo { fn add_assign(&mut self, rhs: Self) { *self = *self + rhs; } } -impl std::iter::Sum for AllocInfo { +impl core::iter::Sum for AllocInfo { fn sum(iter: I) -> Self where I: Iterator, @@ -95,13 +95,13 @@ impl AllocInfo { } pub fn from_slice(slice: &[T]) -> Self { - use std::mem::size_of; + use core::mem::size_of; let element_size = size_of::(); Self { element_size: ElementSize::Homogeneous(element_size), num_allocs: 1, num_elements: slice.len(), - num_bytes: std::mem::size_of_val(slice), + num_bytes: core::mem::size_of_val(slice), } } diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 1b072e4bf..1a5b2f0d1 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -1,4 +1,5 @@ -use std::{fmt::Debug, sync::Arc}; +use core::fmt::Debug; +use std::sync::Arc; use emath::GuiRounding as _; @@ -86,9 +87,9 @@ where } } -impl std::hash::Hash for Stroke { +impl core::hash::Hash for Stroke { #[inline(always)] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { let Self { width, color } = *self; emath::OrderedFloat(width).hash(state); color.hash(state); diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 716fe5424..7b207abeb 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1579,7 +1579,7 @@ impl Tessellator { let eased = 2.0 * (percent - percent.powf(2.0)) * ratio + percent.powf(2.0); // Scale the ease to the quarter - let t = eased * std::f32::consts::FRAC_PI_2; + let t = eased * core::f32::consts::FRAC_PI_2; Vec2::new(radius.x * f32::cos(t), radius.y * f32::sin(t)) }) .collect(); diff --git a/crates/epaint/src/text/cursor.rs b/crates/epaint/src/text/cursor.rs index ac2e4216d..1143fc59f 100644 --- a/crates/epaint/src/text/cursor.rs +++ b/crates/epaint/src/text/cursor.rs @@ -37,7 +37,7 @@ impl PartialEq for CCursor { } } -impl std::ops::Add for CCursor { +impl core::ops::Add for CCursor { type Output = Self; fn add(self, rhs: usize) -> Self::Output { @@ -48,7 +48,7 @@ impl std::ops::Add for CCursor { } } -impl std::ops::Add for CCursor { +impl core::ops::Add for CCursor { type Output = Self; fn add(self, rhs: CharIndex) -> Self::Output { @@ -59,7 +59,7 @@ impl std::ops::Add for CCursor { } } -impl std::ops::Sub for CCursor { +impl core::ops::Sub for CCursor { type Output = Self; fn sub(self, rhs: usize) -> Self::Output { @@ -70,7 +70,7 @@ impl std::ops::Sub for CCursor { } } -impl std::ops::Sub for CCursor { +impl core::ops::Sub for CCursor { type Output = Self; fn sub(self, rhs: CharIndex) -> Self::Output { @@ -81,13 +81,13 @@ impl std::ops::Sub for CCursor { } } -impl std::ops::AddAssign for CCursor { +impl core::ops::AddAssign for CCursor { fn add_assign(&mut self, rhs: usize) { self.index = self.index.saturating_add(rhs); } } -impl std::ops::SubAssign for CCursor { +impl core::ops::SubAssign for CCursor { fn sub_assign(&mut self, rhs: usize) { self.index = self.index.saturating_sub(rhs); } diff --git a/crates/epaint/src/text/font.rs b/crates/epaint/src/text/font.rs index 12885596e..287230db3 100644 --- a/crates/epaint/src/text/font.rs +++ b/crates/epaint/src/text/font.rs @@ -382,7 +382,7 @@ impl FontFace { font_data: Blob, index: u32, tweak: FontTweak, - ) -> Result> { + ) -> Result> { let font = FontCell::try_new(font_data, |font_data| { let skrifa_font = skrifa::FontRef::from_index(AsRef::<[u8]>::as_ref(font_data.as_ref()), index)?; @@ -414,7 +414,7 @@ impl FontFace { }) .flatten(); - Ok::, Box>(DependentFontData { + Ok::, Box>(DependentFontData { skrifa: skrifa_font, charmap, outline_glyphs: glyphs, @@ -574,7 +574,7 @@ impl FontFace { let axes = font_data.skrifa.axes(); // Override the default coordinates with ones specified via FontTweak, then the ones specified directly via the // argument (probably from TextFormat). - let settings = std::iter::chain(self.tweak.coords.as_ref(), coords.as_ref()); + let settings = core::iter::chain(self.tweak.coords.as_ref(), coords.as_ref()); let location = axes.location(settings); let location_hash = LocationHash::new(&location); diff --git a/crates/epaint/src/text/fonts.rs b/crates/epaint/src/text/fonts.rs index 62b1b83a0..28ab8bbf5 100644 --- a/crates/epaint/src/text/fonts.rs +++ b/crates/epaint/src/text/fonts.rs @@ -1,11 +1,5 @@ -use std::{ - borrow::Cow, - collections::BTreeMap, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, -}; +use core::sync::atomic::{AtomicUsize, Ordering}; +use std::{borrow::Cow, collections::BTreeMap, sync::Arc}; use crate::{ TextureAtlas, @@ -60,9 +54,9 @@ impl FontId { } } -impl std::hash::Hash for FontId { +impl core::hash::Hash for FontId { #[inline(always)] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { let Self { size, family } = self; emath::OrderedFloat(*size).hash(state); family.hash(state); @@ -100,8 +94,8 @@ pub enum FontFamily { Name(Arc), } -impl std::fmt::Display for FontFamily { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for FontFamily { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Self::Monospace => "Monospace".fmt(f), Self::Proportional => "Proportional".fmt(f), diff --git a/crates/epaint/src/text/index.rs b/crates/epaint/src/text/index.rs index 3fcd9a4ce..0f1450740 100644 --- a/crates/epaint/src/text/index.rs +++ b/crates/epaint/src/text/index.rs @@ -4,7 +4,7 @@ //! (Unicode scalar) offset. Mixing the two is a common source of bugs, //! so we use distinct types to keep them apart. -use std::ops::Range; +use core::ops::Range; /// A byte offset into a UTF-8 string. /// @@ -63,7 +63,7 @@ macro_rules! impl_text_index { } } - impl std::ops::Add for $Type { + impl core::ops::Add for $Type { type Output = Self; #[inline] @@ -73,7 +73,7 @@ macro_rules! impl_text_index { } /// Compose offsets, e.g. a base position plus a relative one. - impl std::ops::Add<$Type> for $Type { + impl core::ops::Add<$Type> for $Type { type Output = Self; #[inline] @@ -82,7 +82,7 @@ macro_rules! impl_text_index { } } - impl std::ops::Sub for $Type { + impl core::ops::Sub for $Type { type Output = Self; #[inline] @@ -91,7 +91,7 @@ macro_rules! impl_text_index { } } - impl std::ops::Sub<$Type> for $Type { + impl core::ops::Sub<$Type> for $Type { type Output = Self; #[inline] @@ -100,30 +100,30 @@ macro_rules! impl_text_index { } } - impl std::ops::AddAssign for $Type { + impl core::ops::AddAssign for $Type { #[inline] fn add_assign(&mut self, rhs: usize) { self.0 += rhs; } } - impl std::ops::AddAssign<$Type> for $Type { + impl core::ops::AddAssign<$Type> for $Type { #[inline] fn add_assign(&mut self, rhs: Self) { self.0 += rhs.0; } } - impl std::ops::SubAssign for $Type { + impl core::ops::SubAssign for $Type { #[inline] fn sub_assign(&mut self, rhs: usize) { self.0 -= rhs; } } - impl std::fmt::Display for $Type { + impl core::fmt::Display for $Type { #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { self.0.fmt(f) } } diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 442f1f86c..06ddc3b1f 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -1,7 +1,7 @@ #![expect(clippy::unwrap_used)] // TODO(emilk): remove unwraps +use core::{iter, ops::Range}; use std::sync::Arc; -use std::{iter, ops::Range}; use emath::{Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2, pos2, vec2}; @@ -523,7 +523,7 @@ fn layout_section( /// 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>), + Split(core::str::Split<'a, char>), Whole(iter::Once<&'a str>), } @@ -571,7 +571,7 @@ fn calculate_intrinsic_size( .glyphs .iter() .map(|g| g.line_height) - .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal)) .unwrap_or(paragraph.empty_paragraph_height); if idx == 0 { height = f32::max(height, job.first_row_min_height); @@ -1437,7 +1437,7 @@ fn shape_text( #[cfg(test)] mod tests { - use std::iter; + use core::iter; use super::{super::*, *}; use crate::text::cursor::CCursor; diff --git a/crates/epaint/src/text/text_layout_types.rs b/crates/epaint/src/text/text_layout_types.rs index 97af735b4..e51c0e1cd 100644 --- a/crates/epaint/src/text/text_layout_types.rs +++ b/crates/epaint/src/text/text_layout_types.rs @@ -1,5 +1,5 @@ +use core::{ops::Range, str::FromStr as _}; use std::sync::Arc; -use std::{ops::Range, str::FromStr as _}; use super::{ cursor::{CCursor, LayoutCursor}, @@ -271,7 +271,7 @@ impl LayoutJob { let Range { start, end } = section.byte_range; assert!(start <= end, "LayoutSection has a reversed byte_range"); } - for (prev, next) in std::iter::zip(&self.sections, self.sections.iter().skip(1)) { + for (prev, next) in core::iter::zip(&self.sections, self.sections.iter().skip(1)) { assert_eq!( prev.byte_range.end, next.byte_range.start, "LayoutSections must be ordered with no gaps and no overlaps" @@ -304,9 +304,9 @@ impl LayoutJob { } } -impl std::hash::Hash for LayoutJob { +impl core::hash::Hash for LayoutJob { #[inline] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { let Self { text, sections, @@ -355,9 +355,9 @@ pub struct LayoutSection { pub format: TextFormat, } -impl std::hash::Hash for LayoutSection { +impl core::hash::Hash for LayoutSection { #[inline] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { let Self { leading_space, byte_range, @@ -462,8 +462,8 @@ impl AsMut<[(font_types::Tag, f32)]> for VariationCoords { } } -impl std::hash::Hash for VariationCoords { - fn hash(&self, state: &mut H) { +impl core::hash::Hash for VariationCoords { + fn hash(&self, state: &mut H) { self.0.len().hash(state); for (tag, coord) in &self.0 { tag.hash(state); @@ -541,9 +541,9 @@ impl Default for TextFormat { } } -impl std::hash::Hash for TextFormat { +impl core::hash::Hash for TextFormat { #[inline] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { let Self { font_id, extra_letter_spacing, @@ -651,9 +651,9 @@ pub struct TextWrapping { pub overflow_character: Option, } -impl std::hash::Hash for TextWrapping { +impl core::hash::Hash for TextWrapping { #[inline] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { let Self { max_width, max_rows, @@ -817,7 +817,7 @@ impl PlacedRow { } } -impl std::ops::Deref for PlacedRow { +impl core::ops::Deref for PlacedRow { type Target = Row; fn deref(&self) -> &Self::Target { @@ -1126,14 +1126,14 @@ impl AsRef for Galley { } } -impl std::borrow::Borrow for Galley { +impl core::borrow::Borrow for Galley { #[inline] fn borrow(&self) -> &str { self.text() } } -impl std::ops::Deref for Galley { +impl core::ops::Deref for Galley { type Target = str; #[inline] fn deref(&self) -> &str { diff --git a/crates/epaint/src/texture_atlas.rs b/crates/epaint/src/texture_atlas.rs index 4f8548817..0bb235b33 100644 --- a/crates/epaint/src/texture_atlas.rs +++ b/crates/epaint/src/texture_atlas.rs @@ -202,7 +202,7 @@ impl TextureAtlas { pub fn take_delta(&mut self) -> Option { let texture_options = Self::texture_options(); - let dirty = std::mem::replace(&mut self.dirty, Rectu::NOTHING); + let dirty = core::mem::replace(&mut self.dirty, Rectu::NOTHING); if dirty == Rectu::NOTHING { None } else if dirty == Rectu::EVERYTHING { diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index bbbf490b5..b8b4310dc 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -47,9 +47,9 @@ impl PartialEq for TextureHandle { impl Eq for TextureHandle {} -impl std::hash::Hash for TextureHandle { +impl core::hash::Hash for TextureHandle { #[inline] - fn hash(&self, state: &mut H) { + fn hash(&self, state: &mut H) { self.id.hash(state); } } diff --git a/crates/epaint/src/textures.rs b/crates/epaint/src/textures.rs index 1c4104a6e..594cada75 100644 --- a/crates/epaint/src/textures.rs +++ b/crates/epaint/src/textures.rs @@ -1,7 +1,7 @@ use crate::{ImageData, ImageDelta, TextureId}; use ahash::{HashMap, HashSet}; +use core::mem; use smallvec::{SmallVec, smallvec}; -use std::mem; // ---------------------------------------------------------------------------- @@ -100,7 +100,7 @@ impl TextureManager { /// /// These should be applied to the painting subsystem each frame. pub fn take_delta(&mut self) -> TexturesDelta { - std::mem::take(&mut self.delta) + core::mem::take(&mut self.delta) } /// Get meta-data about a specific texture. @@ -343,9 +343,9 @@ impl Drop for TexturesDelta { } } -impl std::fmt::Debug for TexturesDelta { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use std::fmt::Write as _; +impl core::fmt::Debug for TexturesDelta { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use core::fmt::Write as _; let mut debug_struct = f.debug_struct("TexturesDelta"); if !self.set.is_empty() { diff --git a/crates/epaint/src/util/mod.rs b/crates/epaint/src/util/mod.rs index 471576630..ad324adcf 100644 --- a/crates/epaint/src/util/mod.rs +++ b/crates/epaint/src/util/mod.rs @@ -1,12 +1,12 @@ /// Hash the given value with a predictable hasher. #[inline] -pub fn hash(value: impl std::hash::Hash) -> u64 { +pub fn hash(value: impl core::hash::Hash) -> u64 { ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(value) } /// Hash the given value with the given hasher. #[inline] -pub fn hash_with(value: impl std::hash::Hash, mut hasher: impl std::hash::Hasher) -> u64 { +pub fn hash_with(value: impl core::hash::Hash, mut hasher: impl core::hash::Hasher) -> u64 { value.hash(&mut hasher); hasher.finish() } diff --git a/examples/external_eventloop/src/main.rs b/examples/external_eventloop/src/main.rs index 227fd3f33..a84dcd551 100644 --- a/examples/external_eventloop/src/main.rs +++ b/examples/external_eventloop/src/main.rs @@ -1,8 +1,9 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![expect(rustdoc::missing_crate_level_docs, clippy::unwrap_used)] // it's an example +use core::cell::Cell; use eframe::{UserEvent, egui}; -use std::{cell::Cell, rc::Rc}; +use std::rc::Rc; use winit::event_loop::{ControlFlow, EventLoop}; fn main() -> eframe::Result { diff --git a/examples/external_eventloop_async/src/app.rs b/examples/external_eventloop_async/src/app.rs index d0e62e3cc..ea9c0b413 100644 --- a/examples/external_eventloop_async/src/app.rs +++ b/examples/external_eventloop_async/src/app.rs @@ -1,6 +1,7 @@ #![expect(clippy::unwrap_used)] // It's an example -use std::{cell::Cell, io, os::fd::AsRawFd as _, rc::Rc, time::Duration}; +use core::{cell::Cell, time::Duration}; +use std::{io, os::fd::AsRawFd as _, rc::Rc}; use tokio::task::LocalSet; use winit::event_loop::{ControlFlow, EventLoop}; diff --git a/examples/file_dialog/src/main.rs b/examples/file_dialog/src/main.rs index d42da9b82..3034e2376 100644 --- a/examples/file_dialog/src/main.rs +++ b/examples/file_dialog/src/main.rs @@ -83,8 +83,8 @@ impl eframe::App for MyApp { /// Preview hovering files: fn preview_files_being_dropped(ctx: &egui::Context) { + use core::fmt::Write as _; use egui::{Align2, Color32, Id, LayerId, Order, TextStyle}; - use std::fmt::Write as _; if !ctx.input(|i| i.raw.hovered_files.is_empty()) { let text = ctx.input(|i| { diff --git a/examples/hello_world_par/src/main.rs b/examples/hello_world_par/src/main.rs index b064c65bd..ee62b1668 100644 --- a/examples/hello_world_par/src/main.rs +++ b/examples/hello_world_par/src/main.rs @@ -106,10 +106,10 @@ impl MyApp { } } -impl std::ops::Drop for MyApp { +impl core::ops::Drop for MyApp { fn drop(&mut self) { for (handle, show_tx) in self.threads.drain(..) { - std::mem::drop(show_tx); + core::mem::drop(show_tx); handle.join().unwrap(); } } diff --git a/examples/multiple_viewports/src/main.rs b/examples/multiple_viewports/src/main.rs index b75d3c016..59383aa93 100644 --- a/examples/multiple_viewports/src/main.rs +++ b/examples/multiple_viewports/src/main.rs @@ -1,10 +1,8 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![expect(rustdoc::missing_crate_level_docs)] // it's an example -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; +use core::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use eframe::egui; diff --git a/examples/puffin_profiler/src/main.rs b/examples/puffin_profiler/src/main.rs index 89118e8c7..03e4eb790 100644 --- a/examples/puffin_profiler/src/main.rs +++ b/examples/puffin_profiler/src/main.rs @@ -1,10 +1,8 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![expect(rustdoc::missing_crate_level_docs)] // it's an example -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; +use core::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use eframe::egui; @@ -88,7 +86,7 @@ impl eframe::App for MyApp { .clicked() { puffin::profile_scope!("long_sleep"); - std::thread::sleep(std::time::Duration::from_millis(50)); + std::thread::sleep(core::time::Duration::from_millis(50)); } ui.checkbox( @@ -172,7 +170,7 @@ fn start_puffin_server() { // We can store the server if we want, but in this case we just want // it to keep running. Dropping it closes the server, so let's not drop it! #[expect(clippy::mem_forget)] - std::mem::forget(puffin_server); + core::mem::forget(puffin_server); } Err(err) => { log::error!("Failed to start puffin server: {err}"); diff --git a/examples/serial_windows/src/main.rs b/examples/serial_windows/src/main.rs index 8ef07dc9c..0ece04d34 100644 --- a/examples/serial_windows/src/main.rs +++ b/examples/serial_windows/src/main.rs @@ -19,7 +19,7 @@ fn main() -> eframe::Result { Box::new(|_cc| Ok(Box::new(MyApp { has_next: true }))), )?; - std::thread::sleep(std::time::Duration::from_secs(2)); + std::thread::sleep(core::time::Duration::from_secs(2)); log::info!("Starting second window…"); eframe::run_native( @@ -28,7 +28,7 @@ fn main() -> eframe::Result { Box::new(|_cc| Ok(Box::new(MyApp { has_next: true }))), )?; - std::thread::sleep(std::time::Duration::from_secs(2)); + std::thread::sleep(core::time::Duration::from_secs(2)); log::info!("Starting third window…"); eframe::run_native( diff --git a/examples/user_attention/src/main.rs b/examples/user_attention/src/main.rs index 46d8fbb99..6b16ddd07 100644 --- a/examples/user_attention/src/main.rs +++ b/examples/user_attention/src/main.rs @@ -4,7 +4,8 @@ use eframe::{CreationContext, NativeOptions, egui}; use egui::{Button, CentralPanel, UserAttentionType}; -use std::time::{Duration, SystemTime}; +use core::time::Duration; +use std::time::SystemTime; fn main() -> eframe::Result { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). diff --git a/tests/egui_tests/tests/regression_tests.rs b/tests/egui_tests/tests/regression_tests.rs index 1a65254a7..b72ac6b61 100644 --- a/tests/egui_tests/tests/regression_tests.rs +++ b/tests/egui_tests/tests/regression_tests.rs @@ -316,7 +316,7 @@ fn warn_if_rect_changes_id() { #[test] #[cfg(debug_assertions)] fn warn_if_rect_changes_id_false_positive_parent_shift() { - use std::cell::Cell; + use core::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)); diff --git a/tests/egui_tests/tests/test_atoms.rs b/tests/egui_tests/tests/test_atoms.rs index 48babfbd7..6368de3a8 100644 --- a/tests/egui_tests/tests/test_atoms.rs +++ b/tests/egui_tests/tests/test_atoms.rs @@ -220,8 +220,8 @@ fn test_atom_selectable_senses_click_and_drag() { /// See . #[test] fn test_atom_selectable_text_can_be_copied() { + use core::cell::Cell; use egui::{AtomLayout, Event, Modifiers, OutputCommand, PointerButton, Pos2, Rect}; - use std::cell::Cell; fn copied_text(selectable: bool) -> Option { let rect_cell = Cell::new(Rect::NOTHING); diff --git a/tests/egui_tests/tests/test_panel_drag.rs b/tests/egui_tests/tests/test_panel_drag.rs index 0909753cc..96c7daba1 100644 --- a/tests/egui_tests/tests/test_panel_drag.rs +++ b/tests/egui_tests/tests/test_panel_drag.rs @@ -381,7 +381,7 @@ fn switched_bottom_panel_harness(start_expanded: bool) -> Harness<'static, Switc } /// Assert that the panel edge crossed `gap` gradually, rather than in one frame. -fn assert_crossed_gradually(tops: &[f32], gap: std::ops::Range) { +fn assert_crossed_gradually(tops: &[f32], gap: core::ops::Range) { let frames_in_gap = tops.iter().filter(|top| gap.contains(top)).count(); assert!( 3 <= frames_in_gap, diff --git a/tests/test_background_logic/src/main.rs b/tests/test_background_logic/src/main.rs index b1c559916..0709bc2ed 100644 --- a/tests/test_background_logic/src/main.rs +++ b/tests/test_background_logic/src/main.rs @@ -2,7 +2,7 @@ #![expect(rustdoc::missing_crate_level_docs)] #![allow(clippy::print_stderr)] -use std::time::Duration; +use core::time::Duration; use eframe::egui::{self, ViewportInfo}; @@ -56,7 +56,7 @@ fn viewport_info(ctx: &egui::Context) -> String { ]; for (name, value) in flags { if let Some(value) = value { - use std::fmt::Write as _; + use core::fmt::Write as _; write!(s, " {name}={value}").ok(); } } diff --git a/tests/test_inline_glow_paint/src/main.rs b/tests/test_inline_glow_paint/src/main.rs index d23288706..517d1706d 100644 --- a/tests/test_inline_glow_paint/src/main.rs +++ b/tests/test_inline_glow_paint/src/main.rs @@ -11,7 +11,7 @@ use eframe::egui; use eframe::glow; -fn main() -> Result<(), Box> { +fn main() -> Result<(), Box> { env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). let options = eframe::NativeOptions { renderer: eframe::Renderer::Glow, diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 81471d622..fb2bf2570 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -6,7 +6,7 @@ mod deny; pub(crate) mod utils; -type DynError = Box; +type DynError = Box; fn main() { if let Err(e) = try_main() { From 65e827e23a26f4e63dfc3502d129453ea200667e Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 7 Aug 2026 14:26:26 +0200 Subject: [PATCH 35/49] Fix `Sense::drag` detecting drags when clicking widget above it (#8396) Co-authored-by: Lucas Meurer Co-authored-by: Claude Opus 5 (1M context) --- crates/egui/src/interaction.rs | 19 +++++--- tests/egui_tests/tests/test_click_or_drag.rs | 47 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index e625fc298..cac08f595 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -198,17 +198,22 @@ pub(crate) fn interact( // When the mouse first is pressed, it could be either, // so we postpone the decision until we know. // - // …unless a click is no longer possible at all: a click has to be - // released on the widget, and `hits.click` tells us whether the - // pointer is still somewhere a release would land on this widget. - // Note that this is not the same as being inside `interact_rect`: - // the hit-test also picks up widgets within `interact_radius`, and - // lets a widget on top take the hit. + // …unless the pointer has left the widget: a click has to be + // released on the widget, so once the pointer is outside there is + // nothing left to wait for. // // Deciding here means a thin drag handle (narrower than // `max_click_dist`) doesn't spend the decision window as neither // hovered nor dragged, which would make its highlight blink out. - let could_still_be_clicked = hits.click.is_some_and(|hit| hit.id == widget.id); + // The hit-test picks up widgets within `interact_radius`, so + // `hits.click` can name such a handle even when the pointer is a + // few points outside it. + // + // A widget on top might "steal" the click hit, but then the pointer is still inside + // us, and pressing that button must not start a drag. So we check both. + let pointer_is_inside = hits.contains_pointer.iter().any(|w| w.id == widget.id); + let could_still_be_clicked = + pointer_is_inside || hits.click.is_some_and(|hit| hit.id == widget.id); input.pointer.is_decidedly_dragging() || !could_still_be_clicked } else { // This widget is just sensitive to drags, so we can mark it as dragged right away: diff --git a/tests/egui_tests/tests/test_click_or_drag.rs b/tests/egui_tests/tests/test_click_or_drag.rs index 58be44247..c3cc010ec 100644 --- a/tests/egui_tests/tests/test_click_or_drag.rs +++ b/tests/egui_tests/tests/test_click_or_drag.rs @@ -185,3 +185,50 @@ fn click_inside_a_widget_still_clicks() { "press and release without moving should be a click" ); } + +/// A button inside a draggable row takes the click hit, because it is on top. +/// The pointer is still inside the row though, so the press must stay undecided — +/// otherwise the row starts dragging the moment the user touches the button. +#[test] +fn press_on_a_button_inside_a_draggable_row_stays_undecided() { + let button_id = Id::new("button"); + let button_size = Vec2::new(50.0, 20.0); + + let mut harness = Harness::builder() + .with_step_dt(1.0 / 60.0) + .with_size(Vec2::new(300.0, 200.0)) + .build_ui(move |ui| { + let row_rect = ui.max_rect(); + ui.interact(row_rect, widget_id(), Sense::click_and_drag()); + + // Allocated after the row, so it ends up _on top_ of it. + let button_rect = Rect::from_min_size(row_rect.min, button_size); + ui.interact(button_rect, button_id, Sense::click()); + }); + harness.step(); + + let grab = Rect::from_min_size(widget_rect(&harness).min, button_size).center(); + press_at(&mut harness, grab); + + let (_hovered, dragged) = widget_state(&harness); + assert!( + !dragged, + "pressing a button inside the row must not start dragging the row" + ); + + harness.event(egui::Event::PointerButton { + pos: grab, + button: egui::PointerButton::Primary, + pressed: false, + modifiers: egui::Modifiers::NONE, + }); + harness.step(); + + assert!( + harness + .ctx + .read_response(button_id) + .is_some_and(|r| r.clicked()), + "the button should have been clicked" + ); +} From 46ba6405bf5c550041f14b8817f9c16e58a446e7 Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Fri, 7 Aug 2026 15:08:38 +0200 Subject: [PATCH 36/49] Release 0.36.1 - Fix `Sense::drag` bug (#8397) --- CHANGELOG.md | 4 +++ Cargo.lock | 34 ++++++++++++------------ Cargo.toml | 28 +++++++++---------- crates/ecolor/CHANGELOG.md | 4 +++ crates/eframe/CHANGELOG.md | 4 +++ 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_inspection/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 +++ 14 files changed, 79 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7194ccc5a..397279699 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.36.1 - 2026-08-07 +* Fix `Sense::drag` detecting drags when clicking widget above it [#8396](https://github.com/emilk/egui/pull/8396) by [@lucasmerlin](https://github.com/lucasmerlin) + + ## 0.36.0 - 2026-08-05 ### Highlights ✨ diff --git a/Cargo.lock b/Cargo.lock index 97791c6f9..e472f7c3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1243,7 +1243,7 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" -version = "0.36.0" +version = "0.36.1" dependencies = [ "bytemuck", "cint", @@ -1255,7 +1255,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.36.0" +version = "0.36.1" dependencies = [ "ahash", "bytemuck", @@ -1293,7 +1293,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.36.0" +version = "0.36.1" dependencies = [ "accesskit", "ahash", @@ -1315,7 +1315,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.36.0" +version = "0.36.1" dependencies = [ "ahash", "bytemuck", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.36.0" +version = "0.36.1" dependencies = [ "accesskit_winit", "arboard", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.36.0" +version = "0.36.1" dependencies = [ "accesskit", "accesskit_consumer", @@ -1385,7 +1385,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.36.0" +version = "0.36.1" dependencies = [ "criterion", "document-features", @@ -1403,7 +1403,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.36.0" +version = "0.36.1" dependencies = [ "ahash", "document-features", @@ -1423,7 +1423,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.36.0" +version = "0.36.1" dependencies = [ "bytemuck", "document-features", @@ -1439,7 +1439,7 @@ dependencies = [ [[package]] name = "egui_inspection" -version = "0.36.0" +version = "0.36.1" dependencies = [ "document-features", "egui", @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.36.0" +version = "0.36.1" dependencies = [ "dify", "document-features", @@ -1473,7 +1473,7 @@ dependencies = [ [[package]] name = "egui_tests" -version = "0.36.0" +version = "0.36.1" dependencies = [ "egui", "egui_extras", @@ -1503,7 +1503,7 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "emath" -version = "0.36.0" +version = "0.36.1" dependencies = [ "bytemuck", "document-features", @@ -1601,7 +1601,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.36.0" +version = "0.36.1" dependencies = [ "ahash", "bytemuck", @@ -1630,7 +1630,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.36.0" +version = "0.36.1" [[package]] name = "equivalent" @@ -3594,7 +3594,7 @@ dependencies = [ [[package]] name = "popups" -version = "0.36.0" +version = "0.36.1" dependencies = [ "eframe", "env_logger", @@ -5903,7 +5903,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.36.0" +version = "0.36.1" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 124719eff..5f087c309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ members = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" -version = "0.36.0" +version = "0.36.1" [profile.release] @@ -56,19 +56,19 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.36.0", path = "crates/emath", default-features = false } -ecolor = { version = "0.36.0", path = "crates/ecolor", default-features = false } -epaint = { version = "0.36.0", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.36.0", path = "crates/epaint_default_fonts" } -egui = { version = "0.36.0", path = "crates/egui", default-features = false } -egui-winit = { version = "0.36.0", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.36.0", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.36.0", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.36.0", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.36.0", path = "crates/egui_glow", default-features = false } -egui_inspection = { version = "0.36.0", path = "crates/egui_inspection", default-features = false } -egui_kittest = { version = "0.36.0", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.36.0", path = "crates/eframe", default-features = false } +emath = { version = "0.36.1", path = "crates/emath", default-features = false } +ecolor = { version = "0.36.1", path = "crates/ecolor", default-features = false } +epaint = { version = "0.36.1", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.36.1", path = "crates/epaint_default_fonts" } +egui = { version = "0.36.1", path = "crates/egui", default-features = false } +egui-winit = { version = "0.36.1", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.36.1", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.36.1", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.36.1", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.36.1", path = "crates/egui_glow", default-features = false } +egui_inspection = { version = "0.36.1", path = "crates/egui_inspection", default-features = false } +egui_kittest = { version = "0.36.1", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.36.1", path = "crates/eframe", default-features = false } accesskit = "0.24.1" accesskit_consumer = "0.35.0" # Can't update to 0.36+: kittest 0.4 pins accesskit_consumer 0.35, so bumping splits it into two versions diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 349a6e52f..31300e172 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 0e904260f..eb3dcb350 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 ### 🔧 Changed * Improve robustness of text input handling for `eframe/web` [#8045](https://github.com/emilk/egui/pull/8045) by [@umajho](https://github.com/umajho) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index e45767f52..b2d1063b3 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 * Upgrade wgpu to v30 [#8289](https://github.com/emilk/egui/pull/8289) by [@akx](https://github.com/akx) * Fix: ensure mapped range is dropped before unmapping buffer in capture [#8337](https://github.com/emilk/egui/pull/8337) by [@MagicCrazyMan](https://github.com/MagicCrazyMan) diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index f9cc12307..f7ed2fa07 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 Nothing new diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index a42cb4d62..db7464d9b 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 Nothing new diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 78197938a..959f16b57 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 * Add `egui_inspection::Request::Settle` [#8344](https://github.com/emilk/egui/pull/8344) by [@lucasmerlin](https://github.com/lucasmerlin) diff --git a/crates/egui_kittest/CHANGELOG.md b/crates/egui_kittest/CHANGELOG.md index a588a42a5..fe999ffc9 100644 --- a/crates/egui_kittest/CHANGELOG.md +++ b/crates/egui_kittest/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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 * Handle `ViewportCommand::InnerSize` in `egui_kittest` [#8350](https://github.com/emilk/egui/pull/8350) by [@lucasmerlin](https://github.com/lucasmerlin) * Report failing pixels by threshold when a kittest snapshot fails [#8360](https://github.com/emilk/egui/pull/8360) by [@emilk](https://github.com/emilk) diff --git a/crates/emath/CHANGELOG.md b/crates/emath/CHANGELOG.md index d0cdae830..2361112d7 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 Nothing new diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index 15c27e4b4..d0db754f3 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 * Add `LayoutJob::clear` [#8376](https://github.com/emilk/egui/pull/8376) by [@emilk](https://github.com/emilk) * Add `extra_text_line_spacing` to control vertical spacing between text lines [#8040](https://github.com/emilk/egui/pull/8040) by [@rustbasic](https://github.com/rustbasic) diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 8ee27c5c0..0d6af564f 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.36.1 - 2026-08-07 +Nothing new + + ## 0.36.0 - 2026-08-05 Nothing new From b42d2ef4f0802cb5d879f9cae571fc39e5de3a57 Mon Sep 17 00:00:00 2001 From: 42Pupusas <133644935+42Pupusas@users.noreply.github.com> Date: Tue, 11 Aug 2026 05:52:12 -0600 Subject: [PATCH 37/49] Don't busy-loop a CPU core while waiting for a redraw (#8398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #8326 `check_redraw_requests` switched the event loop to `ControlFlow::Poll` every time it called `request_redraw`, and only ever restored a sleeping control flow when a *timed* repaint was still pending. Once the last scheduled repaint had been consumed the `Poll` was never undone, so the loop kept spinning. This is most visible on Wayland, where `RedrawRequested` is only delivered after the compositor sends a frame callback: between the request and the callback eframe burns 100% of a CPU core, so simply moving the mouse over a reactive app pegs a core. `request_redraw` already wakes the event loop on its own, so the `Poll` is not needed. Drop it, and always set an explicit sleeping control flow at the end of `check_redraw_requests`: `WaitUntil` for the earliest scheduled repaint, `Wait` when nothing is scheduled. **Measured effect of this patch** Two byte-identical eframe apps (a 400-row scrolling page, free-running at 60fps), toggling only whether `eframe` resolves to stock 0.36.0 or this patch. Same machine and session, native Wayland (niri, wgpu/Vulkan). Whole-process CPU is `utime+stime` from `/proc/self/stat`, so it counts every thread — what a system monitor sees. | configuration | whole process | |---|---| | eframe 0.34.3, Wayland | ~12% of a core | | stock 0.36.0, Wayland | **99–100% of a core** | | stock 0.36.0, XWayland (same binary) | ~15% of a core | | **0.36.0 + this patch, Wayland** | **15–16% of a core** | The patch restores the 0.34 baseline and matches the XWayland figure for the same binary — ~6.5× less CPU — with frame delivery unchanged at 60fps. Two details worth noting: the same 0.36 binary is already fine on XWayland, so this isn't application repaint behaviour; and the per-frame *closure* cost rises slightly (1.55 to 2.15 ms) because those frames now run on a CPU that isn't being held at max clocks by the spin loop. * [X] I have followed the instructions in the PR template --- crates/eframe/src/native/run.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 53979591d..7c38aba4b 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -207,7 +207,12 @@ impl WinitAppWrapper { invisible_window_ids.push(*window_id); } else { log::trace!("request_redraw for {window_id:?}"); - event_loop.set_control_flow(ControlFlow::Poll); + // Don't switch to `ControlFlow::Poll` here. `request_redraw` + // is enough to wake the event loop, and on Wayland the + // `RedrawRequested` event is only delivered once the + // compositor sends a frame callback. Polling in the meantime + // busy-loops a whole CPU core. + // See https://github.com/emilk/egui/issues/8326. window.request_redraw(); } } else { @@ -237,10 +242,16 @@ impl WinitAppWrapper { } } + // Always set an explicit, sleeping control flow. Previously we only set + // `WaitUntil` when a repaint was already scheduled, which meant that a + // `ControlFlow::Poll` set earlier was never undone once the last timed + // repaint had been consumed, leaving the loop spinning. + // See https://github.com/emilk/egui/issues/8326. let next_repaint_time = self.windows_next_repaint_times.values().min().copied(); - if let Some(next_repaint_time) = next_repaint_time { - event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); - } + event_loop.set_control_flow(match next_repaint_time { + Some(next_repaint_time) => ControlFlow::WaitUntil(next_repaint_time), + None => ControlFlow::Wait, + }); } } From 9338f23bc6c8f44ce61fd0fc9822cd3d2c72775b Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Tue, 11 Aug 2026 13:52:58 +0200 Subject: [PATCH 38/49] Update dependencies to get rid of quick-xml 0.39 (plus update vello_cpu) (#8411) * [x] I have followed the instructions in the PR template --- Cargo.lock | 96 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- deny.toml | 3 +- 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e472f7c3b..a35154eee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -205,7 +205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" dependencies = [ "android_log-sys", - "env_filter", + "env_filter 0.1.4", "log", ] @@ -226,9 +226,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -247,9 +247,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -1587,14 +1587,24 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.11.8" +name = "env_filter" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", - "env_filter", + "env_filter 2.0.0", "jiff", "log", ] @@ -1645,7 +1655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1993,9 +2003,9 @@ dependencies = [ [[package]] name = "glifo" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" +checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be" dependencies = [ "bytemuck", "foldhash", @@ -3529,7 +3539,7 @@ checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" dependencies = [ "base64", "indexmap", - "quick-xml 0.41.0", + "quick-xml", "serde", "time", ] @@ -3730,16 +3740,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.39.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -4098,7 +4098,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4558,7 +4558,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5082,9 +5082,9 @@ dependencies = [ [[package]] name = "vello_common" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" +checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd" dependencies = [ "bytemuck", "fearless_simd", @@ -5097,9 +5097,9 @@ dependencies = [ [[package]] name = "vello_cpu" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" +checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9" dependencies = [ "bytemuck", "glifo", @@ -5281,12 +5281,12 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.10" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml 0.39.4", + "quick-xml", "quote", ] @@ -5558,7 +5558,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5939,9 +5939,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" dependencies = [ "async-broadcast", "async-executor", @@ -5998,9 +5998,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.16.0" +version = "5.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6013,9 +6013,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.2" +version = "4.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" dependencies = [ "serde", "winnow", @@ -6024,12 +6024,12 @@ dependencies = [ [[package]] name = "zbus_xml" -version = "5.1.1" +version = "5.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8067892e940ed1727dea64690378601603b31d62dfde019a5335fbb7c0e0ed9" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" dependencies = [ - "quick-xml 0.39.4", "serde", + "winnow", "zbus_names", "zvariant", ] @@ -6137,9 +6137,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.12.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" dependencies = [ "endi", "enumflags2", @@ -6151,9 +6151,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.12.0" +version = "5.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -6164,9 +6164,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5f087c309..dd12fe9f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,7 +146,7 @@ type-map = "0.5.1" unicode_names2 = { version = "3.1", default-features = false } unicode-general-category = "1.1" unicode-segmentation = "1.13" -vello_cpu = { version = "0.1.0", default-features = false, features = [ +vello_cpu = { version = "0.2.0", default-features = false, features = [ "std", "u8_pipeline", "f32_pipeline", diff --git a/deny.toml b/deny.toml index ff6c8e2ac..cdc81d99d 100644 --- a/deny.toml +++ b/deny.toml @@ -34,8 +34,6 @@ ignore = [ "RUSTSEC-2024-0320", # unmaintained yaml-rust pulled in by syntect "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-0192", # ttf-parser is unmaintained. Only brought in via winit/sctk-adwaita (wayland window frame rendering) - "RUSTSEC-2026-0194", # quick-xml DoS - fix is in >=0.41, but held back transitively by zbus_xml (accesskit) and wayland-scanner (winit) - "RUSTSEC-2026-0195", # quick-xml DoS - same as above "RUSTSEC-2026-0206", # rustybuzz is unmaintained. Brought in via resvg. TODO(linebender/resvg#922): Remove once the PR lands and is released ] @@ -53,6 +51,7 @@ skip = [ { name = "bit-vec" }, # dependency of bit-set in turn, different between 0.6 and 0.5 { name = "bitflags" }, # old 1.0 version via glutin, png, spirv, … { name = "core-foundation" }, # version conflict between winit and wgpu ecosystems + { name = "env_filter" }, # 0.1.4 only used by android_logger, which is only used by the hello_android example { name = "foldhash" }, # pulled by the duplicated hashbrown versions { name = "getrandom" }, # ring / rustls (and thus ehttp) still depend on getrandom 0.2 { name = "hashbrown" }, # wgpu's naga depends on 0.16, accesskit depends on 0.15 From 6d98e1dccbe0954932a88416c4c1c031506fb499 Mon Sep 17 00:00:00 2001 From: Vitaly Kravchenko Date: Tue, 11 Aug 2026 12:54:44 +0100 Subject: [PATCH 39/49] Allow explicit popup sizing passes (#8407) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [x] I have followed the instructions in the PR template ## Summary - Add an opt-in `Popup::sizing_pass(bool)` builder for remeasuring a popup whose contents change while it remains open. - Preserve the automatic first-open/reopen sizing pass and all existing default behavior. - Add a headless regression covering growth to a capped scroll viewport with overflowing content remaining scrollable. ## Why this is necessary A continuously open popup can first shrink around a short result set and later receive more content, such as an autocomplete after its query changes or a “show more” action. The cached `Area` height constrains the `ScrollArea` input size, so `ScrollArea::max_height` can cap the viewport but cannot make the containing popup grow again. PR #8315 taught `Popup` to rerun its sizing pass after closing and reopening. That fixes the same cached-size feedback loop when `was_open_last_frame` is false, but a continuously open popup keeps that value true while its contents change. In that case the caller is the component that knows the cached natural size is stale. This API exposes the existing one-frame `Area` sizing mechanism through `Popup`. It is additive, defaults to false, and combines with the automatic reopen pass, so unrelated popups, menus, tooltips, and areas keep their current behavior. --- crates/egui/src/containers/popup.rs | 18 ++++++- crates/egui_kittest/tests/popup.rs | 79 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 8cd545dd0..beee9f900 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -179,6 +179,7 @@ pub struct Popup<'a> { /// Default width passed to the Area width: Option, + sizing_pass: bool, sense: Sense, interactable: bool, layout: Layout, @@ -202,6 +203,7 @@ impl<'a> Popup<'a> { alternative_aligns: None, gap: 0.0, width: None, + sizing_pass: false, sense: Sense::click(), interactable: true, layout: Layout::default(), @@ -401,6 +403,19 @@ impl<'a> Popup<'a> { self } + /// Force the popup's underlying [`Area`] to run an invisible sizing pass. + /// + /// Popups automatically run a sizing pass when they open or reopen. Set this to `true` for + /// one frame when the contents of an already open popup change and its cached size may no + /// longer fit. Do not leave it enabled continuously, because the popup would remain invisible. + /// + /// Default: `false`. + #[inline] + pub fn sizing_pass(mut self, sizing_pass: bool) -> Self { + self.sizing_pass = sizing_pass; + self + } + /// Set the id of the Area. #[inline] pub fn id(mut self, id: Id) -> Self { @@ -556,6 +571,7 @@ impl<'a> Popup<'a> { alternative_aligns: _, gap, width, + sizing_pass, sense, interactable, layout, @@ -584,7 +600,7 @@ impl<'a> Popup<'a> { .sense(sense) .interactable(interactable) .layout(layout) - .sizing_pass(!was_open_last_frame) + .sizing_pass(sizing_pass || !was_open_last_frame) .info(info.unwrap_or_else(|| { UiStackInfo::new(kind.into()).with_tag_value( MenuConfig::MENU_CONFIG_TAG, diff --git a/crates/egui_kittest/tests/popup.rs b/crates/egui_kittest/tests/popup.rs index 4dbe33909..9d9dba654 100644 --- a/crates/egui_kittest/tests/popup.rs +++ b/crates/egui_kittest/tests/popup.rs @@ -67,6 +67,85 @@ fn reopened_popup_resizes_for_wider_items() { ); } +#[test] +fn open_popup_resizes_after_explicit_sizing_pass() { + const POPUP_BUTTON: &str = "Growing popup"; + const MAX_HEIGHT: f32 = 100.0; + + struct State { + item_count: usize, + needs_sizing_pass: bool, + popup_height: f32, + viewport_height: f32, + content_height: f32, + } + + let mut harness = Harness::builder() + .with_size(egui::Vec2::new(500.0, 300.0)) + .build_ui_state( + |ui, state| { + let response = ui.button(POPUP_BUTTON); + let needs_sizing_pass = core::mem::take(&mut state.needs_sizing_pass); + let item_count = state.item_count; + + if let Some(popup) = Popup::from_response(&response) + .sizing_pass(needs_sizing_pass) + .show(|ui| { + egui::ScrollArea::vertical() + .max_height(MAX_HEIGHT) + .show(ui, |ui| { + for index in 0..item_count { + ui.label(format!("Item {index}")); + } + }) + }) + { + state.popup_height = popup.response.rect.height(); + state.viewport_height = popup.inner.inner_rect.height(); + state.content_height = popup.inner.content_size.y; + } + }, + State { + item_count: 2, + needs_sizing_pass: false, + popup_height: 0.0, + viewport_height: 0.0, + content_height: 0.0, + }, + ); + + harness.run(); + let initial_popup_height = harness.state().popup_height; + + harness.state_mut().item_count = 20; + harness.run(); + let stale_viewport_height = harness.state().viewport_height; + assert!( + stale_viewport_height < MAX_HEIGHT, + "viewport unexpectedly reached its maximum without a sizing pass" + ); + + harness.state_mut().needs_sizing_pass = true; + harness.run(); + + assert!( + harness.state().popup_height > initial_popup_height, + "popup did not grow after an explicit sizing pass" + ); + assert!( + harness.state().viewport_height > stale_viewport_height, + "scroll viewport did not grow after an explicit sizing pass" + ); + assert!( + (harness.state().viewport_height - MAX_HEIGHT).abs() <= 0.5, + "scroll viewport did not stop at its maximum height" + ); + assert!( + harness.state().content_height > harness.state().viewport_height, + "popup contents did not remain scrollable at the maximum height" + ); +} + #[test] fn test_interactive_tooltip() { struct State { From 3c69fb4833194864d3634f802c57fa87db85e7dc Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:03:53 +0900 Subject: [PATCH 40/49] Fallback `window_title_frame` to `window_frame` when unspecified (#8400) ### Summary When `title_frame` is not explicitly set, fall back to `window_frame` instead of `Frame::window(&style)`. ### Motivation If a custom `frame` is provided for a window, `window_title_frame` should maintain visual consistency with it by default unless a separate `title_frame` is specified. * Related #8154 * Related #8353 --- crates/egui/src/containers/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index 69d3f0bf0..c3f4dcb24 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -627,8 +627,8 @@ impl Window<'_> { let style = ctx.global_style(); // We get or create the Frame for the title and content - let window_title_frame = title_frame.unwrap_or_else(|| Frame::window(&style)); let window_frame = frame.unwrap_or_else(|| Frame::window(&style)); + let window_title_frame = title_frame.unwrap_or(window_frame); // We apply the window margin by using the `ScrollArea::content_margin`. let window_content_margin = window_frame.inner_margin; From d802a982ce959839d92f4f6718fdffa41143110e Mon Sep 17 00:00:00 2001 From: Teddy Tennant Date: Tue, 11 Aug 2026 08:04:09 -0400 Subject: [PATCH 41/49] Don't revert external changes to a focused `DragValue` (#8403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Closes * [x] I have followed the instructions in the PR template ## The bug While a `DragValue` has focus it is rendered as a `TextEdit`, and the text being edited is stored in `Memory::data` between frames. That is needed so that half-finished input such as `"1."` or `"-"` isn't thrown away just because it doesn't parse to the current value. The stored text was only discarded when the widget *gained* focus or when the widget itself changed the value. If something else changed the value while the `DragValue` was focused, the stored text was kept, shown to the user, and written back to the value when focus was lost — silently undoing the external change: ```rust ui.add(egui::DragValue::new(&mut self.value)); if ui.button("increment").clicked() { self.value += 1; } ``` Click into the `DragValue` so it has focus, then press "increment": the value goes up for one frame and then snaps back. `Slider` shows the same behaviour, since it uses a `DragValue` for its value field. ## The fix Store the value the text belongs to next to the text, and discard the text when the value no longer matches it. The remembered value is read back from the get/set closure *after* the widget has applied its own edits, so a change the widget made itself never looks like an external one — this matters for values that can't represent what was typed, e.g. `"12.5"` in a `DragValue`. This keeps the reason the text is stored in the first place intact: as long as nothing else touches the value, the text the user is typing is preserved verbatim. ## Tests Three tests in `crates/egui_kittest/tests/regression_tests.rs`: * `drag_value_should_not_revert_external_changes_while_focused` — the actual regression. Fails on `main`: ``` ---- drag_value_should_not_revert_external_changes_while_focused stdout ---- assertion `left == right` failed left: Some("0") right: Some("42") ``` and, with the display assertion removed so the test reaches the blur, on the value itself: ``` assertion `left == right` failed left: 0 right: 42 ``` * `drag_value_should_keep_text_while_typing` and `drag_value_should_keep_text_the_value_cannot_represent` — guards for the behaviour the stored text exists for. Both pass on `main` and after the fix, and both fail if the text is re-read from the value too eagerly. `cargo test -p egui_kittest` and `cargo test -p egui` pass, as do `cargo fmt --all --check`, `scripts/lint.py` and `cargo clippy -p egui -p egui_kittest --all-targets --all-features -- -D warnings`. ## Not changed `DragValue` still ignores the stored text when Escape is pressed, and `update_while_editing` still decides when typed text is applied — neither is touched here. --- crates/egui/src/widgets/drag_value.rs | 48 +++++++-- crates/egui_kittest/tests/regression_tests.rs | 97 +++++++++++++++++++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/crates/egui/src/widgets/drag_value.rs b/crates/egui/src/widgets/drag_value.rs index 81f686fe1..eed264a82 100644 --- a/crates/egui/src/widgets/drag_value.rs +++ b/crates/egui/src/widgets/drag_value.rs @@ -25,6 +25,24 @@ fn set(get_set_value: &mut GetSetValue<'_>, value: f64) { (get_set_value)(Some(value)); } +// ---------------------------------------------------------------------------- + +/// What the user has typed into a [`DragValue`] that is being edited as text. +/// +/// Stored in [`crate::Memory::data`] between frames, because the text can be +/// something that doesn't (yet) parse to a number, e.g. `"1."` or `"-"`. +#[derive(Clone, Default)] +struct EditState { + /// The text the user is editing. + text: String, + + /// The value of the [`DragValue`] the last time we stored `text`. + /// + /// If the value has changed since then it was changed by something other than + /// this widget, and `text` is stale and must not be written back to the value. + value: f64, +} + /// A numeric value that you can change by dragging the number. More compact than a [`crate::Slider`]. /// /// ``` @@ -466,7 +484,7 @@ impl Widget for DragValue<'_> { }); if ui.memory_mut(|mem| mem.gained_focus(id)) { - ui.data_mut(|data| data.remove::(id)); + ui.data_mut(|data| data.remove::(id)); } let old_value = get(&mut get_set_value); @@ -524,7 +542,7 @@ impl Widget for DragValue<'_> { if old_value != value { set(&mut get_set_value, value); - ui.data_mut(|data| data.remove::(id)); + ui.data_mut(|data| data.remove::(id)); } let value_text = match custom_formatter { @@ -538,8 +556,13 @@ impl Widget for DragValue<'_> { let text_style = ui.style().drag_value_text_style.clone(); if ui.memory(|mem| mem.lost_focus(id)) && !ui.input(|i| i.key_pressed(Key::Escape)) { - let value_text = ui.data_mut(|data| data.remove_temp::(id)); - if let Some(value_text) = value_text { + let edit_state = ui.data_mut(|data| data.remove_temp::(id)); + // Ignore the text if the value was changed by something else while we were editing it, + // or we would revert that change. + if let Some(value_text) = edit_state + .filter(|edit_state| edit_state.value == old_value) + .map(|edit_state| edit_state.text) + { // We were editing the value as text last frame, but lost focus. // Make sure we applied the last text value: let parsed_value = parse(custom_parser.as_ref(), &value_text); @@ -552,9 +575,12 @@ impl Widget for DragValue<'_> { } let mut response = if is_kb_editing { + // Keep editing the text from last frame, unless the value was changed by + // something else in the meantime, in which case the text is stale. let mut value_text = ui - .data_mut(|data| data.remove_temp::(id)) - .unwrap_or_else(|| value_text.clone()); + .data_mut(|data| data.remove_temp::(id)) + .filter(|edit_state| edit_state.value == old_value) + .map_or_else(|| value_text.clone(), |edit_state| edit_state.text); let response = ui.add( TextEdit::singleline(&mut value_text) .clip_text(false) @@ -589,7 +615,13 @@ impl Widget for DragValue<'_> { set(&mut get_set_value, parsed_value); } } - ui.data_mut(|data| data.insert_temp(id, value_text)); + // Remember the value the text belongs to, so that next frame we can tell + // whether the value was changed by us or by something else. + let edit_state = EditState { + text: value_text, + value: get(&mut get_set_value), + }; + ui.data_mut(|data| data.insert_temp(id, edit_state)); response } else { atoms.map_atoms(|atom| { @@ -631,7 +663,7 @@ impl Widget for DragValue<'_> { } if response.clicked() { - ui.data_mut(|data| data.remove::(id)); + ui.data_mut(|data| data.remove::(id)); ui.memory_mut(|mem| mem.request_focus(id)); select_all_text(ui, id, response.id, &value_text); } else if response.dragged() { diff --git a/crates/egui_kittest/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs index 12e66ac56..1f47cd422 100644 --- a/crates/egui_kittest/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -814,3 +814,100 @@ pub fn textedit_hint_text_should_follow_text_alignment() { edit_center_x={edit_center_x}, edit_rect={edit_rect:?}", ); } + +/// A focused `DragValue` keeps the text the user is editing in memory. +/// +/// If something else changes the value while the `DragValue` has focus, +/// that memorized text is stale, and must not be written back to the value. +/// +/// Regression test for . +#[test] +pub fn drag_value_should_not_revert_external_changes_while_focused() { + let mut harness = Harness::new_ui_state( + |ui, value: &mut i32| { + ui.add(egui::DragValue::new(value)); + }, + 0, + ); + + // Focus the `DragValue`, putting it in text-edit mode. + harness.key_press(egui::Key::Tab); + harness.run(); + + // Something else changes the value while the `DragValue` is focused. + *harness.state_mut() = 42; + harness.run(); + + assert_eq!(harness.state(), &42); + let drag_value = harness.get_by_role(accesskit::Role::SpinButton); + assert_eq!(drag_value.value(), Some("42".to_owned())); + + // Losing focus must not restore the value the `DragValue` had when it gained focus. + harness.key_press(egui::Key::Tab); + harness.run(); + + assert_eq!(harness.state(), &42); +} + +/// While the user is typing into a `DragValue`, the half-finished text must be kept +/// between frames, even though it doesn't always parse back to the same text. +#[test] +pub fn drag_value_should_keep_text_while_typing() { + let mut harness = Harness::new_ui_state( + |ui, value: &mut f64| { + ui.add(egui::DragValue::new(value)); + }, + 0.0, + ); + + // Focus the `DragValue`, putting it in text-edit mode with the old text selected. + harness.key_press(egui::Key::Tab); + harness.run(); + + // Type one character per frame. `"1."` parses to `1`, which is formatted as `"1"`, + // so re-reading the text from the value would eat the decimal point. + for character in "1.25".chars() { + harness + .get_by_role(accesskit::Role::SpinButton) + .type_text(&character.to_string()); + harness.run(); + } + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert_eq!(harness.state(), &1.25); +} + +/// An integer `DragValue` cannot represent everything the user types into it, +/// but the text must still survive until the user is done typing. +#[test] +pub fn drag_value_should_keep_text_the_value_cannot_represent() { + let mut harness = Harness::new_ui_state( + |ui, value: &mut i32| { + ui.add(egui::DragValue::new(value)); + }, + 0, + ); + + // Focus the `DragValue`, putting it in text-edit mode with the old text selected. + harness.key_press(egui::Key::Tab); + harness.run(); + + // `"12.5"` is stored as `12`, which is formatted as `"12"`. + harness + .get_by_role(accesskit::Role::SpinButton) + .type_text("12.5"); + harness.run(); + + // If the text was re-read from the value now, this would append to `"12"`. + harness + .get_by_role(accesskit::Role::SpinButton) + .type_text("9"); + harness.run(); + + harness.key_press(egui::Key::Enter); + harness.run(); + + assert_eq!(harness.state(), &12, "The text should have been \"12.59\""); +} From 5579f831c290c49725114ef0650bb84e97f8dc65 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 17 Aug 2026 21:40:31 -0700 Subject: [PATCH 42/49] Update `webbrowser` to 1.2.2 to fix RUSTSEC-2026-0257 (#8431) Co-authored-by: Claude Opus 5 (1M context) --- Cargo.lock | 30 ++++++++++-------------------- Cargo.toml | 2 +- deny.toml | 1 - 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a35154eee..71ed40f62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -907,16 +907,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -930,7 +920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "core-graphics-types", "foreign-types", "libc", @@ -943,7 +933,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation 0.9.4", + "core-foundation", "libc", ] @@ -1655,7 +1645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4098,7 +4088,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4558,7 +4548,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5324,15 +5314,15 @@ dependencies = [ [[package]] name = "webbrowser" -version = "1.2.1" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" dependencies = [ - "core-foundation 0.10.1", "jni", "log", "ndk-context", "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "url", "web-sys", @@ -5558,7 +5548,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5774,7 +5764,7 @@ dependencies = [ "calloop", "cfg_aliases", "concurrent-queue", - "core-foundation 0.9.4", + "core-foundation", "core-graphics", "cursor-icon", "dpi", diff --git a/Cargo.toml b/Cargo.toml index dd12fe9f0..26a60c57b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,7 +156,7 @@ wasm-bindgen-futures = "0.4.76" wayland-cursor = { version = "0.31.14", default-features = false } web-sys = "0.3.103" web-time = "1.1" # Timekeeping for native and web -webbrowser = "1.2" +webbrowser = "1.2.2" # 1.2.2 fixes RUSTSEC-2026-0257 (`BROWSER` argument injection) wgpu = { version = "30.0", default-features = false, features = ["std"] } windows-sys = "0.61.2" winit = { version = "0.30.13", default-features = false } diff --git a/deny.toml b/deny.toml index cdc81d99d..01faf237a 100644 --- a/deny.toml +++ b/deny.toml @@ -50,7 +50,6 @@ skip = [ { name = "bit-set" }, # wgpu's naga depends on 0.8, syntect's (used by egui_extras) fancy-regex depends on 0.5 { name = "bit-vec" }, # dependency of bit-set in turn, different between 0.6 and 0.5 { name = "bitflags" }, # old 1.0 version via glutin, png, spirv, … - { name = "core-foundation" }, # version conflict between winit and wgpu ecosystems { name = "env_filter" }, # 0.1.4 only used by android_logger, which is only used by the hello_android example { name = "foldhash" }, # pulled by the duplicated hashbrown versions { name = "getrandom" }, # ring / rustls (and thus ehttp) still depend on getrandom 0.2 From 9bb36b0ac2b64ac94d4cb7bef5d8e3e28e87a6c5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 17 Aug 2026 21:41:09 -0700 Subject: [PATCH 43/49] Match image file extensions case-insensitively (#8430) `image.PNG` and `cat.SVG` were not recognized as images. Adds `egui::load::has_extension(uri, extension)`, which ignores ASCII case and any `#fragment`, and uses it for the `.svg`, `.gif`, `.webp` and `.png` checks. Note: gif/webp URIs like `a#b.gif` no longer match, since the fragment is now excluded. * [x] I have followed the instructions in the PR template Co-authored-by: Claude Opus 5 (1M context) --- crates/eframe/src/native/glow_integration.rs | 2 +- crates/egui/src/load.rs | 24 ++++++++++++++++++++ crates/egui/src/load/texture_loader.rs | 2 +- crates/egui/src/widgets/image.rs | 4 ++-- crates/egui_extras/src/loaders/svg_loader.rs | 2 +- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 1b9469cfd..cca1d22c6 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -1720,7 +1720,7 @@ fn save_screenshot_and_exit( screen_size_in_pixels: [u32; 2], ) { assert!( - path.ends_with(".png"), + egui::load::has_extension(path, "png"), "Expected EFRAME_SCREENSHOT_TO to end with '.png', got {path:?}" ); let screenshot = painter.read_screen_rgba(screen_size_in_pixels); diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index f3acbf6af..df390dc64 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -306,6 +306,19 @@ macro_rules! generate_loader_id { } pub use crate::generate_loader_id; +/// Does the given URI end with the given file extension? +/// +/// The comparison ignores ASCII case and any `#fragment` at the end of the URI, +/// so `has_extension("cat.GIF#frame=2", "gif")` is `true`. +/// +/// This is useful when implementing an [`ImageLoader`]. +pub fn has_extension(uri: &str, extension: &str) -> bool { + let path = uri.split('#').next().unwrap_or(uri); + std::path::Path::new(path) + .extension() + .is_some_and(|found| found.eq_ignore_ascii_case(extension)) +} + pub type BytesLoadResult = Result; /// Represents a loader capable of loading raw unstructured bytes from somewhere, @@ -639,3 +652,14 @@ impl Loaders { } } } + +#[test] +fn test_has_extension() { + assert!(has_extension("cat.svg", "svg")); + assert!(has_extension("cat.SVG", "svg")); + assert!(has_extension("http://example.com/cat.gif#frame=2", "gif")); + assert!(!has_extension("cat.svg.png", "svg")); + assert!(!has_extension("svg", "svg")); + assert!(!has_extension("cat.jpeg", "jpg")); + assert!(!has_extension("cat.svg?v=1", "svg")); +} diff --git a/crates/egui/src/load/texture_loader.rs b/crates/egui/src/load/texture_loader.rs index 2ab4d8a57..eb9b4503b 100644 --- a/crates/egui/src/load/texture_loader.rs +++ b/crates/egui/src/load/texture_loader.rs @@ -150,5 +150,5 @@ impl TextureLoader for DefaultTextureLoader { } fn is_svg(uri: &str) -> bool { - uri.ends_with(".svg") + super::has_extension(uri, "svg") } diff --git a/crates/egui/src/widgets/image.rs b/crates/egui/src/widgets/image.rs index 0618e8661..9bce8c4d3 100644 --- a/crates/egui/src/widgets/image.rs +++ b/crates/egui/src/widgets/image.rs @@ -934,7 +934,7 @@ fn animated_image_frame_index(ctx: &Context, uri: &str) -> usize { /// Checks if uri is a gif file fn is_gif_uri(uri: &str) -> bool { - uri.ends_with(".gif") || uri.contains(".gif#") + crate::load::has_extension(uri, "gif") } /// Checks if bytes are gifs @@ -944,7 +944,7 @@ pub fn has_gif_magic_header(bytes: &[u8]) -> bool { /// Checks if uri is a webp file fn is_webp_uri(uri: &str) -> bool { - uri.ends_with(".webp") || uri.contains(".webp#") + crate::load::has_extension(uri, "webp") } /// Checks if bytes are webp diff --git a/crates/egui_extras/src/loaders/svg_loader.rs b/crates/egui_extras/src/loaders/svg_loader.rs index 91063f6b4..53c879056 100644 --- a/crates/egui_extras/src/loaders/svg_loader.rs +++ b/crates/egui_extras/src/loaders/svg_loader.rs @@ -29,7 +29,7 @@ impl SvgLoader { } fn is_supported(uri: &str) -> bool { - uri.ends_with(".svg") + egui::load::has_extension(uri, "svg") } impl Default for SvgLoader { From 34b39d564b8810dc8bc1bc8975f32bb0315bdf23 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 17 Aug 2026 21:55:53 -0700 Subject: [PATCH 44/49] Enable the `clippy::pedantic` lint group (#8429) Instead of opting in to pedantic lints one by one, enable the whole group and opt out of the noisy ones. 64% of the pedantic lints were already listed individually. This deletes 90 explicit lint lines, enables 51 pedantic lints we never listed, and picks up new pedantic lints for free. Each opt-out carries its hit count, so the cost of turning one back on is visible. `restriction` and `nursery` stay opt-in per lint. Stacked on top of #8430, which fixes the one real bug the new lints found. * [x] I have followed the instructions in the PR template --------- Co-authored-by: Claude Opus 5 (1M context) --- Cargo.toml | 126 ++++-------------- crates/ecolor/src/hsva.rs | 4 - crates/eframe/src/native/app_icon.rs | 40 +++--- crates/eframe/src/native/file_storage.rs | 2 +- crates/egui/src/context.rs | 10 +- crates/egui/src/id.rs | 7 + .../src/easy_mark/easy_mark_parser.rs | 16 +-- crates/egui_extras/src/syntax_highlighting.rs | 7 + crates/egui_kittest/src/lib.rs | 14 +- crates/egui_kittest/src/snapshot.rs | 20 +-- crates/epaint/src/shapes/bezier_shape.rs | 2 - crates/epaint/src/tessellator.rs | 8 +- 12 files changed, 97 insertions(+), 159 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26a60c57b..b6534d2d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,138 +180,62 @@ all = "warn" # See also clippy.toml [workspace.lints.clippy] all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } allow_attributes = "warn" as_ptr_cast_mut = "warn" -bool_to_int_with_if = "warn" branches_sharing_code = "warn" -checked_conversions = "warn" clear_with_drain = "warn" clone_on_ref_ptr = "warn" -cloned_instead_of_copied = "warn" coerce_container_to_any = "warn" dbg_macro = "warn" debug_assert_with_mut_call = "warn" -decimal_bitwise_operands = "warn" default_union_representation = "warn" derive_partial_eq_without_eq = "warn" disallowed_script_idents = "warn" # See clippy.toml -doc_broken_link = "warn" -doc_comment_double_space_linebreaks = "warn" doc_include_without_cfg = "warn" -doc_link_with_quotes = "warn" -doc_markdown = "warn" -duration_suboptimal_units = "warn" -elidable_lifetime_names = "warn" empty_enum_variants_with_brackets = "warn" -empty_enums = "warn" -enum_glob_use = "warn" equatable_if_let = "warn" exit = "warn" -expl_impl_clone_on_copy = "warn" -explicit_deref_methods = "warn" -explicit_into_iter_loop = "warn" -explicit_iter_loop = "warn" fallible_impl_from = "warn" -filter_map_next = "warn" -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" -ignore_without_reason = "warn" -ignored_unit_patterns = "warn" -implicit_clone = "warn" imprecise_flops = "warn" -inconsistent_struct_constructor = "warn" -index_refutable_slice = "warn" -inefficient_to_string = "warn" infinite_loop = "warn" -into_iter_without_iter = "warn" -invalid_upcast_comparisons = "warn" -ip_constant = "warn" -iter_filter_is_ok = "warn" -iter_filter_is_some = "warn" -iter_not_returning_iterator = "warn" iter_on_empty_collections = "warn" iter_on_single_items = "warn" iter_over_hash_type = "warn" -iter_without_into_iter = "warn" -large_digit_groups = "warn" -large_futures = "warn" large_include_file = "warn" -large_stack_arrays = "warn" large_stack_frames = "warn" -large_types_passed_by_value = "warn" -linkedlist = "warn" literal_string_with_formatting_args = "warn" lossy_float_literal = "warn" -macro_use_imports = "warn" -manual_assert = "warn" -manual_ilog2 = "warn" -manual_instant_elapsed = "warn" -manual_is_power_of_two = "warn" -manual_is_variant_and = "warn" -manual_let_else = "warn" -manual_midpoint = "warn" # NOTE `midpoint` is often a lot slower for floats, so we have our own `emath::fast_midpoint` function. -manual_string_new = "warn" map_err_ignore = "warn" -match_bool = "warn" -match_same_arms = "warn" -match_wild_err_arm = "warn" -match_wildcard_for_single_variants = "warn" mem_forget = "warn" -mismatching_type_param_order = "warn" missing_assert_message = "warn" -missing_errors_doc = "warn" -missing_fields_in_debug = "warn" -mut_mut = "warn" mutex_integer = "warn" -needless_continue = "warn" -needless_for_each = "warn" needless_pass_by_ref_mut = "warn" -needless_pass_by_value = "warn" -needless_raw_string_hashes = "warn" needless_type_cast = "warn" negative_feature_names = "warn" -non_std_lazy_statics = "warn" non_zero_suggestions = "warn" nonstandard_macro_braces = "warn" -option_as_ref_cloned = "warn" -option_option = "warn" or_fun_call = "warn" path_buf_push_overwrite = "warn" pathbuf_init_then_push = "warn" precedence_bits = "warn" print_stderr = "warn" print_stdout = "warn" -ptr_as_ptr = "warn" -ptr_cast_constness = "warn" -pub_underscore_fields = "warn" pub_without_shorthand = "warn" rc_mutex = "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" return_and_then = "warn" -same_functions_in_if_condition = "warn" -same_length_and_capacity = "warn" -self_only_used_in_recursion = "warn" -semicolon_if_nothing_returned = "warn" set_contains_or_insert = "warn" -single_char_pattern = "warn" -single_match_else = "warn" single_option_map = "warn" std_instead_of_core = "warn" -str_split_at_newline = "warn" str_to_string = "warn" string_add = "warn" -string_add_assign = "warn" string_lit_as_bytes = "warn" string_lit_chars_any = "warn" suspicious_xor_used_as_pow = "warn" @@ -319,52 +243,58 @@ todo = "warn" too_long_first_doc_paragraph = "warn" trailing_empty_array = "warn" trait_duplication_in_bounds = "warn" -transmute_ptr_to_ptr = "warn" tuple_array_conversions = "warn" -unchecked_time_subtraction = "warn" undocumented_unsafe_blocks = "warn" unimplemented = "warn" uninhabited_references = "warn" -uninlined_format_args = "warn" -unnecessary_box_returns = "warn" -unnecessary_debug_formatting = "warn" -unnecessary_literal_bound = "warn" unnecessary_safety_comment = "warn" unnecessary_safety_doc = "warn" unnecessary_self_imports = "warn" -unnecessary_semicolon = "warn" unnecessary_struct_initialization = "warn" -unnecessary_trailing_comma = "warn" -unnecessary_wraps = "warn" -unnested_or_patterns = "warn" -unused_async = "warn" unused_peekable = "warn" unused_rounding = "warn" -unused_self = "warn" unused_trait_names = "warn" unwrap_used = "warn" use_self = "warn" useless_let_if_seq = "warn" verbose_file_reads = "warn" wildcard_dependencies = "warn" -zero_sized_map_values = "warn" -# TODO(emilk): maybe enable more of these lints? -# NOTE: these are all in `pedantic`/`restriction`/`nursery`, so the `allow` is a no-op today. -# We keep them to record our intent in case we ever enable those groups. -cast_possible_wrap = "allow" +# Pedantic lints we opt out of, with the number of hits at the time we enabled `pedantic`: +cast_lossless = "allow" # 204 hits +cast_possible_truncation = "allow" # 287 hits +cast_possible_wrap = "allow" # 43 hits +cast_precision_loss = "allow" # 200 hits +cast_sign_loss = "allow" # 98 hits comparison_chain = "allow" +default_trait_access = "allow" # 278 hits +float_cmp = "allow" # exact float comparisons are usually intentional (`float_cmp_const` is still on) +inline_always = "allow" # 271 hits; we know what we are doing +items_after_statements = "allow" # 82 hits +many_single_char_names = "allow" # `r, g, b, a` and `h, s, v` are fine +missing_panics_doc = "allow" # 68 hits +must_use_candidate = "allow" # 1169 hits +redundant_closure_for_method_calls = "allow" # 89 hits +return_self_not_must_use = "allow" # 246 hits should_panic_without_expect = "allow" +similar_names = "allow" # too many false positives, e.g. `encoder`/`encoded` +struct_excessive_bools = "allow" # 32 hits +struct_field_names = "allow" # 23 hits too_many_lines = "allow" +trivially_copy_pass_by_ref = "allow" # 119 hits +unreadable_literal = "allow" # 513 hits +used_underscore_binding = "allow" # 25 hits # These are meh: assigning_clones = "allow" # No please -cast_possible_truncation = "allow" # too many hits -let_underscore_must_use = "allow" -let_underscore_untyped = "allow" manual_range_contains = "allow" # this one is just worse imho map_unwrap_or = "allow" # so is this one +wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports + +# NOTE: these are in `restriction`/`nursery`, so the `allow` is a no-op today. +# We keep them to record our intent in case we ever enable those groups. +let_underscore_must_use = "allow" +let_underscore_untyped = "allow" self_named_module_files = "allow" # Disabled waiting on https://github.com/rust-lang/rust-clippy/issues/9602 significant_drop_tightening = "allow" # Too many false positives -wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports diff --git a/crates/ecolor/src/hsva.rs b/crates/ecolor/src/hsva.rs index 17008f5da..8d92d40b8 100644 --- a/crates/ecolor/src/hsva.rs +++ b/crates/ecolor/src/hsva.rs @@ -41,7 +41,6 @@ impl Hsva { /// From linear RGBA with premultiplied alpha #[inline] pub fn from_rgba_premultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - #![expect(clippy::many_single_char_names)] if a <= 0.0 { if r == 0.0 && b == 0.0 && a == 0.0 { Self::default() @@ -57,7 +56,6 @@ impl Hsva { /// From linear RGBA without premultiplied alpha #[inline] pub fn from_rgba_unmultiplied(r: f32, g: f32, b: f32, a: f32) -> Self { - #![expect(clippy::many_single_char_names)] let (h, s, v) = hsv_from_rgb([r, g, b]); Self { h, s, v, a } } @@ -189,7 +187,6 @@ impl From for Hsva { /// All ranges in 0-1, rgb is linear. #[inline] pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { - #![expect(clippy::many_single_char_names)] let min = r.min(g.min(b)); let max = r.max(g.max(b)); // value @@ -213,7 +210,6 @@ pub fn hsv_from_rgb([r, g, b]: [f32; 3]) -> (f32, f32, f32) { /// All ranges in 0-1, rgb is linear. #[inline] pub fn rgb_from_hsv((h, s, v): (f32, f32, f32)) -> [f32; 3] { - #![expect(clippy::many_single_char_names)] let h = (h.fract() + 1.0).fract(); // wrap let s = s.clamp(0.0, 1.0); diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 9fdeb30e0..ef6c96721 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -161,16 +161,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { if icon_big.is_null() { log::warn!("Failed to create HICON (for big icon) from embedded png data."); return AppIconStatus::NotSetIgnored; // We could try independently with the small icon but what's the point, it would look bad! - } else { - // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior. - unsafe { - SendMessageW( - window_handle, - WM_SETICON, - ICON_BIG as usize, - icon_big as isize, - ); - } + } + + // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior. + unsafe { + SendMessageW( + window_handle, + WM_SETICON, + ICON_BIG as usize, + icon_big as isize, + ); } } { @@ -180,16 +180,16 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { if icon_small.is_null() { log::warn!("Failed to create HICON (for small icon) from embedded png data."); return AppIconStatus::NotSetIgnored; - } else { - // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior. - unsafe { - SendMessageW( - window_handle, - WM_SETICON, - ICON_SMALL as usize, - icon_small as isize, - ); - } + } + + // SAFETY: Unsafe WinApi function, takes objects previously created with WinAPI, all checked for null prior. + unsafe { + SendMessageW( + window_handle, + WM_SETICON, + ICON_SMALL as usize, + icon_small as isize, + ); } } diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index f6f4ef477..70b4a3cae 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -67,7 +67,7 @@ fn roaming_appdata() -> Option { &FOLDERID_RoamingAppData, KF_FLAG_DONT_VERIFY as u32, core::ptr::null_mut(), - &mut path_raw, + &raw mut path_raw, ) }; diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 45193e15e..25b384c95 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1761,11 +1761,11 @@ impl Context { .get(&id) .map(|v| v.repaint.cumulative_frame_nr) .unwrap_or_else(|| { - if cfg!(debug_assertions) { - panic!("cumulative_frame_nr_for failed to find the viewport {id:?}"); - } else { - 0 - } + debug_assert!( + false, + "cumulative_frame_nr_for failed to find the viewport {id:?}" + ); + 0 }) }) } diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index c9d05465e..6b15fd05d 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -41,6 +41,13 @@ impl AsId for T {} /// This is niche-optimized to that `Option` is the same size as `Id`. #[derive(Clone, Copy, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[cfg_attr( + feature = "serde", + expect( + clippy::unsafe_derive_deserialize, + reason = "`from_high_entropy_bits` is only `unsafe` about entropy, not memory safety" + ) +)] pub struct Id(NonZeroU64); impl nohash_hasher::IsEnabled for Id {} diff --git a/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs b/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs index 66c0dc04f..0e36844c3 100644 --- a/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs +++ b/crates/egui_demo_lib/src/easy_mark/easy_mark_parser.rs @@ -118,15 +118,15 @@ impl<'a> Parser<'a> { { let language = &language_start[..newline]; let code_start = &language_start[newline + 1..]; - if let Some(end) = code_start.find("\n```") { + return if let Some(end) = code_start.find("\n```") { let code = &code_start[..end].trim(); self.s = &code_start[end + 4..]; self.start_of_line = false; - return Some(Item::CodeBlock(language, code)); + Some(Item::CodeBlock(language, code)) } else { self.s = ""; - return Some(Item::CodeBlock(language, code_start)); - } + Some(Item::CodeBlock(language, code_start)) + }; } None } @@ -138,18 +138,18 @@ impl<'a> Parser<'a> { self.start_of_line = false; self.style.code = true; let rest_of_line = &self.s[..self.s.find('\n').unwrap_or(self.s.len())]; - if let Some(end) = rest_of_line.find('`') { + return if let Some(end) = rest_of_line.find('`') { let item = Item::Text(self.style, &self.s[..end]); self.s = &self.s[end + 1..]; self.style.code = false; - return Some(item); + Some(item) } else { let end = rest_of_line.len(); let item = Item::Text(self.style, rest_of_line); self.s = &self.s[end..]; self.style.code = false; - return Some(item); - } + Some(item) + }; } None } diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index d09151d21..f45b68f2c 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -210,6 +210,13 @@ impl SyntectTheme { derive(serde::Deserialize, serde::Serialize), serde(default) )] +#[cfg_attr( + all(feature = "serde", not(feature = "syntect")), + expect( + clippy::unsafe_derive_deserialize, + reason = "the `enum_map!` macro expands to `unsafe` code" + ) +)] pub struct CodeTheme { dark_mode: bool, diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index fa8f26311..8b4ff9ad6 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -247,16 +247,16 @@ impl<'a, State> Harness<'a, State> { pub fn step(&mut self) { let events = core::mem::take(&mut *self.queued_events.lock()); if events.is_empty() { - self._step(false); + self.step_impl(false); } for event in events { self.input.events.push(event); - self._step(false); + self.step_impl(false); } } /// Run a single step. This will not process any events. - fn _step(&mut self, sizing_pass: bool) { + fn step_impl(&mut self, sizing_pass: bool) { self.input.predicted_dt = self.step_dt; let mut output = self.ctx.run_ui(self.input.take(), |ui| { @@ -297,7 +297,7 @@ impl<'a, State> Harness<'a, State> { /// [`Harness::new_ui`] / [`Harness::new_ui_state`] or /// [`HarnessBuilder::build_ui`] / [`HarnessBuilder::build_ui_state`]. pub fn fit_contents(&mut self) { - self._step(true); + self.step_impl(true); // Calculate size including all content (main UI + popups + tooltips) if let Some(rect) = self.compute_total_rect_with_popups() { @@ -333,7 +333,7 @@ impl<'a, State> Harness<'a, State> { } } - fn _try_run(&mut self, sleep: bool) -> Result { + fn try_run_impl(&mut self, sleep: bool) -> Result { let mut steps = 0; loop { steps += 1; @@ -374,7 +374,7 @@ impl<'a, State> Harness<'a, State> { /// - [`Harness::run_steps`]. /// - [`Harness::try_run_realtime`]. pub fn try_run(&mut self) -> Result { - self._try_run(false) + self.try_run_impl(false) } /// Run until @@ -414,7 +414,7 @@ impl<'a, State> Harness<'a, State> { /// - [`Harness::run_steps`]. /// - [`Harness::try_run`]. pub fn try_run_realtime(&mut self) -> Result { - self._try_run(true) + self.try_run_impl(true) } /// Run a number of steps. diff --git a/crates/egui_kittest/src/snapshot.rs b/crates/egui_kittest/src/snapshot.rs index e4472219f..2f709cc3c 100644 --- a/crates/egui_kittest/src/snapshot.rs +++ b/crates/egui_kittest/src/snapshot.rs @@ -535,31 +535,31 @@ fn try_image_snapshot_options_impl( Ok(image) => image.to_rgba8(), Err(err) => { // No previous snapshot - probably a new test. - if mode.is_update() { - return update_snapshot(); + return if mode.is_update() { + update_snapshot() } else { write_new_png()?; - return Err(SnapshotError::OpenSnapshot { + Err(SnapshotError::OpenSnapshot { path: snapshot_path.clone(), err, - }); - } + }) + }; } }; if previous.dimensions() != new.dimensions() { - if mode.is_update() { - return update_snapshot(); + return if mode.is_update() { + update_snapshot() } else { write_new_png()?; - return Err(SnapshotError::SizeMismatch { + Err(SnapshotError::SizeMismatch { name, expected: previous.dimensions(), actual: new.dimensions(), - }); - } + }) + }; } // Compare existing image to the new one: diff --git a/crates/epaint/src/shapes/bezier_shape.rs b/crates/epaint/src/shapes/bezier_shape.rs index caf1094a1..99694f4fb 100644 --- a/crates/epaint/src/shapes/bezier_shape.rs +++ b/crates/epaint/src/shapes/bezier_shape.rs @@ -1,5 +1,3 @@ -#![expect(clippy::many_single_char_names)] - use core::ops::Range; use crate::{Color32, PathShape, PathStroke, Shape}; diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 7b207abeb..a9784f48b 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1519,11 +1519,11 @@ impl Tessellator { if stroke.is_empty() { return; // we are done - } else { - // we still need to do the stroke - fill = Color32::TRANSPARENT; // don't fill again below - break; } + + // we still need to do the stroke + fill = Color32::TRANSPARENT; // don't fill again below + break; } } } From b9a0723d88dee87ba0965470b554a6efdab1d0bb Mon Sep 17 00:00:00 2001 From: Recoordinate <296084221+latent-9@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:52:07 +1200 Subject: [PATCH 45/49] Fix documentation typos (#8404) Two small documentation typos noticed while reading the docs: - `crates/egui_extras/README.md`: "adds some features on top top of" -> "adds some features on top of" - `README.md`: "check out the [the egui web demo]" -> "check out [the egui web demo]" (removes the duplicated "the"; link text and target unchanged) Documentation only; no code changes. --- README.md | 2 +- crates/egui_extras/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 45bd23f6d..4f105a0ef 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ ui.image(egui::include_image!("ferris.png")); ## Quick start -There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to and follow the instructions. The official docs are at . For inspiration and more examples, check out the [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code. +There are simple examples in [the `examples/` folder](https://github.com/emilk/egui/blob/main/examples/). If you want to write a web app, then go to and follow the instructions. The official docs are at . For inspiration and more examples, check out [the egui web demo](https://www.egui.rs/#demo) and follow the links in it to its source code. If you want to integrate egui into an existing engine, go to the [Integrations](#integrations) section. diff --git a/crates/egui_extras/README.md b/crates/egui_extras/README.md index 4e5e96dce..41c9ece8a 100644 --- a/crates/egui_extras/README.md +++ b/crates/egui_extras/README.md @@ -6,7 +6,7 @@ ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Apache](https://img.shields.io/badge/license-Apache-blue.svg) -This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`. +This is a crate that adds some features on top of [`egui`](https://github.com/emilk/egui). This crate is for experimental features, and features that require big dependencies that do not belong in `egui`. ## Images One thing `egui_extras` is commonly used for is to install image loaders for `egui`: From fd54387eac03f57ca772a8fb590ceaadf780f31c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 20 Aug 2026 01:42:12 -0700 Subject: [PATCH 46/49] Clean up clippy lint config (#8437) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [x] I have followed the instructions in the PR template Remove clippy lint groups already covered by `all` (`complexity`, `perf`, `suspicious`, and the misspelled `correctnesss`), and two individual lints covered by the `cargo` group (`negative_feature_names`, `wildcard_dependencies`). Add `publish = false` to the two internal crates that were missing it (`popups`, `egui_tests`), which silences the `clippy::cargo_common_metadata` warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 --- Cargo.toml | 9 ++++++--- examples/popups/Cargo.toml | 1 + tests/egui_tests/Cargo.toml | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b6534d2d1..3aae7fa74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,7 +179,11 @@ all = "warn" # See also clippy.toml [workspace.lints.clippy] +# `all` = `correctness` + `suspicious` + `style` + `complexity` + `perf`. +# The remaining groups are `nursery` and `restriction`, +# which are not meant to be enabled wholesale - we cherry-pick from them below. all = { level = "warn", priority = -1 } +cargo = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } allow_attributes = "warn" @@ -216,7 +220,6 @@ missing_assert_message = "warn" mutex_integer = "warn" needless_pass_by_ref_mut = "warn" needless_type_cast = "warn" -negative_feature_names = "warn" non_zero_suggestions = "warn" nonstandard_macro_braces = "warn" or_fun_call = "warn" @@ -258,7 +261,6 @@ unwrap_used = "warn" use_self = "warn" useless_let_if_seq = "warn" verbose_file_reads = "warn" -wildcard_dependencies = "warn" # Pedantic lints we opt out of, with the number of hits at the time we enabled `pedantic`: @@ -286,10 +288,11 @@ trivially_copy_pass_by_ref = "allow" # 119 hits unreadable_literal = "allow" # 513 hits used_underscore_binding = "allow" # 25 hits -# These are meh: +# Other: assigning_clones = "allow" # No please manual_range_contains = "allow" # this one is just worse imho map_unwrap_or = "allow" # so is this one +multiple_crate_versions = "allow" # we handle this with `cargo deny` wildcard_imports = "allow" # `use crate::*` is useful to avoid merge conflicts when adding/removing imports # NOTE: these are in `restriction`/`nursery`, so the `allow` is a no-op today. diff --git a/examples/popups/Cargo.toml b/examples/popups/Cargo.toml index d1f199918..c0d4384c7 100644 --- a/examples/popups/Cargo.toml +++ b/examples/popups/Cargo.toml @@ -2,6 +2,7 @@ name = "popups" edition.workspace = true license.workspace = true +publish = false rust-version.workspace = true version.workspace = true diff --git a/tests/egui_tests/Cargo.toml b/tests/egui_tests/Cargo.toml index 44a7b9c8f..89d6a9fda 100644 --- a/tests/egui_tests/Cargo.toml +++ b/tests/egui_tests/Cargo.toml @@ -2,6 +2,7 @@ name = "egui_tests" edition.workspace = true license.workspace = true +publish = false rust-version.workspace = true version.workspace = true From 38c4ab7b3d030cd34960296f620cfe4d066de032 Mon Sep 17 00:00:00 2001 From: Keith Date: Thu, 20 Aug 2026 13:36:39 -0700 Subject: [PATCH 47/49] Simplify and optimize `Color32::from_rgba_unmultiplied` (#8427) So I first noticed that this function was using a large lookup table behind a OnceLock. I initially thought about just making it a const, but when looking at things further. I realized it could be made much simpler. If we just treated the numbers as fixed point we can get rid of any of the floating point calculations and especially divisions. You can see how efficiently this can compile down here: https://llvm.godbolt.org/z/K83jEjvdq You can see all three versions here: https://godbolt.org/z/nWc1as1nq * First one is basically the original essentially being called by: `ColorImage::from_rgba_unmultiplied()` * Second is the const Lookup table instead of the OnceLock and runtime generation. * Third is the fixed point implementation. At least looking at the bytes reported compiler explorer the OnceLock and the const Table results are in similar size, and the const table is surprisingly smaller when I compile to a binary object in compiler explorer. However though the oncelock is producing a lot SIMD instructions for initialization so I guess not too surprised. The fixed point math is much smaller than both. The const table is probably faster, but does bloat the binary images, and again when it's this fast to compute: https://llvm.godbolt.org/z/K83jEjvdq I am not sure the extra bytes are worth it. Next, what I did was merge `from_rgba_unmultiplied` and `from_rgba_unmultiplied_const`. Moreover with the fixed point math the `from_rgba_unmultiplied_const` is probably not necessary anymore, but it's part of the public API so I left it. Lastly, I just added a sanity test to make sure the math checks out which it does. You can even sweep the 2^16 inputs to be sure. --- crates/ecolor/src/color32.rs | 47 ++++++++++++------------------------ crates/ecolor/src/lib.rs | 11 +++++++++ 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/crates/ecolor/src/color32.rs b/crates/ecolor/src/color32.rs index 68a8fc3d6..7a9cf72b7 100644 --- a/crates/ecolor/src/color32.rs +++ b/crates/ecolor/src/color32.rs @@ -1,4 +1,4 @@ -use crate::{Rgba, fast_round, linear_f32_from_linear_u8}; +use crate::{Rgba, fast_round, mul_frac_round}; /// This format is used for space-efficient color representation (32 bits). /// @@ -131,35 +131,10 @@ impl Color32 { /// but for transparent colors what you get back might be slightly different (rounding errors). #[inline] pub fn from_rgba_unmultiplied(r: u8, g: u8, b: u8, a: u8) -> Self { - use std::sync::OnceLock; - match a { - // common-case optimization: - 0 => Self::TRANSPARENT, - - // common-case optimization: - 255 => Self::from_rgb(r, g, b), - - a => { - static LOOKUP_TABLE: OnceLock> = OnceLock::new(); - let lut = LOOKUP_TABLE.get_or_init(|| { - (0..=u16::MAX) - .map(|i| { - let [value, alpha] = i.to_ne_bytes(); - fast_round(value as f32 * linear_f32_from_linear_u8(alpha)) - }) - .collect() - }); - - let [r, g, b] = - [r, g, b].map(|value| lut[usize::from(u16::from_ne_bytes([value, a]))]); - Self::from_rgba_premultiplied(r, g, b, a) - } - } + Self::from_rgba_unmultiplied_const(r, g, b, a) } - /// Same as [`Self::from_rgba_unmultiplied`], but can be used in a const context. - /// - /// It is slightly slower when operating on non-const data. + /// This is the same as [`Self::from_rgba_unmultiplied`], but for const contexts. #[inline] pub const fn from_rgba_unmultiplied_const(r: u8, g: u8, b: u8, a: u8) -> Self { match a { @@ -170,9 +145,9 @@ impl Color32 { 255 => Self::from_rgb(r, g, b), a => { - let r = fast_round(r as f32 * linear_f32_from_linear_u8(a)); - let g = fast_round(g as f32 * linear_f32_from_linear_u8(a)); - let b = fast_round(b as f32 * linear_f32_from_linear_u8(a)); + let r = mul_frac_round(r, a); + let g = mul_frac_round(g, a); + let b = mul_frac_round(b, a); Self::from_rgba_premultiplied(r, g, b, a) } } @@ -535,4 +510,14 @@ mod test { Color32::from_rgba_unmultiplied(255, 0, 0, 128) ); } + + #[test] + fn mul_frac_round_vs_old() { + for x in (0..=255u8).step_by(4) { + for a in (1..=255u8).step_by(4) { + let old = fast_round(x as f32 * crate::linear_f32_from_linear_u8(a)); + assert_eq!(old, mul_frac_round(x, a)); + } + } + } } diff --git a/crates/ecolor/src/lib.rs b/crates/ecolor/src/lib.rs index ea7cff6f7..02ea96c51 100644 --- a/crates/ecolor/src/lib.rs +++ b/crates/ecolor/src/lib.rs @@ -134,6 +134,17 @@ const fn fast_round(r: f32) -> u8 { (r + 0.5) as _ // rust does a saturating cast since 1.45 } +/// Compute val * (frac/255) with no floating point or divisions. +#[inline] +const fn mul_frac_round(val: u8, frac: u8) -> u8 { + // Treat this as a simple fixed point calculation + let p = (val as u16) * (frac as u16) + 128; + ((p + (p >> 8)) >> 8) as u8 + // Logic split out a bit more. + //let p = (val as u16) * (frac as u16) + 127; // + 127 to round or remove to truncate. + //return ((p + 1 + (p >> 8)) >> 8) as u8; +} + #[test] pub fn test_srgba_conversion() { for b in 0..=255 { From 00dc6e814b251bf34c77063ed7efcd1024f3646f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 21 Aug 2026 01:33:29 -0700 Subject: [PATCH 48/49] Make kittest's predictable texture filtering honor `TextureOptions::NEAREST` (#8441) --- crates/egui-wgpu/src/egui.wgsl | 15 ++++++- crates/egui-wgpu/src/renderer.rs | 44 +++++++++++++++++++ crates/egui_demo_lib/tests/misc.rs | 35 +++++++++++++++ .../snapshots/nearest_texture_filtering.png | 3 ++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png diff --git a/crates/egui-wgpu/src/egui.wgsl b/crates/egui-wgpu/src/egui.wgsl index 39210841b..5b4b8240f 100644 --- a/crates/egui-wgpu/src/egui.wgsl +++ b/crates/egui-wgpu/src/egui.wgsl @@ -98,20 +98,31 @@ fn vs_main( @group(1) @binding(0) var r_tex_color: texture_2d; @group(1) @binding(1) var r_tex_sampler: sampler; +/// 1 if the texture sampler uses nearest filtering, 0 if linear. +/// Only read when `predictable_texture_filtering` is on. +@group(1) @binding(2) var r_tex_nearest_filtering: u32; + fn sample_texture(in: VertexOutput) -> vec4 { if r_locals.predictable_texture_filtering == 0 { // Hardware filtering: fast, but varies across GPUs and drivers. return textureSample(r_tex_color, r_tex_sampler, in.tex_coord); } else { - // Manual bilinear filtering with four taps at pixel centers using textureLoad let texture_size = vec2(textureDimensions(r_tex_color, 0)); let texture_size_f = vec2(texture_size); + let max_coord = texture_size - vec2(1, 1); + + if r_tex_nearest_filtering == 1 { + // Nearest filtering: load the texel under the sample position. + let texel = clamp(vec2(in.tex_coord * texture_size_f), vec2(0, 0), max_coord); + return textureLoad(r_tex_color, texel, 0); + } + + // Manual bilinear filtering with four taps at pixel centers using textureLoad let pixel_coord = in.tex_coord * texture_size_f - 0.5; let pixel_fract = fract(pixel_coord); let pixel_floor = vec2(floor(pixel_coord)); // Manual texture clamping - let max_coord = texture_size - vec2(1, 1); let p00 = clamp(pixel_floor + vec2(0, 0), vec2(0, 0), max_coord); let p10 = clamp(pixel_floor + vec2(1, 0), vec2(0, 0), max_coord); let p01 = clamp(pixel_floor + vec2(0, 1), vec2(0, 0), max_coord); diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index de8808de3..267363591 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -245,6 +245,12 @@ pub struct Renderer { uniform_bind_group: wgpu::BindGroup, texture_bind_group_layout: wgpu::BindGroupLayout, + /// Uniform buffers each holding a single `u32`: + /// 1 if the texture sampler uses nearest filtering, 0 otherwise. + /// Indexed by that flag value. + /// Read by the shader when `predictable_texture_filtering` is on. + nearest_filtering_flag_buffers: [wgpu::Buffer; 2], + /// Map of egui texture IDs to textures and their associated bindgroups (texture view + /// sampler). The texture may be None if the `TextureId` is just a handle to a user-provided /// sampler. @@ -347,10 +353,28 @@ impl Renderer { ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + has_dynamic_offset: false, + min_binding_size: NonZeroU64::new(core::mem::size_of::() as _), + ty: wgpu::BufferBindingType::Uniform, + }, + count: None, + }, ], }) }; + let nearest_filtering_flag_buffers = [0_u32, 1_u32].map(|flag| { + device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("egui_nearest_filtering_flag_{flag}")), + contents: bytemuck::bytes_of(&flag), + usage: wgpu::BufferUsages::UNIFORM, + }) + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("egui_pipeline_layout"), bind_group_layouts: &[ @@ -458,6 +482,7 @@ impl Renderer { previous_uniform_buffer_content: UniformBuffer::zeroed(), uniform_bind_group, texture_bind_group_layout, + nearest_filtering_flag_buffers, textures: HashMap::default(), next_user_texture_id: 0, samplers: HashMap::default(), @@ -709,6 +734,8 @@ impl Renderer { }; let bind_group = bind_group.unwrap_or_else(|| { + let nearest = + image_delta.options.magnification == epaint::textures::TextureFilter::Nearest; let sampler = self .samplers .entry(image_delta.options) @@ -727,6 +754,11 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.nearest_filtering_flag_buffers[usize::from(nearest)] + .as_entire_binding(), + }, ], }) }); @@ -829,6 +861,7 @@ impl Renderer { ) -> epaint::TextureId { profiling::function_scope!(); + let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest; let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, ..sampler_descriptor @@ -846,6 +879,11 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.nearest_filtering_flag_buffers[usize::from(nearest)] + .as_entire_binding(), + }, ], }); @@ -885,6 +923,7 @@ impl Renderer { .get_mut(&id) .expect("Tried to update a texture that has not been allocated yet."); + let nearest = sampler_descriptor.mag_filter == wgpu::FilterMode::Nearest; let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, ..sampler_descriptor @@ -902,6 +941,11 @@ impl Renderer { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, + wgpu::BindGroupEntry { + binding: 2, + resource: self.nearest_filtering_flag_buffers[usize::from(nearest)] + .as_entire_binding(), + }, ], }); diff --git a/crates/egui_demo_lib/tests/misc.rs b/crates/egui_demo_lib/tests/misc.rs index 427710629..07b61215a 100644 --- a/crates/egui_demo_lib/tests/misc.rs +++ b/crates/egui_demo_lib/tests/misc.rs @@ -1,6 +1,41 @@ use egui::{Color32, accesskit::Role}; use egui_kittest::{Harness, kittest::Queryable as _}; +/// Textures with [`egui::TextureOptions::NEAREST`] should render crisp, +/// also with kittest's predictable texture filtering. +#[test] +fn test_nearest_texture_filtering() { + let mut texture: Option = None; + let mut harness = Harness::builder() + .with_size(egui::Vec2::new(80.0, 48.0)) + .build_ui(move |ui| { + let texture = texture.get_or_insert_with(|| { + let pixels = [ + Color32::BLACK, + Color32::WHITE, + Color32::BLACK, + Color32::WHITE, + Color32::WHITE, + Color32::BLACK, + Color32::WHITE, + Color32::BLACK, + ]; + let image = egui::ColorImage::new([4, 2], pixels.to_vec()); + ui.ctx() + .load_texture("checkerboard", image, egui::TextureOptions::NEAREST) + }); + + let rect = egui::Rect::from_min_size(egui::pos2(8.0, 8.0), egui::vec2(64.0, 32.0)); + let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)); + ui.painter().add( + egui::epaint::RectShape::filled(rect, 0, Color32::WHITE) + .with_texture(texture.id(), uv), + ); + }); + harness.run(); + harness.snapshot("nearest_texture_filtering"); +} + #[test] fn test_kerning() { let mut results = egui_kittest::SnapshotResults::new(); diff --git a/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png new file mode 100644 index 000000000..fedbc8f13 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/nearest_texture_filtering.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72e133c853ab933d37711665ce1278f8f23ce35b8a74a46559f269d0dfd0bf40 +size 353 From f5c9373e263b09ccb74e78170b767c69120d8029 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 21 Aug 2026 01:33:54 -0700 Subject: [PATCH 49/49] Add `epaint::RoundedRect` primitive (#8440) --- crates/epaint/src/lib.rs | 2 + crates/epaint/src/rounded_rect.rs | 152 ++++++++++++++++++++++++++++++ crates/epaint/src/shadow.rs | 12 ++- crates/epaint/src/tessellator.rs | 28 +++--- 4 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 crates/epaint/src/rounded_rect.rs diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index bff5c79a3..c3e7a5b74 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -33,6 +33,7 @@ mod margin; mod margin_f32; mod mesh; pub mod mutex; +mod rounded_rect; mod shadow; pub mod shape_transform; mod shapes; @@ -56,6 +57,7 @@ pub use self::{ margin::Margin, margin_f32::*, mesh::{Mesh, Mesh16, Vertex}, + rounded_rect::RoundedRect, shadow::Shadow, shapes::{ CircleShape, CubicBezierShape, EllipseShape, PaintCallback, PaintCallbackInfo, PathShape, diff --git a/crates/epaint/src/rounded_rect.rs b/crates/epaint/src/rounded_rect.rs new file mode 100644 index 000000000..e5023dcd0 --- /dev/null +++ b/crates/epaint/src/rounded_rect.rs @@ -0,0 +1,152 @@ +use emath::{Pos2, Rect, Vec2, vec2}; + +use crate::CornerRadiusF32; + +/// A rectangle geometry with rounded corners. +/// +/// Not a painting primitive. For that, see [`crate::RectShape`]. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct RoundedRect { + rect: Rect, + corner_radius: CornerRadiusF32, +} + +impl RoundedRect { + /// The corner radius is clamped to half the size of the rectangle. + #[inline] + pub fn new(rect: Rect, corner_radius: impl Into) -> Self { + let max_radius = 0.5 * rect.size().min_elem(); + Self { + rect, + corner_radius: corner_radius.into().at_most(max_radius).at_least(0.0), + } + } + + #[inline] + pub fn rect(&self) -> Rect { + self.rect + } + + #[inline] + pub fn corner_radius(&self) -> CornerRadiusF32 { + self.corner_radius + } + + /// Split into the rectangle and the corner radius. + #[inline] + pub fn into_parts(self) -> (Rect, CornerRadiusF32) { + let Self { + rect, + corner_radius, + } = self; + (rect, corner_radius) + } + + /// Expand the rectangle and the corner radii by the given amount. + #[inline] + #[must_use] + pub fn expand(self, amount: f32) -> Self { + Self::new( + self.rect.expand(amount), + self.corner_radius + CornerRadiusF32::same(amount), + ) + } + + /// Clamp the given position to lie within this rounded rectangle. + /// + /// Positions in the corner regions are projected onto the corner arcs. + pub fn clamp_pos(&self, pos: Pos2) -> Pos2 { + let Self { + rect, + corner_radius, + } = *self; + let pos = rect.clamp(pos); + let corners = [ + (corner_radius.nw, vec2(-1.0, -1.0)), + (corner_radius.ne, vec2(1.0, -1.0)), + (corner_radius.sw, vec2(-1.0, 1.0)), + (corner_radius.se, vec2(1.0, 1.0)), + ]; + for (radius, dir) in corners { + let arc_center = rect.center() + dir * (rect.size() / 2.0 - Vec2::splat(radius)); + let offset = pos - arc_center; + if 0.0 < offset.x * dir.x && 0.0 < offset.y * dir.y && radius < offset.length() { + return arc_center + (radius / offset.length()) * offset; + } + } + pos + } +} + +impl From for RoundedRect { + #[inline] + fn from(rect: Rect) -> Self { + Self { + rect, + corner_radius: CornerRadiusF32::ZERO, + } + } +} + +#[cfg(test)] +mod tests { + use emath::pos2; + + use super::*; + + #[test] + fn clamp_pos() { + let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)); + let rounded = RoundedRect::new( + rect, + CornerRadiusF32 { + nw: 10.0, + ne: 0.0, + sw: 0.0, + se: 20.0, + }, + ); + + // Interior point is untouched: + assert_eq!(rounded.clamp_pos(pos2(50.0, 50.0)), pos2(50.0, 50.0)); + + // Sharp corner is untouched: + assert_eq!(rounded.clamp_pos(pos2(100.0, 0.0)), pos2(100.0, 0.0)); + + // Outside the rect is clamped to the edge: + assert_eq!(rounded.clamp_pos(pos2(-10.0, 50.0)), pos2(0.0, 50.0)); + + // Rounded corner is projected onto the arc: + let clamped = rounded.clamp_pos(pos2(0.0, 0.0)); + let arc_center = pos2(10.0, 10.0); + assert!((clamped - arc_center).length() - 10.0 < 0.001); + let expected = 10.0 - 10.0 / core::f32::consts::SQRT_2; + assert!((clamped - pos2(expected, expected)).length() < 0.001); + + // Point on the arc stays put: + assert_eq!(rounded.clamp_pos(pos2(10.0, 0.0)), pos2(10.0, 0.0)); + } + + #[test] + fn expand() { + let rect = Rect::from_min_max(pos2(10.0, 10.0), pos2(90.0, 90.0)); + let expanded = RoundedRect::new(rect, 20.0).expand(10.0); + assert_eq!( + expanded.rect(), + Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)) + ); + assert_eq!(expanded.corner_radius(), CornerRadiusF32::same(30.0)); + } + + #[test] + fn oversized_radius_is_clamped() { + // A radius larger than half the rect is clamped, like in the tessellator: + let rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)); + assert_eq!(RoundedRect::new(rect, 200.0), RoundedRect::new(rect, 50.0)); + assert_eq!( + RoundedRect::new(rect, 200.0).corner_radius(), + CornerRadiusF32::same(50.0) + ); + } +} diff --git a/crates/epaint/src/shadow.rs b/crates/epaint/src/shadow.rs index 251b57b7a..11dd3f1d1 100644 --- a/crates/epaint/src/shadow.rs +++ b/crates/epaint/src/shadow.rs @@ -1,4 +1,4 @@ -use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, Vec2}; +use crate::{Color32, CornerRadius, MarginF32, Rect, RectShape, RoundedRect, Vec2}; /// The color and fuzziness of a fuzzy shape. /// @@ -56,10 +56,12 @@ impl Shadow { } = *self; let [offset_x, offset_y] = offset; - let rect = rect - .translate(Vec2::new(offset_x as _, offset_y as _)) - .expand(spread as _); - let corner_radius = corner_radius.into() + CornerRadius::from(spread); + let (rect, corner_radius) = RoundedRect::new( + rect.translate(Vec2::new(offset_x as _, offset_y as _)), + corner_radius.into(), + ) + .expand(f32::from(spread)) + .into_parts(); RectShape::filled(rect, corner_radius, color).with_blur_width(blur as _) } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a9784f48b..6f3a3698b 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -11,8 +11,8 @@ use emath::{ use crate::{ CircleShape, ClippedPrimitive, ClippedShape, Color32, CornerRadiusF32, CubicBezierShape, - EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, Shape, Stroke, - StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke, + EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape, RectShape, RoundedRect, Shape, + Stroke, StrokeKind, TextShape, TextureId, Vertex, color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, }; @@ -536,18 +536,19 @@ impl Path { pub mod path { //! Helpers for constructing paths - use crate::CornerRadiusF32; - use emath::{Pos2, Rect, pos2}; + use crate::{CornerRadiusF32, RoundedRect}; + use emath::{Pos2, pos2}; /// overwrites existing points - pub fn rounded_rectangle(path: &mut Vec, rect: Rect, cr: CornerRadiusF32) { + pub fn rounded_rectangle(path: &mut Vec, rounded_rect: RoundedRect) { path.clear(); + // The corner radius is already clamped to half the rect size by `RoundedRect`: + let (rect, cr) = rounded_rect.into_parts(); + let min = rect.min; let max = rect.max; - let cr = clamp_corner_radius(cr, rect); - if cr == CornerRadiusF32::ZERO { path.reserve(4); path.push(pos2(min.x, min.y)); // left top @@ -633,14 +634,6 @@ pub mod path { path.extend(quadrant_vertices.iter().map(|&n| center + radius * n)); } } - - // Ensures the radius of each corner is within a valid range - fn clamp_corner_radius(cr: CornerRadiusF32, rect: Rect) -> CornerRadiusF32 { - let half_width = rect.width() * 0.5; - let half_height = rect.height() * 0.5; - let max_cr = half_width.min(half_height); - cr.at_most(max_cr).at_least(0.0) - } } // ---------------------------------------------------------------------------- @@ -1938,7 +1931,10 @@ impl Tessellator { let path = &mut self.scratchpad_path; path.clear(); - path::rounded_rectangle(&mut self.scratchpad_points, rect, corner_radius); + path::rounded_rectangle( + &mut self.scratchpad_points, + RoundedRect::new(rect, corner_radius), + ); // Apply rotation if angle is non-zero if angle != 0.0 {