From d28929ec7225205100590fce2836f885bc9ba688 Mon Sep 17 00:00:00 2001 From: Vitaly Kravchenko Date: Thu, 23 Jul 2026 15:58:26 +0100 Subject: [PATCH 1/4] Rerun `sizing_pass` when reopening popup (#8315) * Closes * [x] I have followed the instructions in the PR template ## Summary - Recalculate a menu popup's cached `Area` size when it reopens. - Preserve cached sizing for continuously open menus and leave tooltips and general popups unchanged. - Add a headless regression test covering a wider item added while the menu is closed. ## Root cause `Area` keeps its cached size after a menu closes. When that menu reopened with wider content, the cached width constrained the new item and caused it to wrap instead of allowing the popup to grow. The fix requests the same invisible sizing pass used for a first-open `Area` whenever a menu was not open during the previous frame. ## User impact Menus now expand to fit newly added wider items after reopening. Existing wrapping, explicit-width, alignment, screen-constraining, and continuously open menu behavior remain unchanged. ## Validation - `cargo test -p egui_kittest --test menu` - `cargo check -p egui` - `cargo fmt --all -- --check` --- crates/egui/src/containers/popup.rs | 1 + crates/egui_kittest/tests/popup.rs | 67 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 2a9335ede..080c00bd1 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -571,6 +571,7 @@ impl<'a> Popup<'a> { .fixed_pos(anchor) .sense(sense) .layout(layout) + .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 a8d3bcc9d..4dbe33909 100644 --- a/crates/egui_kittest/tests/popup.rs +++ b/crates/egui_kittest/tests/popup.rs @@ -1,5 +1,72 @@ +use egui::{Align, Layout, Popup}; +use egui_kittest::Harness; use kittest::Queryable as _; +#[test] +fn reopened_popup_resizes_for_wider_items() { + const POPUP_BUTTON: &str = "Dynamic popup"; + const SHORT_ITEM: &str = "Short item"; + const WIDE_ITEM: &str = "Newly added item with a much wider label"; + + #[derive(Default)] + struct State { + open: bool, + show_wide_item: bool, + } + + 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); + if response.clicked() { + state.open = !state.open; + } + + Popup::from_response(&response) + .open(state.open) + .layout(Layout::top_down_justified(Align::Min)) + .show(|ui| { + _ = ui.selectable_label(false, SHORT_ITEM); + _ = ui.selectable_label(false, "Another short item"); + if state.show_wide_item { + _ = ui.selectable_label(false, WIDE_ITEM); + } + }); + }, + State::default(), + ); + + harness.get_by_label(POPUP_BUTTON).click(); + harness.run(); + let initial_row_size = harness.get_by_label(SHORT_ITEM).rect().size(); + + harness.get_by_label(POPUP_BUTTON).click(); + harness.run(); + assert!(harness.query_by_label(SHORT_ITEM).is_none()); + + harness.state_mut().show_wide_item = true; + harness.run(); + harness.get_by_label(POPUP_BUTTON).click(); + harness.run(); + + let reopened_row_size = harness.get_by_label(SHORT_ITEM).rect().size(); + let wide_row_size = harness.get_by_label(WIDE_ITEM).rect().size(); + + assert!( + reopened_row_size.x > initial_row_size.x, + "reopened row width ({}) did not grow beyond its initial width ({})", + reopened_row_size.x, + initial_row_size.x + ); + assert!( + wide_row_size.y <= initial_row_size.y + 0.5, + "new row height ({}) exceeds the single-line row height ({})", + wide_row_size.y, + initial_row_size.y + ); +} + #[test] fn test_interactive_tooltip() { struct State { From b98f4b40344e2a815db32388510d2c0ad792d109 Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:23:06 -0700 Subject: [PATCH 2/4] Ignore RUSTSEC-2026-0206 (rustybuzz unmaintained) in cargo-deny (#8334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - [x] I have followed the instructions in the PR template Fixes the 12 failing `cargo-deny` CI checks on `main` (introduced by #8289). ### Root cause `rustybuzz` was flagged as unmaintained by [RUSTSEC-2026-0206](https://rustsec.org/advisories/RUSTSEC-2026-0206), reported on 2026-07-12 — after the previous cargo-deny fix (#8300) was merged on 2026-07-07. The advisory fires on all 12 cargo-deny target triples. `rustybuzz` is pulled in transitively via `resvg` → `usvg`, which `egui_extras` uses for SVG support. ### Why ignore (not fix)? - `resvg` is pinned at `0.45.1` and cannot be bumped: `0.47` needs `tiny-skia 0.12`, but `winit 0.30`'s `sctk-adwaita` is stuck on `tiny-skia 0.11` (see comment in `Cargo.toml` line 128). - The advisory's recommended replacement is [`harfrust`](https://github.com/harfbuzz/harfrust), which `resvg` has not adopted yet. - This is the same pattern used for the other transitively-unmaintained advisories already in the ignore list (`ttf-parser` via winit, `quick-xml` via accesskit/wayland, `bincode`, `yaml-rust`). ### Change Added `RUSTSEC-2026-0206` to the `[advisories] ignore` list in `deny.toml` with an explanatory comment. ## Test plan - [x] `cargo deny check advisories` — passes (`advisories ok`) - [x] `cargo deny check` — all sections pass (`advisories ok, bans ok, licenses ok, sources ok`) --------- Co-authored-by: Lucas Meurer --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index dc1e50be1..ff6c8e2ac 100644 --- a/deny.toml +++ b/deny.toml @@ -36,6 +36,7 @@ ignore = [ "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 ] [bans] From e6eb00a31c7089d4458c55fcbe5f1253311a7176 Mon Sep 17 00:00:00 2001 From: Davy <95214375+thedavidweng@users.noreply.github.com> Date: Fri, 24 Jul 2026 01:24:33 -0700 Subject: [PATCH 3/4] Fix comment style: add space after // (#8333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - [x] I have followed the instructions in the PR template Fixes comment-style violations of the `CONTRIBUTING.md` code style rule: > `// Comment like this.` and not `//like this` ### Files changed - `crates/epaint/src/shapes/bezier_shape.rs`: `//temporary solution` → `// temporary solution`, and 22× `//add the start point` → `// add the start point` - `crates/egui/src/animation_manager.rs`: `//start new animation…` → `// start new animation…` - `crates/eframe/src/web/web_painter_wgpu.rs`: `//create_new.instance_descriptor…` → `// create_new.instance_descriptor…` No code logic changes — only adding the missing space after `//` in inline and standalone comments. ## Test plan - [x] `cargo fmt --check` - [x] `cargo clippy -p epaint -p egui` - [x] `cargo test -p epaint --lib` --- crates/eframe/src/web/web_painter_wgpu.rs | 2 +- crates/egui/src/animation_manager.rs | 2 +- crates/epaint/src/shapes/bezier_shape.rs | 46 +++++++++++------------ 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index 1728d7adc..0665d99f9 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -90,7 +90,7 @@ impl WebPainterWgpu { && create_new.display_handle.is_none() { // Force WebGL, useful for quick & dirty testing: - //create_new.instance_descriptor.backends = wgpu::Backends::GL; + // create_new.instance_descriptor.backends = wgpu::Backends::GL; create_new.display_handle = Some(Box::new(WebDisplay)); } diff --git a/crates/egui/src/animation_manager.rs b/crates/egui/src/animation_manager.rs index 50f97e992..4dc21df09 100644 --- a/crates/egui/src/animation_manager.rs +++ b/crates/egui/src/animation_manager.rs @@ -95,7 +95,7 @@ impl AnimationManager { anim.from_value..=anim.to_value, ); if anim.to_value != value { - anim.from_value = current_value; //start new animation from current position of playing animation + anim.from_value = current_value; // start new animation from current position of playing animation anim.to_value = value; anim.toggle_time = input.time; } diff --git a/crates/epaint/src/shapes/bezier_shape.rs b/crates/epaint/src/shapes/bezier_shape.rs index b20c56691..dfeaae5ec 100644 --- a/crates/epaint/src/shapes/bezier_shape.rs +++ b/crates/epaint/src/shapes/bezier_shape.rs @@ -86,7 +86,7 @@ impl CubicBezierShape { /// Logical bounding rectangle (ignoring stroke width) pub fn logical_bounding_rect(&self) -> Rect { - //temporary solution + // temporary solution let (mut min_x, mut max_x) = if self.points[0].x < self.points[3].x { (self.points[0].x, self.points[3].x) } else { @@ -793,7 +793,7 @@ mod tests { assert!((bbox.max.x - 180.0).abs() < 0.01); assert!((bbox.max.y - 170.0).abs() < 0.01); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); @@ -817,7 +817,7 @@ mod tests { assert!((bbox.max.x - 130.42).abs() < 0.01); assert!((bbox.max.y - 170.0).abs() < 0.01); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); @@ -837,28 +837,28 @@ mod tests { fill: Default::default(), stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 9); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 25); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 77); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); @@ -938,35 +938,35 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 10); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.5, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 13); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 28); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 83); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); @@ -988,7 +988,7 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1007,7 +1007,7 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1026,7 +1026,7 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1045,7 +1045,7 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1064,7 +1064,7 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1083,7 +1083,7 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); @@ -1100,34 +1100,34 @@ mod tests { stroke: Default::default(), }; - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(1.0, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 9); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.5, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 11); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.1, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 24); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.01, &mut |pos, _t| { result.push(pos); }); assert_eq!(result.len(), 72); - let mut result = vec![curve.points[0]]; //add the start point + let mut result = vec![curve.points[0]]; // add the start point curve.for_each_flattened_with_t(0.001, &mut |pos, _t| { result.push(pos); }); From 2cb071f7f6d71e0f888ba31bec9b3eb5ed5428fe Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 27 Jul 2026 16:17:22 +0200 Subject: [PATCH 4/4] Remove `Modifiers` from `RawInput` and make it a `egui::Event` (#8336) - needs #8335 Previously it was impossible to correctly set modifiers via egui inspection, since that only passes egui::Event and egui::Event had no way to generally express modifiers. Now modifiers are passed to egui as a Event and not as a field of `RawInput`. --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/web/backend.rs | 20 ++++++++++- crates/eframe/src/web/events.rs | 20 +++++------ crates/egui-winit/src/lib.rs | 45 ++++++++++++++++--------- crates/egui/src/data/input/event.rs | 3 ++ crates/egui/src/data/input/raw_input.rs | 11 +----- crates/egui/src/input_state/mod.rs | 7 +++- crates/egui_kittest/src/lib.rs | 28 ++++++--------- crates/egui_kittest/src/node.rs | 41 +++++++++++++--------- 8 files changed, 106 insertions(+), 69 deletions(-) diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index e2724fc49..9c092b68d 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -20,6 +20,12 @@ pub(crate) struct WebInput { /// Helps to track the delta rotation from gesture events pub accumulated_rotation: f32, + /// The last modifier state we sent to egui. + /// + /// The web has no dedicated modifier event, so we derive the state from each DOM event and + /// emit [`egui::Event::ModifiersChanged`] when it changes (see [`Self::set_modifiers`]). + pub modifiers: egui::Modifiers, + /// The raw input to `egui`. pub raw: egui::RawInput, } @@ -53,11 +59,23 @@ impl WebInput { } // log::debug!("on_web_page_focus_change: {focused}"); - self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: + self.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab: self.raw.focused = focused; self.raw.events.push(egui::Event::WindowFocused(focused)); self.primary_touch = None; } + + /// Update the modifier state, emitting [`egui::Event::ModifiersChanged`] if it changed. + /// + /// Call before pushing the DOM event itself so egui sees the new modifier state first. + pub fn set_modifiers(&mut self, modifiers: egui::Modifiers) { + if self.modifiers != modifiers { + self.modifiers = modifiers; + self.raw + .events + .push(egui::Event::ModifiersChanged(modifiers)); + } + } } // ---------------------------------------------------------------------------- diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index f6ff078c5..308c268a6 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -196,7 +196,7 @@ pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) } let modifiers = modifiers_from_kb_event(&event); - runner.input.raw.modifiers = modifiers; + runner.input.set_modifiers(modifiers); let key = event.key(); let egui_key = translate_key(&key); @@ -287,7 +287,7 @@ fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV #[expect(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener` pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) { let modifiers = modifiers_from_kb_event(&event); - runner.input.raw.modifiers = modifiers; + runner.input.set_modifiers(modifiers); let mut should_stop_propagation = true; @@ -535,11 +535,11 @@ fn install_pointerdown(runner_ref: &WebRunner, target: &EventTarget) -> Result<( "pointerdown", |event: web_sys::PointerEvent, runner: &mut AppRunner| { let modifiers = modifiers_from_mouse_event(&event); - runner.input.raw.modifiers = modifiers; + runner.input.set_modifiers(modifiers); let mut should_stop_propagation = true; if let Some(button) = button_from_mouse_event(&event) { let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); - let modifiers = runner.input.raw.modifiers; + let modifiers = runner.input.modifiers; let egui_event = egui::Event::PointerButton { pos, button, @@ -572,7 +572,7 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), "pointerup", |event: web_sys::PointerEvent, runner| { let modifiers = modifiers_from_mouse_event(&event); - runner.input.raw.modifiers = modifiers; + runner.input.set_modifiers(modifiers); let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); @@ -581,7 +581,7 @@ fn install_pointerup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), egui::pos2(event.client_x() as f32, event.client_y() as f32), ) && let Some(button) = button_from_mouse_event(&event) { - let modifiers = runner.input.raw.modifiers; + let modifiers = runner.input.modifiers; let egui_event = egui::Event::PointerButton { pos, button, @@ -647,7 +647,7 @@ fn is_interested_in_pointer_event(runner: &AppRunner, pos: egui::Pos2) -> bool { fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> { runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| { let modifiers = modifiers_from_mouse_event(&event); - runner.input.raw.modifiers = modifiers; + runner.input.set_modifiers(modifiers); let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx()); @@ -705,7 +705,7 @@ fn install_touchstart(runner_ref: &WebRunner, target: &EventTarget) -> Result<() pos, button: egui::PointerButton::Primary, pressed: true, - modifiers: runner.input.raw.modifiers, + modifiers: runner.input.modifiers, }; should_stop_propagation = (runner.web_options.should_stop_propagation)(&egui_event); should_prevent_default = (runner.web_options.should_prevent_default)(&egui_event); @@ -770,7 +770,7 @@ fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), pos, button: egui::PointerButton::Primary, pressed: false, - modifiers: runner.input.raw.modifiers, + modifiers: runner.input.modifiers, }; should_stop_propagation &= (runner.web_options.should_stop_propagation)(&egui_event); should_prevent_default &= (runner.web_options.should_prevent_default)(&egui_event); @@ -832,7 +832,7 @@ fn install_wheel(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsV let modifiers = modifiers_from_wheel_event(&event); - let egui_event = if modifiers.ctrl && !runner.input.raw.modifiers.ctrl { + let egui_event = if modifiers.ctrl && !runner.input.modifiers.ctrl { // The browser is saying the ctrl key is down, but it isn't _really_. // This happens on pinch-to-zoom on multitouch trackpads // egui will treat ctrl+scroll as zoom, so it all works. diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 908ac8ff3..d17428adb 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -84,6 +84,13 @@ pub struct State { viewport_id: ViewportId, start_time: web_time::Instant, egui_input: egui::RawInput, + + /// The current modifier state. + /// + /// We keep a copy so we can stamp + /// it onto per-event `modifiers` fields and emit [`egui::Event::ModifiersChanged`]. + modifiers: egui::Modifiers, + pointer_pos_in_points: Option, any_pointer_button_down: bool, current_cursor_icon: Option, @@ -146,6 +153,7 @@ impl State { .unwrap_or_else(web_time::Instant::now), egui_ctx, egui_input, + modifiers: egui::Modifiers::default(), pointer_pos_in_points: None, any_pointer_button_down: false, current_cursor_icon: None, @@ -422,6 +430,10 @@ impl State { }; self.egui_input.focused = focused; + if !focused { + // Avoid sticky modifiers when focus is lost (egui clears its own copy too). + self.modifiers = egui::Modifiers::default(); + } self.egui_input .events .push(egui::Event::WindowFocused(focused)); @@ -473,16 +485,20 @@ impl State { let shift = state.shift_key(); let super_ = state.super_key(); - self.egui_input.modifiers.alt = alt; - self.egui_input.modifiers.ctrl = ctrl; - self.egui_input.modifiers.shift = shift; - self.egui_input.modifiers.mac_cmd = cfg!(target_os = "macos") && super_; - self.egui_input.modifiers.command = if cfg!(target_os = "macos") { + self.modifiers.alt = alt; + self.modifiers.ctrl = ctrl; + self.modifiers.shift = shift; + self.modifiers.mac_cmd = cfg!(target_os = "macos") && super_; + self.modifiers.command = if cfg!(target_os = "macos") { super_ } else { ctrl }; + self.egui_input + .events + .push(egui::Event::ModifiersChanged(self.modifiers)); + EventResponse { repaint: true, consumed: false, @@ -541,7 +557,7 @@ impl State { unit: egui::MouseWheelUnit::Point, delta: Vec2::new(delta.x, delta.y) / pixels_per_point, phase: to_egui_touch_phase(*phase), - modifiers: self.egui_input.modifiers, + modifiers: self.modifiers, }); EventResponse { repaint: true, @@ -790,7 +806,7 @@ impl State { pos, button, pressed, - modifiers: self.egui_input.modifiers, + modifiers: self.modifiers, }); if self.simulate_touch_screen { @@ -937,7 +953,7 @@ impl State { ), }; let phase = to_egui_touch_phase(phase); - let modifiers = self.egui_input.modifiers; + let modifiers = self.modifiers; self.egui_input.events.push(egui::Event::MouseWheel { unit, delta, @@ -998,13 +1014,13 @@ impl State { // See also: https://github.com/emilk/egui/issues/3653 if let Some(active_key) = logical_key.or(physical_key) { if pressed { - if is_cut_command(self.egui_input.modifiers, active_key) { + if is_cut_command(self.modifiers, active_key) { self.egui_input.events.push(egui::Event::Cut); return; - } else if is_copy_command(self.egui_input.modifiers, active_key) { + } else if is_copy_command(self.modifiers, active_key) { self.egui_input.events.push(egui::Event::Copy); return; - } else if is_paste_command(self.egui_input.modifiers, active_key) { + } else if is_paste_command(self.modifiers, active_key) { if let Some(contents) = self.clipboard.get() { let contents = contents.replace("\r\n", "\n"); if !contents.is_empty() { @@ -1020,7 +1036,7 @@ impl State { physical_key, pressed, repeat: false, // egui will fill this in for us! - modifiers: self.egui_input.modifiers, + modifiers: self.modifiers, }); } @@ -1036,9 +1052,8 @@ impl State { // We need to ignore these characters that are side-effects of commands. // Also make sure the key is pressed (not released). On Linux, text might // contain some data even when the key is released. - let is_cmd = self.egui_input.modifiers.ctrl - || self.egui_input.modifiers.command - || self.egui_input.modifiers.mac_cmd; + let is_cmd = + self.modifiers.ctrl || self.modifiers.command || self.modifiers.mac_cmd; if pressed && !is_cmd { self.egui_input .events diff --git a/crates/egui/src/data/input/event.rs b/crates/egui/src/data/input/event.rs index 117a200b6..9641d081d 100644 --- a/crates/egui/src/data/input/event.rs +++ b/crates/egui/src/data/input/event.rs @@ -69,6 +69,9 @@ pub enum Event { modifiers: Modifiers, }, + /// The set of held modifier keys changed. + ModifiersChanged(Modifiers), + /// The mouse or touch moved to a new place. PointerMoved(Pos2), diff --git a/crates/egui/src/data/input/raw_input.rs b/crates/egui/src/data/input/raw_input.rs index 2ba2caada..b9fc6e66a 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, Modifiers, SafeAreaInsets, ViewportInfo}; +use super::{DroppedFile, Event, HoveredFile, SafeAreaInsets, ViewportInfo}; /// What the integrations provides to egui at the start of each frame. /// @@ -53,9 +53,6 @@ pub struct RawInput { /// Can safely be left at its default value. pub predicted_dt: f32, - /// Which modifier keys are down at the start of the frame? - pub modifiers: Modifiers, - /// In-order events received this frame. /// /// There is currently no way to know if egui handles a particular event, @@ -92,7 +89,6 @@ impl Default for RawInput { max_texture_side: None, time: None, predicted_dt: 1.0 / 60.0, - modifiers: Modifiers::default(), events: vec![], hovered_files: Default::default(), dropped_files: Default::default(), @@ -127,7 +123,6 @@ impl RawInput { max_texture_side: self.max_texture_side.take(), time: self.time, predicted_dt: self.predicted_dt, - modifiers: self.modifiers, events: std::mem::take(&mut self.events), hovered_files: self.hovered_files.clone(), dropped_files: std::mem::take(&mut self.dropped_files), @@ -145,7 +140,6 @@ impl RawInput { max_texture_side, time, predicted_dt, - modifiers, mut events, mut hovered_files, mut dropped_files, @@ -160,7 +154,6 @@ impl RawInput { self.max_texture_side = max_texture_side.or(self.max_texture_side); self.time = time; // use latest time self.predicted_dt = predicted_dt; // use latest dt - self.modifiers = modifiers; // use latest self.events.append(&mut events); self.hovered_files.append(&mut hovered_files); self.dropped_files.append(&mut dropped_files); @@ -179,7 +172,6 @@ impl RawInput { max_texture_side, time, predicted_dt, - modifiers, events, hovered_files, dropped_files, @@ -210,7 +202,6 @@ impl RawInput { ui.label("time: None"); } ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); - ui.label(format!("modifiers: {modifiers:#?}")); ui.label(format!("hovered_files: {}", hovered_files.len())); ui.label(format!("dropped_files: {}", dropped_files.len())); ui.label(format!("focused: {focused}")); diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 55c76bb06..36d6f9bc9 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -393,6 +393,7 @@ impl InputState { let pointer = self.pointer.begin_pass(time, &new, options); let mut keys_down = self.keys_down; + let mut modifiers = self.modifiers; let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor let mut rotation_radians = 0.0; @@ -429,6 +430,9 @@ impl InputState { *modifiers, ); } + Event::ModifiersChanged(new_modifiers) => { + modifiers = *new_modifiers; + } Event::Zoom(factor) => { zoom_factor_delta *= *factor; } @@ -442,6 +446,7 @@ impl InputState { // So we take the safe route and just clear all the keys and modifiers when // the app loses focus. keys_down.clear(); + modifiers = Modifiers::default(); } _ => {} } @@ -482,7 +487,7 @@ impl InputState { predicted_dt: new.predicted_dt, stable_dt, focused: new.focused, - modifiers: new.modifiers, + modifiers, keys_down, events: new.events.clone(), // TODO(emilk): remove clone() and use raw.events raw: new, diff --git a/crates/egui_kittest/src/lib.rs b/crates/egui_kittest/src/lib.rs index 3c859560d..4f691c8c4 100644 --- a/crates/egui_kittest/src/lib.rs +++ b/crates/egui_kittest/src/lib.rs @@ -251,14 +251,7 @@ impl<'a, State> Harness<'a, State> { self._step(false); } for event in events { - match event { - EventType::Event(event) => { - self.input.events.push(event); - } - EventType::Modifiers(modifiers) => { - self.input.modifiers = modifiers; - } - } + self.input.events.push(event); self._step(false); } } @@ -471,7 +464,7 @@ impl<'a, State> Harness<'a, State> { /// Queue an event to be processed in the next frame. pub fn event(&self, event: egui::Event) { - self.queued_events.lock().push(EventType::Event(event)); + self.queued_events.lock().push(event); } /// Queue an event with modifiers. @@ -479,15 +472,15 @@ impl<'a, State> Harness<'a, State> { /// Queues the modifiers to be pressed, then the event, then the modifiers to be released. pub fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) { let mut queue = self.queued_events.lock(); - queue.push(EventType::Modifiers(modifiers)); - queue.push(EventType::Event(event)); - queue.push(EventType::Modifiers(Modifiers::default())); + queue.push(egui::Event::ModifiersChanged(modifiers)); + queue.push(event); + queue.push(egui::Event::ModifiersChanged(Modifiers::default())); } fn modifiers(&self, modifiers: Modifiers) { self.queued_events .lock() - .push(EventType::Modifiers(modifiers)); + .push(egui::Event::ModifiersChanged(modifiers)); } pub fn key_down(&self, key: egui::Key) { @@ -735,10 +728,11 @@ impl<'a, State> Harness<'a, State> { /// The root node of the test harness. pub fn root(&self) -> Node<'_> { - Node { - accesskit_node: self.kittest.root(), - queue: &self.queued_events, - } + Node::new( + self.kittest.root(), + &self.queued_events, + self.ctx.pixels_per_point(), + ) } /// Spawn a real native eframe window running this harness's app, reusing its [`egui::Context`]. diff --git a/crates/egui_kittest/src/node.rs b/crates/egui_kittest/src/node.rs index 7e3161c09..729fda763 100644 --- a/crates/egui_kittest/src/node.rs +++ b/crates/egui_kittest/src/node.rs @@ -4,17 +4,13 @@ use egui::{Modifiers, PointerButton, Pos2, accesskit}; use kittest::{AccessKitNode, NodeT, debug_fmt_node}; use std::fmt::{Debug, Formatter}; -pub(crate) enum EventType { - Event(egui::Event), - Modifiers(Modifiers), -} - -pub(crate) type EventQueue = Mutex>; +pub type EventQueue = Mutex>; #[derive(Clone, Copy)] pub struct Node<'tree> { pub(crate) accesskit_node: AccessKitNode<'tree>, pub(crate) queue: &'tree EventQueue, + pub(crate) pixels_per_point: f32, } impl Debug for Node<'_> { @@ -29,20 +25,32 @@ impl<'tree> NodeT<'tree> for Node<'tree> { } fn new_related(&self, child_node: AccessKitNode<'tree>) -> Self { - Self { - queue: self.queue, - accesskit_node: child_node, - } + Self::new(child_node, self.queue, self.pixels_per_point) } } -impl Node<'_> { +impl<'tree> Node<'tree> { + /// Construct a new accesskit node + pub fn new( + accesskit_node: AccessKitNode<'tree>, + queue: &'tree EventQueue, + pixels_per_point: f32, + ) -> Self { + Self { + accesskit_node, + queue, + pixels_per_point, + } + } + fn event(&self, event: egui::Event) { - self.queue.lock().push(EventType::Event(event)); + self.queue.lock().push(event); } fn modifiers(&self, modifiers: Modifiers) { - self.queue.lock().push(EventType::Modifiers(modifiers)); + self.queue + .lock() + .push(egui::Event::ModifiersChanged(modifiers)); } pub fn hover(&self) { @@ -104,14 +112,17 @@ impl Node<'_> { )); } + /// This returns the rect in logical ui coordinates while the underlying [`accesskit::Node`] has it + /// in physical screen coordinates. pub fn rect(&self) -> egui::Rect { let rect = self .accesskit_node .bounding_box() .expect("Every egui node should have a rect"); + let ppp = self.pixels_per_point; egui::Rect { - min: Pos2::new(rect.x0 as f32, rect.y0 as f32), - max: Pos2::new(rect.x1 as f32, rect.y1 as f32), + min: Pos2::new(rect.x0 as f32 / ppp, rect.y0 as f32 / ppp), + max: Pos2::new(rect.x1 as f32 / ppp, rect.y1 as f32 / ppp), } }