From 654a2a974d1991fe1fcea81a4aaa17c770adee0d Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Wed, 27 May 2026 11:27:25 +0200 Subject: [PATCH 01/11] Bump version to 0.34.3 and update changelogs (#8207) --- .typos.toml | 1 + CHANGELOG.md | 4 +++ Cargo.lock | 32 ++++++++++++------------ Cargo.toml | 26 +++++++++---------- crates/ecolor/CHANGELOG.md | 4 +++ crates/eframe/CHANGELOG.md | 5 ++++ crates/egui-wgpu/CHANGELOG.md | 4 +++ crates/egui-winit/CHANGELOG.md | 4 +++ crates/egui_extras/CHANGELOG.md | 4 +++ crates/egui_glow/CHANGELOG.md | 4 +++ crates/egui_kittest/CHANGELOG.md | 4 +++ crates/emath/CHANGELOG.md | 4 +++ crates/epaint/CHANGELOG.md | 4 +++ crates/epaint_default_fonts/CHANGELOG.md | 4 +++ 14 files changed, 75 insertions(+), 29 deletions(-) diff --git a/.typos.toml b/.typos.toml index 97c54d657..8f2a34b6e 100644 --- a/.typos.toml +++ b/.typos.toml @@ -4,6 +4,7 @@ [default.extend-words] ime = "ime" # Input Method Editor +abou = "abou" # part of @AmmarAbouZor username nknown = "nknown" # part of @55nknown username isse = "isse" # part of @IsseW username tye = "tye" # part of @tye-exe username diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bae7a734..e7c9601d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +* Fix `ScrollArea::scroll_to_*` calls when `stick_to_bottom` is Active [#8033](https://github.com/emilk/egui/pull/8033) by [@AmmarAbouZor](https://github.com/AmmarAbouZor) + + ## 0.34.2 - 2026-05-04 ### ⭐ Added * Add regression test for O(n²) word boundary scan [#8077](https://github.com/emilk/egui/pull/8077) by [@hallyhaa](https://github.com/hallyhaa) diff --git a/Cargo.lock b/Cargo.lock index 94bfe892a..76ed5b660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1197,7 +1197,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.34.2" +version = "0.34.3" dependencies = [ "bytemuck", "cint", @@ -1209,7 +1209,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "bytemuck", @@ -1248,7 +1248,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.34.2" +version = "0.34.3" dependencies = [ "accesskit", "ahash", @@ -1269,7 +1269,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "bytemuck", @@ -1287,7 +1287,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.34.2" +version = "0.34.3" dependencies = [ "accesskit_winit", "arboard", @@ -1310,7 +1310,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.34.2" +version = "0.34.3" dependencies = [ "accesskit", "accesskit_consumer", @@ -1339,7 +1339,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.34.2" +version = "0.34.3" dependencies = [ "criterion", "document-features", @@ -1356,7 +1356,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "document-features", @@ -1376,7 +1376,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.34.2" +version = "0.34.3" dependencies = [ "bytemuck", "document-features", @@ -1393,7 +1393,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.34.2" +version = "0.34.3" dependencies = [ "dify", "document-features", @@ -1413,7 +1413,7 @@ dependencies = [ [[package]] name = "egui_tests" -version = "0.34.2" +version = "0.34.3" dependencies = [ "egui", "egui_extras", @@ -1443,7 +1443,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.34.2" +version = "0.34.3" dependencies = [ "bytemuck", "document-features", @@ -1541,7 +1541,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.34.2" +version = "0.34.3" dependencies = [ "ahash", "bytemuck", @@ -1570,7 +1570,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.34.2" +version = "0.34.3" [[package]] name = "equivalent" @@ -3545,7 +3545,7 @@ dependencies = [ [[package]] name = "popups" -version = "0.34.2" +version = "0.34.3" dependencies = [ "eframe", "env_logger", @@ -5893,7 +5893,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.34.2" +version = "0.34.3" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 3710f8cb3..1eb1d7737 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.92" -version = "0.34.2" +version = "0.34.3" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.34.2", path = "crates/emath", default-features = false } -ecolor = { version = "0.34.2", path = "crates/ecolor", default-features = false } -epaint = { version = "0.34.2", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.34.2", path = "crates/epaint_default_fonts" } -egui = { version = "0.34.2", path = "crates/egui", default-features = false } -egui-winit = { version = "0.34.2", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.34.2", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.34.2", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.34.2", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.34.2", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.34.2", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.34.2", path = "crates/eframe", default-features = false } +emath = { version = "0.34.3", path = "crates/emath", default-features = false } +ecolor = { version = "0.34.3", path = "crates/ecolor", default-features = false } +epaint = { version = "0.34.3", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.34.3", path = "crates/epaint_default_fonts" } +egui = { version = "0.34.3", path = "crates/egui", default-features = false } +egui-winit = { version = "0.34.3", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.34.3", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.34.3", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.34.3", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.34.3", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.34.3", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.34.3", path = "crates/eframe", default-features = false } accesskit = "0.24.0" accesskit_consumer = "0.35.0" diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 1e10de83b..e441bf982 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 9071754f2..faea2d407 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,11 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +* Default `app_id` to `app_name` on native [#8172](https://github.com/emilk/egui/pull/8172) by [@grtlr](https://github.com/grtlr) +* Add winit window access to `eframe::Frame` and `CreationContext` [#8205](https://github.com/emilk/egui/pull/8205) by [@emilk](https://github.com/emilk) + + ## 0.34.2 - 2026-05-04 * Document glow-only fields in `NativeOptions` [#8104](https://github.com/emilk/egui/pull/8104) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 2eda803fc..f36b8f539 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +* Fix random hangs by improving `wgpu::Surface` lifecycle handling [#8171](https://github.com/emilk/egui/pull/8171) by [@grtlr](https://github.com/grtlr) + + ## 0.34.2 - 2026-05-04 * Update to wgpu 29.0.1 [#8073](https://github.com/emilk/egui/pull/8073) by [@emilk](https://github.com/emilk) * Warn if using a software rasterizer [#8101](https://github.com/emilk/egui/pull/8101) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index 15c7c2d52..58cc9be9e 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index f2685a11b..45c1b9f56 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 32ed885cd..83cf74472 100644 --- a/crates/egui_glow/CHANGELOG.md +++ b/crates/egui_glow/CHANGELOG.md @@ -6,6 +6,10 @@ Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/emath/CHANGELOG.md b/crates/emath/CHANGELOG.md index bc7f428ba..1c69ee38b 100644 --- a/crates/emath/CHANGELOG.md +++ b/crates/emath/CHANGELOG.md @@ -6,6 +6,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index e6ccb10a1..7d0781d52 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 * Fix text layout bugs in wrapped texts [#8137](https://github.com/emilk/egui/pull/8137) by [@lucasmerlin](https://github.com/lucasmerlin) diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index ee3a39e22..35afd930b 100644 --- a/crates/epaint_default_fonts/CHANGELOG.md +++ b/crates/epaint_default_fonts/CHANGELOG.md @@ -5,6 +5,10 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.34.3 - 2026-05-27 +Nothing new + + ## 0.34.2 - 2026-05-04 Nothing new From db7559368dc38704194d48f13400a98c2cf09584 Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:32:40 +0900 Subject: [PATCH 02/11] Refactor scroll area fade painting logic (#8214) Reserve the scroll area before painting fades and update the fade painting logic. * Closes #8213 --- crates/egui/src/containers/scroll_area.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index f95a5c2bd..5e0c69936 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -1042,17 +1042,13 @@ impl ScrollArea { .inner; let (content_size, state) = prepared.end(ui); - let output = ScrollAreaOutput { + ScrollAreaOutput { inner, id, state, content_size, inner_rect, - }; - - paint_fade_areas(ui, &output); - - output + } } } @@ -1260,6 +1256,11 @@ impl Prepared { let scroll_style = ui.spacing().scroll; + // Reserve the scroll area before painting fades, because fade painting uses ui.min_rect(). + ui.advance_cursor_after_rect(outer_rect); + + paint_fade_areas_impl(ui, inner_rect, content_size, state.offset); + // Paint the bars: let scroll_bar_rect = scroll_bar_rect.unwrap_or(inner_rect); for d in 0..2 { @@ -1508,8 +1509,6 @@ impl Prepared { } } - ui.advance_cursor_after_rect(outer_rect); - if show_scroll_this_frame != state.show_scroll { ui.request_repaint(); } @@ -1551,7 +1550,7 @@ impl Prepared { /// Paint fade-out gradients at the top and/or bottom of a scroll area to /// indicate that more content is available beyond the visible region. -fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { +fn paint_fade_areas_impl(ui: &Ui, inner_rect: Rect, content_size: Vec2, offset: Vec2) { let crate::style::ScrollFadeStyle { strength, size: fade_size, @@ -1563,11 +1562,9 @@ fn paint_fade_areas(ui: &Ui, scroll_output: &ScrollAreaOutput) { let bg = ui.stack().bg_color(); - let offset = scroll_output.state.offset; - let overflow = scroll_output.content_size - scroll_output.inner_rect.size(); + let overflow = content_size - inner_rect.size(); - let paint_rect = scroll_output - .inner_rect + let paint_rect = inner_rect .intersect(ui.min_rect()) .expand(ui.visuals().clip_rect_margin); From 71c4ff3c337a08bee934f249463d6701bf76b420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Gyebn=C3=A1r?= Date: Fri, 5 Jun 2026 15:58:36 +0200 Subject: [PATCH 03/11] Fixes color picker hue drift at low alpha values (#8208) * [x] I have followed the instructions in the PR template At low apha values, premultiplied colors lose precision. This PR makes the color picker use unmultiplied colors internally. Before: https://github.com/user-attachments/assets/4617a355-daa9-4911-86e6-518ac6867014 After: https://github.com/user-attachments/assets/d9681b01-50d8-418e-b5a5-79b4bd1bbddf --- crates/egui/src/widgets/color_picker.rs | 35 ++++++++++++++++--- .../tests/snapshots/imageviewer.png | 4 +-- .../tessellation_test/Additive rectangle.png | 4 +-- .../snapshots/tessellation_test/Blurred.png | 4 +-- .../tessellation_test/Thin filled.png | 4 +-- 5 files changed, 39 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index e4dc95335..a85c2a7b4 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -85,7 +85,29 @@ pub fn show_color_at(painter: &Painter, color: Color32, rect: Rect) { } } -fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { +/// Show a color with background checkers to demonstrate transparency (if any). +fn show_srgba_unmultiplied(ui: &mut Ui, srgba: [u8; 4], desired_size: Vec2) -> Response { + let (rect, response) = ui.allocate_at_least(desired_size, Sense::hover()); + if ui.is_rect_visible(rect) { + show_srgba_unmultiplied_at(ui.painter(), srgba, rect); + } + response +} + +/// Show a color with background checkers to demonstrate transparency (if any). +fn show_srgba_unmultiplied_at(painter: &Painter, [r, g, b, a]: [u8; 4], rect: Rect) { + if a == 255 { + painter.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b)); + } else { + background_checkers(painter, rect); + let left = Rect::from_min_max(rect.left_top(), rect.center_bottom()); + let right = Rect::from_min_max(rect.center_top(), rect.right_bottom()); + painter.rect_filled(left, 0.0, Color32::from_rgba_unmultiplied(r, g, b, a)); + painter.rect_filled(right, 0.0, Color32::from_rgb(r, g, b)); + } +} + +fn color_button(ui: &mut Ui, srgba: [u8; 4], open: bool) -> Response { let size = ui.spacing().interact_size; let (rect, response) = ui.allocate_exact_size(size, Sense::click()); response.widget_info(|| WidgetInfo::new(WidgetType::ColorButton)); @@ -99,7 +121,7 @@ fn color_button(ui: &mut Ui, color: Color32, open: bool) -> Response { let rect = rect.expand(visuals.expansion); let stroke_width = 1.0; - show_color_at(ui.painter(), color, rect.shrink(stroke_width)); + show_srgba_unmultiplied_at(ui.painter(), srgba, rect.shrink(stroke_width)); let corner_radius = visuals.corner_radius.at_most(2); // Can't do more rounding because the background grid doesn't do any rounding ui.painter().rect_stroke( @@ -314,7 +336,12 @@ fn color_picker_hsvag_2d(ui: &mut Ui, hsvag: &mut HsvaGamma, alpha: Alpha) { } let current_color_size = vec2(ui.spacing().slider_width, ui.spacing().interact_size.y); - show_color(ui, *hsvag, current_color_size).on_hover_text("Selected color"); + show_srgba_unmultiplied( + ui, + Hsva::from(*hsvag).to_srgba_unmultiplied(), + current_color_size, + ) + .on_hover_text("Selected color"); if alpha == Alpha::BlendOrAdditive { let a = &mut hsvag.a; @@ -491,7 +518,7 @@ pub fn color_picker_color32(ui: &mut Ui, srgba: &mut Color32, alpha: Alpha) -> b pub fn color_edit_button_hsva(ui: &mut Ui, hsva: &mut Hsva, alpha: Alpha) -> Response { let popup_id = ui.auto_id_with("popup"); let open = Popup::is_id_open(ui.ctx(), popup_id); - let mut button_response = color_button(ui, (*hsva).into(), open); + let mut button_response = color_button(ui, hsva.to_srgba_unmultiplied(), open); if ui.style().explanation_tooltips { button_response = button_response.on_hover_text("Click to edit color"); } diff --git a/crates/egui_demo_app/tests/snapshots/imageviewer.png b/crates/egui_demo_app/tests/snapshots/imageviewer.png index c47f260e9..80725c619 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:b9f5204a9b8f15e0f144e66f0df8685e4e3ed90cd265474f2600fdd4cb5df390 -size 98934 +oid sha256:1cf4c34af7b69cd8220b11ff7e355ddf8d7b52a43a60a1748abf9cc1d5c7da9b +size 98869 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png index 09763f516..f2494709e 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Additive rectangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c0bf76e2a4d60fd4ba302b2217beaa4b0627614ff7d23294c7f5e6b81a028c7 -size 45061 +oid sha256:3daf9b7cbfb48f6d083126e58c605cb19f462509acc03843f0bca5c645945e23 +size 45044 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png index 85a4fa0da..e88d67b6b 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Blurred.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:645519e1e4c196b985b6b422ddf1ba51e7e92d1b0c80972318dcf44dab3022d3 -size 118907 +oid sha256:4ef3a17791ca4a3e0209eec5d191c00c66e08fc4dd07fb4845490288c5bfbdb6 +size 118889 diff --git a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png index ec2750465..44c159f4f 100644 --- a/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png +++ b/crates/egui_demo_lib/tests/snapshots/tessellation_test/Thin filled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e1a958b753fee4bec405a74dd7727e5c477c946484ed086c5c4ffee058dd5e8 -size 35960 +oid sha256:75ff881add9f2968d19d8bb1d39239de5d99105027e91af209dfa5b45e644d2c +size 35943 From 858c8fd99f0335999340f3bcb4252a9595fa2d45 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 10 Jun 2026 09:39:33 +0200 Subject: [PATCH 04/11] Improve `Debug`-formatting of `Id` in debug-builds (#8190) The Debug-formatting of `Id` in debug-builds now contain the full lineage: ```rs #[test] fn with_chain() { let id = Id::new("a").with("b").with("c").with(7_i32); assert_eq!( format!("{id:?}"), r#"Id::new("a").with("b").with("c").with(7)"# ); } ``` ## Related * https://github.com/emilk/egui/pull/5851 * https://github.com/emilk/egui/pull/7988 --- crates/egui/src/id.rs | 138 ++++++++++++++++-- crates/egui/src/id_salt.rs | 79 +++++++++- .../tests/snapshots/demos/ID Test.png | 4 +- 3 files changed, 208 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/id.rs b/crates/egui/src/id.rs index c0e21fbe2..e7bb4b2e2 100644 --- a/crates/egui/src/id.rs +++ b/crates/egui/src/id.rs @@ -65,7 +65,12 @@ impl Id { /// Generate a new root [`Id`] by hashing some source (e.g. a string or integer). pub fn new(source: impl AsId) -> Self { - Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + let id = Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(&source)); + + #[cfg(debug_assertions)] + id_source::insert_root(id, &source); + + id } /// Generate a child [`Id`] by salting the parent [`Id`] with the given argument. @@ -73,8 +78,13 @@ impl Id { use std::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()); - Self::from_hash(hasher.finish()) + hasher.write_u64(IdSalt::new(&salt).value()); + let id = Self::from_hash(hasher.finish()); + + #[cfg(debug_assertions)] + id_source::insert_child(id, self, &salt); + + id } /// Short and readable summary @@ -116,10 +126,19 @@ impl Id { impl std::fmt::Debug for Id { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:04X}", self.value() as u16) + if *self == Self::NULL { + return write!(f, "Id::NULL"); + } + #[cfg(debug_assertions)] + if let Some(source) = id_source::get(*self) { + return f.write_str(&source); + } + write!(f, "id_{:04X}", self.value() as u16) } } +// ---------------------------------------------------------------------------- + /// Convenience impl From<&'static str> for Id { #[inline] @@ -135,12 +154,6 @@ impl From for Id { } } -#[test] -fn id_size() { - assert_eq!(std::mem::size_of::(), 8); - assert_eq!(std::mem::size_of::>(), 8); -} - // ---------------------------------------------------------------------------- /// `IdSet` is a `HashSet` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. @@ -148,3 +161,108 @@ pub type IdSet = nohash_hasher::IntSet; /// `IdMap` is a `HashMap` optimized by knowing that [`Id`] has good entropy, and doesn't need more hashing. pub type IdMap = nohash_hasher::IntMap; + +// ---------------------------------------------------------------------------- + +/// In debug builds, remember the `Debug`-formatted call chain that produced each [`Id`]. +/// +/// Used by [`Id`]'s `Debug` impl so that `Id::new("foo")` prints as `Id::new("foo")`, +/// and `Id::new("foo").with("bar")` prints as `Id::new("foo").with("bar")`, etc. +#[cfg(debug_assertions)] +mod id_source { + use super::{AsId, AsIdSalt, Id, IdMap}; + use epaint::mutex::RwLock; + use std::sync::LazyLock; + + static SOURCE_MAP: LazyLock>> = LazyLock::new(RwLock::default); + + pub(super) fn insert_root(id: Id, source: &impl AsId) { + if SOURCE_MAP.read().contains_key(&id) { + return; + } + // Format outside the lock since `{source:?}` may itself recurse into [`Id`]'s `Debug` impl. + let formatted = format!("Id::new({source:?})"); + SOURCE_MAP.write().insert(id, formatted); + } + + pub(super) fn insert_child(id: Id, parent: Id, salt: &impl AsIdSalt) { + if SOURCE_MAP.read().contains_key(&id) { + return; + } + // Look up parent's repr and drop the read guard before formatting, + // since `{parent:?}` and `{salt:?}` may themselves recurse into [`Id`]'s `Debug` impl. + let cached_parent_repr = SOURCE_MAP.read().get(&parent).cloned(); + let parent_repr = cached_parent_repr.unwrap_or_else(|| format!("{parent:?}")); + let formatted = format!("{parent_repr}.with({salt:?})"); + SOURCE_MAP.write().insert(id, formatted); + } + + pub(super) fn get(id: Id) -> Option { + SOURCE_MAP.read().get(&id).cloned() + } +} + +#[test] +fn id_size() { + assert_eq!(std::mem::size_of::(), 8); + assert_eq!(std::mem::size_of::>(), 8); +} + +#[cfg(test)] +#[cfg(debug_assertions)] +mod debug_format_tests { + use crate::IdSalt; + + use super::Id; + + #[test] + fn root_string() { + let id = Id::new("foo"); + assert_eq!(format!("{id:?}"), r#"Id::new("foo")"#); + } + + #[test] + fn root_integer() { + let id = Id::new(42_i32); + assert_eq!(format!("{id:?}"), "Id::new(42)"); + } + + #[test] + fn root_id_salt() { + let id = Id::new(IdSalt::new("foo")); + assert_eq!(format!("{id:?}"), r#"Id::new(IdSalt::new("foo"))"#); + } + + #[test] + fn with_one_child() { + let id = Id::new("parent").with("child"); + assert_eq!(format!("{id:?}"), r#"Id::new("parent").with("child")"#); + } + + #[test] + fn with_chain() { + let id = Id::new("a").with("b").with("c").with(7_i32); + assert_eq!( + format!("{id:?}"), + r#"Id::new("a").with("b").with("c").with(7)"# + ); + } + + #[test] + fn nested_id_as_source() { + let inner = Id::new("foo"); + let outer = Id::new(inner); + assert_eq!(format!("{outer:?}"), r#"Id::new(Id::new("foo"))"#); + } + + #[test] + fn null_prints_as_null() { + assert_eq!(format!("{:?}", Id::NULL), "Id::NULL"); + } + + #[test] + fn null_as_parent() { + let id = Id::NULL.with("foo"); + assert_eq!(format!("{id:?}"), r#"Id::NULL.with("foo")"#); + } +} diff --git a/crates/egui/src/id_salt.rs b/crates/egui/src/id_salt.rs index 0912dcbd3..486dda239 100644 --- a/crates/egui/src/id_salt.rs +++ b/crates/egui/src/id_salt.rs @@ -30,7 +30,12 @@ impl nohash_hasher::IsEnabled for IdSalt {} impl IdSalt { /// Create a new [`IdSalt`] by hashing some source (e.g. a string or integer). pub fn new(source: impl AsIdSalt) -> Self { - Self::from_hash(ahash::RandomState::with_seeds(1, 2, 3, 4).hash_one(source)) + let id_salt = Self::from_hash(ahash::RandomState::with_seeds(5, 6, 7, 8).hash_one(&source)); + + #[cfg(debug_assertions)] + id_salt_source::maybe_insert(id_salt, &source); + + id_salt } /// Create a new root [`IdSalt`] from a high-entropy hash. @@ -54,6 +59,78 @@ impl IdSalt { impl std::fmt::Debug for IdSalt { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + #[cfg(debug_assertions)] + if let Some(source) = id_salt_source::get(*self) { + return write!(f, "IdSalt::new({source})"); + } write!(f, "salt_{:04X}", self.value() as u16) } } + +/// In debug builds, remember the `Debug`-formatted source that produced each [`IdSalt`]. +/// +/// Used by [`IdSalt`]'s `Debug` impl so that `IdSalt::new("foo")` prints as +/// `IdSalt::new("foo")`, and `IdSalt::new(IdSalt::new("foo"))` prints as +/// `IdSalt::new(IdSalt::new("foo"))`, etc. +#[cfg(debug_assertions)] +mod id_salt_source { + use super::{AsIdSalt, IdSalt}; + use epaint::mutex::RwLock; + use nohash_hasher::IntMap; + use std::sync::LazyLock; + + static SOURCE_MAP: LazyLock>> = LazyLock::new(RwLock::default); + + pub(super) fn maybe_insert(id_salt: IdSalt, source: &impl AsIdSalt) { + if !SOURCE_MAP.read().contains_key(&id_salt) { + let formatted = format!("{source:?}"); + SOURCE_MAP.write().insert(id_salt, formatted); + } + } + + pub(super) fn get(id_salt: IdSalt) -> Option { + SOURCE_MAP.read().get(&id_salt).cloned() + } +} + +#[cfg(test)] +#[cfg(debug_assertions)] +mod tests { + use super::IdSalt; + + #[test] + fn debug_format_string_source() { + let salt = IdSalt::new("foo"); + assert_eq!(format!("{salt:?}"), r#"IdSalt::new("foo")"#); + } + + #[test] + fn debug_format_integer_source() { + let salt = IdSalt::new(42_i32); + assert_eq!(format!("{salt:?}"), "IdSalt::new(42)"); + } + + #[test] + fn debug_format_nested_salt() { + let inner = IdSalt::new("foo"); + let outer = IdSalt::new(inner); + assert_eq!(format!("{outer:?}"), r#"IdSalt::new(IdSalt::new("foo"))"#); + } + + #[test] + fn debug_format_triple_nested_salt() { + let a = IdSalt::new("foo"); + let b = IdSalt::new(a); + let c = IdSalt::new(b); + assert_eq!( + format!("{c:?}"), + r#"IdSalt::new(IdSalt::new(IdSalt::new("foo")))"# + ); + } + + #[test] + fn debug_format_tuple_source() { + let salt = IdSalt::new(("foo", 7_i32)); + assert_eq!(format!("{salt:?}"), r#"IdSalt::new(("foo", 7))"#); + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png index f26458ea5..53575912a 100644 --- a/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png +++ b/crates/egui_demo_lib/tests/snapshots/demos/ID Test.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9b497aa9b2b92843937c84a6ff8901f248501d5d4a89c2fb1237008a44fcad1 -size 114038 +oid sha256:bbd7fa4db7dd580968949b9d76c1521d5627cc1ee8cd19bb3dea752d3d47607b +size 114174 From 0b920aae423fb8822a88fe4290177a01a76a634e Mon Sep 17 00:00:00 2001 From: Sylvain <67423638+Le-Syl21@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:53:48 +0200 Subject: [PATCH 05/11] Add modifier keys to `egui::Key` (#8127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem winit has always delivered distinct physical variants for every keyboard key — \`KeyCode::ShiftLeft\` vs \`KeyCode::ShiftRight\`, \`KeyCode::ControlLeft\`/\`ControlRight\`, \`AltLeft\`/\`AltRight\`, \`SuperLeft\`/\`SuperRight\`, plus the ISO 102nd key \`KeyCode::IntlBackslash\` (the one between LShift and Z, labelled \`<>|\` on French AZERTY and \`\\|\` on UK QWERTY). Today none of these reach egui: - Pressing Shift / Ctrl / Alt alone produces *no* \`Event::Key\` at all. \`key_from_key_code\` and \`key_from_named_key\` both return \`None\` for modifiers, so the \`if let Some(active_key)\` branch in \`on_keyboard_input\` is skipped. The collapsed \`Modifiers\` bools are the only trace of the press, and they don't distinguish left vs right. - \`KeyCode::IntlBackslash\` has no arm in \`key_from_key_code\`, so on French / UK ISO keyboards the \`<>|\` key is completely invisible to egui apps — neither \`key\` nor \`physical_key\` is ever set. ## Who hits this - Games / kiosk frontends / pincab UIs that bind \`LeftFlipper = LShift\` vs \`RightFlipper = RShift\` (or \`LeftMagna = LCtrl\` vs \`RightMagna = RCtrl\`) — currently impossible inside egui without shelling out to platform APIs (\`device_query\`, raw X11, etc.). - Anyone on an ISO keyboard who wants to capture the 102nd key in an input-binding UI. Previously discussed: context in #2977 (closed by #3649 which added \`physical_key\`, but only for keys already in \`egui::Key\`). ## Change Two small additions, no behaviour change for existing code: **\`crates/egui/src/data/key.rs\`** — new variants at the end of \`Key\`: - \`ShiftLeft\`, \`ShiftRight\`, \`ControlLeft\`, \`ControlRight\`, \`AltLeft\`, \`AltRight\`, \`SuperLeft\`, \`SuperRight\` - \`IntlBackslash\` plus their entries in \`Key::ALL\`, \`Key::from_name\`, and \`Key::name\` (the \`key_from_name\` roundtrip test at the bottom of the file still passes). **\`crates/egui-winit/src/lib.rs\`** — new arms in \`key_from_key_code\`: \`\`\`rust KeyCode::ShiftLeft => Key::ShiftLeft, KeyCode::ShiftRight => Key::ShiftRight, // ...ControlLeft/Right, AltLeft/Right, SuperLeft/Right... KeyCode::IntlBackslash => Key::IntlBackslash, \`\`\` The existing \`Modifiers\` struct is untouched — shortcut matching (\"Ctrl+C\"), \`consume_shortcut\`, etc. still see the collapsed state. The new variants are purely additive and only surface as physical \`Event::Key\` presses when someone is specifically looking for them. ## Test - Existing \`test_key_from_name\` test still passes (updated the sentinel to \`Key::IntlBackslash as usize + 1\`). - Manual smoke test: pressing left vs right Shift, Ctrl, Alt each produces an \`Event::Key { key: Key::ShiftLeft/Right/..., physical_key: Some(Key::ShiftLeft/Right/...), ... }\`; pressing the AZERTY 102nd key yields \`Key::IntlBackslash\`. Character-key behaviour and \`Modifiers\` bools are unchanged. ## Not included - **Web backend** (\`eframe_web\`): \`PhysicalKey\` isn't fully exposed there yet per the existing \`physical_key\` docs, so these new variants are only emitted on native. Happy to extend to web in a follow-up if wanted. - \`ModifiersSymmetric\` / per-side sticky state: would be a bigger API change in \`Modifiers\`. This PR stays at the minimum: forward what winit already gives us for the event path. Closes no issue directly but addresses the underlying gap noted in the thread of #2977 (scancode forwarding) for the modifier / Intl-key subset. --- crates/egui-winit/src/lib.rs | 16 +++++++++ crates/egui/src/data/key.rs | 67 +++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 59ab42222..5385eb948 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -1582,6 +1582,22 @@ fn key_from_key_code(key: winit::keyboard::KeyCode) -> Option { KeyCode::F34 => Key::F34, KeyCode::F35 => Key::F35, + // Modifier keys — egui now surfaces them as distinct physical + // variants so games / capture UIs can bind them independently. + // The collapsed `Modifiers.shift/ctrl/alt/command` booleans still + // track just the "any side is pressed" state for shortcut matching. + KeyCode::ShiftLeft => Key::ShiftLeft, + KeyCode::ShiftRight => Key::ShiftRight, + KeyCode::ControlLeft => Key::ControlLeft, + KeyCode::ControlRight => Key::ControlRight, + KeyCode::AltLeft => Key::AltLeft, + KeyCode::AltRight => Key::AltRight, + KeyCode::SuperLeft => Key::SuperLeft, + KeyCode::SuperRight => Key::SuperRight, + + // ISO 102nd key — `<>|` on French AZERTY, `\|` on UK QWERTY. + KeyCode::IntlBackslash => Key::IntlBackslash, + _ => { return None; } diff --git a/crates/egui/src/data/key.rs b/crates/egui/src/data/key.rs index 2a0f33fc3..edcc58f42 100644 --- a/crates/egui/src/data/key.rs +++ b/crates/egui/src/data/key.rs @@ -188,6 +188,38 @@ pub enum Key { /// Android sends this key on Back button press. /// Does not work on Web. BrowserBack, + + // ---------------------------------------------- + // Modifier keys (exposed as distinct left/right variants so that + // games and input-capture UIs can bind them independently). egui's + // `Modifiers` struct still collapses both sides for the common case + // (e.g. "Ctrl+C"); these variants are emitted only as physical + // `Event::Key` presses. + /// Left Shift key. + ShiftLeft, + /// Right Shift key. + ShiftRight, + /// Left Control key. + ControlLeft, + /// Right Control key. + ControlRight, + /// Left Alt / Option key. + AltLeft, + /// Right Alt / AltGr / Option key. + AltRight, + /// Left Super / Meta / Command / Windows key. + SuperLeft, + /// Right Super / Meta / Command / Windows key. + SuperRight, + + // ---------------------------------------------- + // International keys — physical positions that only exist on + // non-US keyboards. + /// ISO 102nd key: physically located between the left Shift and Z + /// on ISO layouts. On French AZERTY it produces `<>|`; on UK + /// QWERTY a secondary `\` / `|`. Missing from US ANSI keyboards. + IntlBackslash, + // When adding keys, remember to also update: // * crates/egui-winit/src/lib.rs // * Key::ALL @@ -314,6 +346,17 @@ impl Key { Self::F35, // Navigation keys: Self::BrowserBack, + // Modifier keys (physical L/R): + Self::ShiftLeft, + Self::ShiftRight, + Self::ControlLeft, + Self::ControlRight, + Self::AltLeft, + Self::AltRight, + Self::SuperLeft, + Self::SuperRight, + // International keys: + Self::IntlBackslash, ]; /// Converts `"A"` to `Key::A`, `Space` to `Key::Space`, etc. @@ -444,6 +487,17 @@ impl Key { "BrowserBack" => Self::BrowserBack, + "ShiftLeft" => Self::ShiftLeft, + "ShiftRight" => Self::ShiftRight, + "ControlLeft" => Self::ControlLeft, + "ControlRight" => Self::ControlRight, + "AltLeft" => Self::AltLeft, + "AltRight" => Self::AltRight, + "SuperLeft" => Self::SuperLeft, + "SuperRight" => Self::SuperRight, + + "IntlBackslash" => Self::IntlBackslash, + _ => return None, }) } @@ -599,6 +653,17 @@ impl Key { Self::F35 => "F35", Self::BrowserBack => "BrowserBack", + + Self::ShiftLeft => "ShiftLeft", + Self::ShiftRight => "ShiftRight", + Self::ControlLeft => "ControlLeft", + Self::ControlRight => "ControlRight", + Self::AltLeft => "AltLeft", + Self::AltRight => "AltRight", + Self::SuperLeft => "SuperLeft", + Self::SuperRight => "SuperRight", + + Self::IntlBackslash => "IntlBackslash", } } } @@ -607,7 +672,7 @@ impl Key { fn test_key_from_name() { assert_eq!( Key::ALL.len(), - Key::BrowserBack as usize + 1, + Key::IntlBackslash as usize + 1, "Some keys are missing in Key::ALL" ); From 5012603e03f380defac510df970d6b2cc635131a Mon Sep 17 00:00:00 2001 From: rustbasic <127506429+rustbasic@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:57:44 +0900 Subject: [PATCH 06/11] Fix: ScrollArea layout jitter with floating bars and zoom levels (#7944) Fix: ScrollArea layout jitter with floating bars and zoom levels * Closes #7937 * Closes #7942 This PR improves the stability of `ScrollArea` by addressing two layout issues: 1. **Discrete Layout for Floating Bars:** Fixed content "shaking" in `floating` mode when a non-zero `allocated_width` is used. By using discrete visibility (`show_bars`) instead of the animated factor for space allocation, we ensure the layout remains stable during scrollbar animations. 2. **Zoom-level Stability:** Introduced a small epsilon (0.1) when checking if content exceeds the viewport. This prevents scrollbars from flickering on and off due to floating-point rounding errors at specific zoom factors (e.g., 1.01 or 0.95). --- crates/egui/src/containers/scroll_area.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/containers/scroll_area.rs b/crates/egui/src/containers/scroll_area.rs index 5e0c69936..0fa4bba0c 100644 --- a/crates/egui/src/containers/scroll_area.rs +++ b/crates/egui/src/containers/scroll_area.rs @@ -753,7 +753,12 @@ impl ScrollArea { ctx.animate_bool_responsive(id.with("v"), show_bars[1]), ); - let current_bar_use = show_bars_factor.yx() * ui.spacing().scroll.allocated_width(); + let scroll_style = ui.spacing().scroll; + let current_bar_use = if scroll_style.floating { + show_bars.to_vec2().yx() * scroll_style.allocated_width() + } else { + show_bars_factor.yx() * scroll_style.allocated_width() + }; let available_outer = ui.available_rect_before_wrap(); @@ -1187,9 +1192,15 @@ impl Prepared { let outer_rect = Rect::from_min_size(inner_rect.min, inner_rect.size() + current_bar_use); + let limit_rect = if ui.spacing().scroll.floating { + outer_rect + } else { + inner_rect + }; + let content_is_too_large = Vec2b::new( - direction_enabled[0] && inner_rect.width() < content_size.x, - direction_enabled[1] && inner_rect.height() < content_size.y, + direction_enabled[0] && (limit_rect.width().ceil() < content_size.x), + direction_enabled[1] && (limit_rect.height().ceil() < content_size.y), ); let max_offset = content_size - inner_rect.size(); From 923ddcf30d48673cffdd5a04df793c3f6396a1ff Mon Sep 17 00:00:00 2001 From: n4n5 Date: Wed, 10 Jun 2026 09:58:19 +0200 Subject: [PATCH 07/11] Enhance documentation for Ui::disabled method (#8209) * [x] I have followed the instructions in the PR template Related to https://github.com/emilk/egui/discussions/4263 --- crates/egui/src/ui_builder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/egui/src/ui_builder.rs b/crates/egui/src/ui_builder.rs index 0900d0d06..67ed692a2 100644 --- a/crates/egui/src/ui_builder.rs +++ b/crates/egui/src/ui_builder.rs @@ -117,6 +117,8 @@ impl UiBuilder { /// Make the new `Ui` disabled, i.e. grayed-out and non-interactive. /// /// Note that if the parent `Ui` is disabled, the child will always be disabled. + /// + /// See also [`crate::Ui::add_enabled`], [`crate::Ui::add_enabled_ui`] and [`crate::Ui::is_enabled`]. #[inline] pub fn disabled(mut self) -> Self { self.disabled = true; From 704d86e4aaba8d96405c26e6559976f1a82b3367 Mon Sep 17 00:00:00 2001 From: psyche314 <180074435+psyche314@users.noreply.github.com> Date: Wed, 10 Jun 2026 16:00:41 +0800 Subject: [PATCH 08/11] Expose interactive rects from the last pass (#8211) ## Summary Adds `Context::interactive_rects_last_pass() -> Vec`, an integration-facing helper that returns the same widget interaction rectangles egui uses for hit-testing in the last completed pass. The method filters out disabled widgets, non-interactive widgets, and layers that currently do not allow interaction. It also applies per-layer transforms so the returned rectangles are in global viewport coordinates. ## Motivation Some egui integrations need to declare platform-level input regions before pointer events can reach egui itself. A concrete example is a transparent or click-through overlay: the platform/windowing layer must know which parts of the overlay should receive input and which parts should pass through to whatever is underneath. Today egui keeps this information internally in `WidgetRects` and uses it for its own hit-testing, but integrations cannot enumerate those rectangles. Downstream integrations therefore need app-level side channels where each app manually reports its clickable rectangles. That is fragile because it duplicates data egui already has, is easy for applications to forget, and tends to go stale when widgets move or popups/menus appear. This method exposes only the already-derived, integration-relevant result instead of making `WidgetRects` itself part of the public API. ## Notes - The method uses the last completed pass because that is the same data egui uses for interaction at the start of the next pass. - Rectangles are returned in layer order for deterministic output. - Non-positive or non-finite rectangles are skipped. ## Verification - `cargo check -p egui` - `cargo test -p egui --lib` Co-authored-by: psyche314 --- crates/egui/src/context.rs | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index cd9cc896b..57e285575 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1307,6 +1307,52 @@ impl Context { .map(|widget_rect| self.get_response(widget_rect)) } + /// Rectangles that could receive pointer input in the last completed pass. + /// + /// This exposes the same widget rectangles egui uses for hit-testing, after + /// filtering out disabled widgets, non-interactive widgets, and layers that + /// are currently blocked from interaction. The returned rectangles are in + /// global viewport coordinates, with layer transforms applied. + /// + /// This is meant for integrations that must declare platform input regions + /// before pointer events can be delivered to egui, such as transparent or + /// click-through overlays. + #[must_use] + pub fn interactive_rects_last_pass(&self) -> Vec { + self.read(|ctx| { + let Some(viewport) = ctx.viewports.get(&ctx.viewport_id()) else { + return Vec::new(); + }; + + let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); + layers.sort_by(|&a, &b| ctx.memory.areas().compare_order(a, b)); + + let mut rects = Vec::new(); + for layer_id in layers { + if !ctx.memory.allows_interaction(layer_id) { + continue; + } + + let to_global = ctx.memory.to_global.get(&layer_id).copied(); + for widget in viewport.prev_pass.widgets.get_layer(layer_id) { + if !widget.enabled || !widget.sense.interactive() { + continue; + } + + let rect = if let Some(to_global) = to_global { + to_global * widget.interact_rect + } else { + widget.interact_rect + }; + if rect.is_positive() && rect.is_finite() { + rects.push(rect); + } + } + } + rects + }) + } + /// Do all interaction for an existing widget, without (re-)registering it. pub(crate) fn get_response(&self, widget_rect: WidgetRect) -> Response { use response::Flags; From b3e4cde85a930ce1439f1ad232b249bbaabde1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Cin=C3=A0?= Date: Wed, 10 Jun 2026 10:00:55 +0200 Subject: [PATCH 09/11] Fix #2142 - lost_focus not firing after a mid-frame focus transfer (#8210) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem `Response::lost_focus()` could silently fail to fire when keyboard focus moved from one widget to another *within the same frame* — for example, clicking a `TextEdit` that was added to the UI *after* the currently-focused one. ### Fix This widens the detection window by one extra frame, which is exactly enough for the deferred loss signal to reach the previously focused widget on its next render. ### Notes * The `test_demo_app` test fails, but it has nothing to do with this PR; it fails on the current master branch, too. * This PR replaces https://github.com/emilk/egui/pull/3247 * Closes * [x] I have followed the instructions in the PR template --- crates/egui/src/memory/mod.rs | 73 +++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index b65dfdffa..43694453c 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -490,6 +490,13 @@ pub(crate) struct Focus { /// The ID of a widget that had keyboard focus during the previous frame. id_previous_frame: Option, + /// The ID of a widget that had keyboard focus *two* frames ago. + /// + /// Kept so `Response::lost_focus` can still fire after a mid-frame + /// focus transition (e.g. clicking a `TextEdit` that was added to + /// the UI later than the currently focused one). + id_two_frames_ago: Option, + /// The ID of a widget to give the focus to in the next frame. id_next_frame: Option, @@ -545,6 +552,7 @@ impl Focus { } fn begin_pass(&mut self, new_input: &crate::data::input::RawInput) { + self.id_two_frames_ago = self.id_previous_frame; self.id_previous_frame = self.focused(); if let Some(id) = self.id_next_frame.take() { self.focused_widget = Some(FocusWidget::new(id)); @@ -831,10 +839,21 @@ impl Memory { self.focus().and_then(|f| f.id_previous_frame) == Some(id) } - /// Check if the layer lost focus last frame. - /// returns `true` if the layer lost focus last frame, but not this one. + /// Check if the widget lost keyboard focus. + /// + /// Returns `true` when `id` was the focused widget at the start + /// of this frame *or* the start of the previous frame — but is + /// not focused now. The two-frame window matters when focus + /// transfers mid-frame: the previously-focused widget has + /// usually already been rendered by the time another widget + /// claims focus, so the loss signal can only reach it on its + /// next render pass. pub(crate) fn lost_focus(&self, id: Id) -> bool { - self.had_focus_last_frame(id) && !self.has_focus(id) + let had_recent_focus = self + .focus() + .map(|f| f.id_previous_frame == Some(id) || f.id_two_frames_ago == Some(id)) + .unwrap_or(false); + had_recent_focus && !self.has_focus(id) } /// Check if the layer gained focus this frame. @@ -1368,6 +1387,54 @@ fn memory_impl_send_sync() { assert_send_sync::(); } +// Regression test for https://github.com/emilk/egui/issues/2142. +#[test] +fn lost_focus_fires_after_mid_frame_focus_transfer() { + use crate::data::input::RawInput; + let a = Id::new("A"); + let b = Id::new("B"); + let mut focus = Focus::default(); + let raw = RawInput::default(); + + fn lost_focus_check(focus: &Focus, id: Id) -> bool { + let was_focused = + focus.id_previous_frame == Some(id) || focus.id_two_frames_ago == Some(id); + was_focused && focus.focused() != Some(id) + } + + // Frame N-1 + { + focus.begin_pass(&raw); + focus.focused_widget = Some(FocusWidget::new(a)); + } + + // Frame N: `A` is focused at start; user clicks `B` mid-frame + { + focus.begin_pass(&raw); + assert_eq!(focus.id_previous_frame, Some(a)); + assert!(!lost_focus_check(&focus, a)); + focus.focused_widget = Some(FocusWidget::new(b)); + } + + // Frame N+1: `A` deferred lost_focus signal must fire + { + focus.begin_pass(&raw); + assert_eq!(focus.id_two_frames_ago, Some(a)); + assert_eq!(focus.id_previous_frame, Some(b)); + assert!(lost_focus_check(&focus, a), "`A` lost_focus must fire"); + assert!(!lost_focus_check(&focus, b)); + } + + // Frame N+2 + { + focus.begin_pass(&raw); + assert!( + !lost_focus_check(&focus, a), + "A's lost_focus must stop firing once the two-frame window passes", + ); + } +} + #[test] fn order_map_total_ordering() { let mut layers = [ From 333442008cf1a4ee90e683f81a530e7904d4a664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Navarro?= Date: Wed, 10 Jun 2026 10:01:11 +0200 Subject: [PATCH 10/11] `Column::remainder().clip(true)` now shrinks when available width decreases (#8048) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem `Column::remainder().clip(true)` grows correctly when the panel is made wider, but does not shrink when the panel is made narrower. ### Root cause There are two places in `table.rs` where the `max_used_widths` floor is applied to column widths. One correctly checks `column.clip`, the other doesn't. **Post-render** path (already correct, line ~827): ```rust if !column.clip { *column_width = column_width.at_least(max_used_widths[i]); } ``` `TableState::load` **pre-render path** (the bug, line 647): ```rust .at_least(column.width_range.min.max(*max_used)) // clip flag ignored ``` In `TableState::load`, `.at_least(max_used)` is applied to every column regardless of `clip`. For a `Column::remainder()`, `max_used` accumulates the historically widest rendered content. When the panel shrinks, this floor prevents the column from computing a smaller width, creating a cycle where it can never shrink. ### Fix Apply the same `clip` guard that already exists in the post-render path: ```rust .at_least(if column.clip { column.width_range.min } else { column.width_range.min.max(*max_used) }) ``` When `clip = true`, the floor is just `width_range.min` (0.0 by default), allowing the remainder column to shrink freely to the actual remaining space. ### Minimal reproduction
Show code ```rust use eframe::egui; use egui_extras::{Column, TableBuilder}; fn main() -> eframe::Result { eframe::run_ui_native( "Column::remainder().clip(true) shrink test", Default::default(), |ui, _frame| { egui::Frame::central_panel(ui.style()).show(ui, |ui| { let mut text = String::from("some content"); egui::ScrollArea::horizontal().show(ui, |ui| { TableBuilder::new(ui) .column(Column::auto()) .column(Column::remainder().clip(true)) .header(20.0, |mut header| { header.col(|ui| { ui.strong("Auto"); }); header.col(|ui| { ui.strong("Remainder + clip"); }); }) .body(|mut body| { body.row(18.0, |mut row| { row.col(|ui| { ui.label("label"); }); row.col(|ui| { ui.add( egui::TextEdit::singleline(&mut text) .desired_width(f32::INFINITY), ); }); }); }); }); }); }, ) } ```
**Without the fix:** make the window wider (column grows ✓), then narrower — column does not shrink and a horizontal scrollbar appears ✗ https://github.com/user-attachments/assets/2b586588-9f72-4a15-80f4-afddadb69441 **With the fix:** the column shrinks correctly and no scrollbar appears ✓ https://github.com/user-attachments/assets/f1655641-e135-489c-9f59-2af3faa887ab --- crates/egui_extras/src/table.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 9147b3c0d..6fb5e014e 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -634,7 +634,11 @@ impl TableState { InitialColumnSize::Automatic(_) => Size::exact(*prev_width), InitialColumnSize::Remainder => Size::remainder(), } - .at_least(column.width_range.min.max(*max_used)) + .at_least(if column.clip { + column.width_range.min + } else { + column.width_range.min.max(*max_used) + }) .at_most(column.width_range.max) }; sizing.add(size); From e392fbadd407637eae011a2c4dfbd650d3d6a771 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 10 Jun 2026 10:12:20 +0200 Subject: [PATCH 11/11] Refactor: break up `input.rs` into many files (#8230) --- crates/egui/src/data/input.rs | 1381 ----------------- crates/egui/src/data/input/dropped_file.rs | 19 + crates/egui/src/data/input/event.rs | 186 +++ crates/egui/src/data/input/event_filter.rs | 63 + crates/egui/src/data/input/hovered_file.rs | 10 + crates/egui/src/data/input/ime_event.rs | 25 + .../egui/src/data/input/keyboard_shortcut.rs | 52 + crates/egui/src/data/input/mod.rs | 33 + crates/egui/src/data/input/modifier_names.rs | 70 + crates/egui/src/data/input/modifiers.rs | 410 +++++ .../egui/src/data/input/mouse_wheel_unit.rs | 13 + crates/egui/src/data/input/pointer_button.rs | 23 + crates/egui/src/data/input/raw_input.rs | 225 +++ .../egui/src/data/input/safe_area_insets.rs | 19 + crates/egui/src/data/input/touch.rs | 50 + crates/egui/src/data/input/viewport_info.rs | 217 +++ 16 files changed, 1415 insertions(+), 1381 deletions(-) delete mode 100644 crates/egui/src/data/input.rs create mode 100644 crates/egui/src/data/input/dropped_file.rs create mode 100644 crates/egui/src/data/input/event.rs create mode 100644 crates/egui/src/data/input/event_filter.rs create mode 100644 crates/egui/src/data/input/hovered_file.rs create mode 100644 crates/egui/src/data/input/ime_event.rs create mode 100644 crates/egui/src/data/input/keyboard_shortcut.rs create mode 100644 crates/egui/src/data/input/mod.rs create mode 100644 crates/egui/src/data/input/modifier_names.rs create mode 100644 crates/egui/src/data/input/modifiers.rs create mode 100644 crates/egui/src/data/input/mouse_wheel_unit.rs create mode 100644 crates/egui/src/data/input/pointer_button.rs create mode 100644 crates/egui/src/data/input/raw_input.rs create mode 100644 crates/egui/src/data/input/safe_area_insets.rs create mode 100644 crates/egui/src/data/input/touch.rs create mode 100644 crates/egui/src/data/input/viewport_info.rs diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs deleted file mode 100644 index 2a6e4411e..000000000 --- a/crates/egui/src/data/input.rs +++ /dev/null @@ -1,1381 +0,0 @@ -//! The input needed by egui. - -use epaint::{ColorImage, MarginF32}; - -use crate::{ - Key, OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, - emath::{Pos2, Rect, Vec2}, -}; - -/// What the integrations provides to egui at the start of each frame. -/// -/// Set the values that make sense, leave the rest at their `Default::default()`. -/// -/// You can check if `egui` is using the inputs using -/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`]. -/// -/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner. -/// -/// 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)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct RawInput { - /// The id of the active viewport. - pub viewport_id: ViewportId, - - /// Information about all egui viewports. - pub viewports: ViewportIdMap, - - /// The insets used to only render content in a mobile safe area - /// - /// `None` will be treated as "same as last frame" - pub safe_area_insets: Option, - - /// Position and size of the area that egui should use, in points. - /// Usually you would set this to - /// - /// `Some(Rect::from_min_size(Default::default(), screen_size_in_points))`. - /// - /// but you could also constrain egui to some smaller portion of your window if you like. - /// - /// `None` will be treated as "same as last frame", with the default being a very big area. - pub screen_rect: Option, - - /// Maximum size of one side of the font texture. - /// - /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. - /// - /// The default is a very small (but very portable) 2048. - pub max_texture_side: Option, - - /// Monotonically increasing time, in seconds. Relative to whatever. Used for animations. - /// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds). - pub time: Option, - - /// Should be set to the expected time between frames when painting at vsync speeds. - /// The default for this is 1/60. - /// 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, - /// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`] - /// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`]. - pub events: Vec, - - /// Dragged files hovering over egui. - pub hovered_files: Vec, - - /// Dragged files dropped into egui. - /// - /// 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, - - /// The native window has the keyboard focus (i.e. is receiving key presses). - /// - /// False when the user alt-tab away from the application, for instance. - pub focused: bool, - - /// Does the OS use dark or light mode? - /// - /// `None` means "don't know". - pub system_theme: Option, -} - -impl Default for RawInput { - fn default() -> Self { - Self { - viewport_id: ViewportId::ROOT, - viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), - screen_rect: None, - 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(), - focused: true, // integrations opt into global focus tracking - system_theme: None, - safe_area_insets: Default::default(), - } - } -} - -impl RawInput { - /// Info about the active viewport - #[inline] - pub fn viewport(&self) -> &ViewportInfo { - self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") - } - - /// Helper: move volatile (deltas and events), clone the rest. - /// - /// * [`Self::hovered_files`] is cloned. - /// * [`Self::dropped_files`] is moved. - pub fn take(&mut self) -> Self { - Self { - viewport_id: self.viewport_id, - viewports: self - .viewports - .iter_mut() - .map(|(id, info)| (*id, info.take())) - .collect(), - screen_rect: self.screen_rect.take(), - safe_area_insets: self.safe_area_insets.take(), - 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), - focused: self.focused, - system_theme: self.system_theme, - } - } - - /// Add on new input. - pub fn append(&mut self, newer: Self) { - let Self { - viewport_id: viewport_ids, - viewports, - screen_rect, - max_texture_side, - time, - predicted_dt, - modifiers, - mut events, - mut hovered_files, - mut dropped_files, - focused, - system_theme, - safe_area_insets: safe_area, - } = newer; - - self.viewport_id = viewport_ids; - self.viewports = viewports; - self.screen_rect = screen_rect.or(self.screen_rect); - 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); - self.focused = focused; - self.system_theme = system_theme; - self.safe_area_insets = safe_area; - } -} - -/// An input event from the backend into egui, about a specific [viewport](crate::viewport). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ViewportEvent { - /// The user clicked the close-button on the window, or similar. - /// - /// If this is the root viewport, the application will exit - /// after this frame unless you send a - /// [`crate::ViewportCommand::CancelClose`] command. - /// - /// If this is not the root viewport, - /// it is up to the user to hide this viewport the next frame. - /// - /// This even will wake up both the child and parent viewport. - Close, -} - -/// Information about the current viewport, given as input each frame. -/// -/// `None` means "unknown". -/// -/// All units are in ui "points", which can be calculated from native physical pixels -/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`]; -#[derive(Clone, Debug, Default, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct ViewportInfo { - /// Parent viewport, if known. - pub parent: Option, - - /// Name of the viewport, if known. - pub title: Option, - - pub events: Vec, - - /// The OS native pixels-per-point. - /// - /// This should always be set, if known. - /// - /// On web this takes browser scaling into account, - /// and corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. - pub native_pixels_per_point: Option, - - /// Current monitor size in egui points. - pub monitor_size: Option, - - /// The inner rectangle of the native window, in monitor space and ui points scale. - /// - /// This is the content rectangle of the viewport. - /// - /// **`eframe` notes**: - /// - /// On Android / Wayland, this will always be `None` since getting the - /// position of the window is not possible. - pub inner_rect: Option, - - /// The outer rectangle of the native window, in monitor space and ui points scale. - /// - /// This is the content rectangle plus decoration chrome. - /// - /// **`eframe` notes**: - /// - /// On Android / Wayland, this will always be `None` since getting the - /// position of the window is not possible. - pub outer_rect: Option, - - /// Are we minimized? - pub minimized: Option, - - /// Are we maximized? - pub maximized: Option, - - /// Are we in fullscreen mode? - pub fullscreen: Option, - - /// Is the window focused and able to receive input? - /// - /// This should be the same as [`RawInput::focused`]. - pub focused: Option, - - /// Is the window fully occluded (completely covered) by another window? - /// - /// Not all platforms support this. - /// On platforms that don't, this will be `None` or `Some(false)`. - pub occluded: Option, -} - -impl ViewportInfo { - /// Is the window considered visible for rendering purposes? - /// - /// A window is not visible if it is minimized or occluded. - /// When not visible, the UI is not painted and rendering is skipped, - /// but application logic may still be executed by some integrations. - pub fn visible(&self) -> Option { - match (self.minimized, self.occluded) { - (Some(true), _) | (_, Some(true)) => Some(false), - (Some(false), Some(false)) => Some(true), - (_, None) | (None, _) => None, - } - } - - /// This viewport has been told to close. - /// - /// If this is the root viewport, the application will exit - /// after this frame unless you send a - /// [`crate::ViewportCommand::CancelClose`] command. - /// - /// If this is not the root viewport, - /// it is up to the user to hide this viewport the next frame. - pub fn close_requested(&self) -> bool { - self.events.contains(&ViewportEvent::Close) - } - - /// Helper: move [`Self::events`], clone the other fields. - pub fn take(&mut self) -> Self { - Self { - parent: self.parent, - title: self.title.clone(), - events: std::mem::take(&mut self.events), - native_pixels_per_point: self.native_pixels_per_point, - monitor_size: self.monitor_size, - inner_rect: self.inner_rect, - outer_rect: self.outer_rect, - minimized: self.minimized, - maximized: self.maximized, - fullscreen: self.fullscreen, - focused: self.focused, - occluded: self.occluded, - } - } - - pub fn ui(&self, ui: &mut crate::Ui) { - let Self { - parent, - title, - events, - native_pixels_per_point, - monitor_size, - inner_rect, - outer_rect, - minimized, - maximized, - fullscreen, - focused, - occluded, - } = self; - - crate::Grid::new("viewport_info").show(ui, |ui| { - ui.label("Parent:"); - ui.label(opt_as_str(parent)); - ui.end_row(); - - ui.label("Title:"); - ui.label(opt_as_str(title)); - ui.end_row(); - - ui.label("Events:"); - ui.label(format!("{events:?}")); - ui.end_row(); - - ui.label("Native pixels-per-point:"); - ui.label(opt_as_str(native_pixels_per_point)); - ui.end_row(); - - ui.label("Monitor size:"); - ui.label(opt_as_str(monitor_size)); - ui.end_row(); - - ui.label("Inner rect:"); - ui.label(opt_rect_as_string(inner_rect)); - ui.end_row(); - - ui.label("Outer rect:"); - ui.label(opt_rect_as_string(outer_rect)); - ui.end_row(); - - ui.label("Minimized:"); - ui.label(opt_as_str(minimized)); - ui.end_row(); - - ui.label("Maximized:"); - ui.label(opt_as_str(maximized)); - ui.end_row(); - - ui.label("Fullscreen:"); - ui.label(opt_as_str(fullscreen)); - ui.end_row(); - - ui.label("Focused:"); - ui.label(opt_as_str(focused)); - ui.end_row(); - - ui.label("Occluded:"); - ui.label(opt_as_str(occluded)); - ui.end_row(); - - let visible = self.visible(); - - ui.label("Visible:"); - ui.label(opt_as_str(&visible)); - ui.end_row(); - - #[expect(clippy::ref_option)] - fn opt_rect_as_string(v: &Option) -> String { - v.as_ref().map_or(String::new(), |r| { - format!("Pos: {:?}, size: {:?}", r.min, r.size()) - }) - } - - #[expect(clippy::ref_option)] - fn opt_as_str(v: &Option) -> String { - v.as_ref().map_or(String::new(), |v| format!("{v:?}")) - } - }); - } -} - -/// A file about to be dropped into egui. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct HoveredFile { - /// Set by the `egui-winit` backend. - pub path: Option, - - /// With the `eframe` web backend, this is set to the mime-type of the file (if available). - pub mime: String, -} - -/// 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, - - /// Name of the file. Set by the `eframe` web backend. - pub name: String, - - /// With the `eframe` web backend, this is set to the mime-type of the file (if available). - pub mime: String, - - /// Set by the `eframe` web backend. - pub last_modified: Option, - - /// Set by the `eframe` web backend. - pub bytes: Option>, -} - -/// An input event generated by the integration. -/// -/// This only covers events that egui cares about. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum Event { - /// The integration detected a "copy" event (e.g. Cmd+C). - Copy, - - /// The integration detected a "cut" event (e.g. Cmd+X). - Cut, - - /// The integration detected a "paste" event (e.g. Cmd+V). - Paste(String), - - /// Text input, e.g. via keyboard. - /// - /// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]). - Text(String), - - /// A key was pressed or released. - /// - /// ## Note for integration authors - /// - /// Key events that has been processed by IMEs should not be sent to `egui`. - Key { - /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak - /// keyboard layout, it will be taken into account. - /// - /// If it's impossible to determine the logical key on desktop platforms (say, in case of non-Latin letters), - /// `key` falls back to the value of the corresponding physical key. This is necessary for proper work of - /// standard shortcuts that only respond to Latin-based bindings (such as `Ctrl` + `V`). - key: Key, - - /// The physical key, corresponding to the actual position on the keyboard. - /// - /// This ignores keymaps, so it is not recommended to use this. - /// The only thing it makes sense for is things like games, - /// where e.g. the physical location of WSAD on QWERTY should always map to movement, - /// even if the user is using Dvorak or AZERTY. - /// - /// `eframe` does not (yet) implement this on web. - physical_key: Option, - - /// Was it pressed or released? - pressed: bool, - - /// If this is a `pressed` event, is it a key-repeat? - /// - /// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats. - /// Sometimes you will want to ignore such events, and this lets you do that. - /// - /// egui will automatically detect such repeat events and mark them as such here. - /// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`). - repeat: bool, - - /// The state of the modifier keys at the time of the event. - modifiers: Modifiers, - }, - - /// The mouse or touch moved to a new place. - PointerMoved(Pos2), - - /// The mouse moved, the units are unspecified. - /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. - /// `PointerMoved` and `MouseMoved` can be sent at the same time. - /// This event is optional. If the integration can not determine unfiltered motion it should not send this event. - MouseMoved(Vec2), - - /// A mouse button was pressed or released (or a touch started or stopped). - PointerButton { - /// Where is the pointer? - pos: Pos2, - - /// What mouse button? For touches, use [`PointerButton::Primary`]. - button: PointerButton, - - /// Was it the button/touch pressed this frame, or released? - pressed: bool, - - /// The state of the modifier keys at the time of the event. - modifiers: Modifiers, - }, - - /// The mouse left the screen, or the last/primary touch input disappeared. - /// - /// This means there is no longer a cursor on the screen for hovering etc. - /// - /// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`. - PointerGone, - - /// Zoom scale factor this frame (e.g. from a pinch gesture). - /// - /// * `zoom = 1`: no change. - /// * `zoom < 1`: pinch together - /// * `zoom > 1`: pinch spread - /// - /// Note that egui also implement zooming by holding `Ctrl` and scrolling the mouse wheel, - /// so integration need NOT emit this `Zoom` event in those cases, just [`Self::MouseWheel`]. - /// - /// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame. - Zoom(f32), - - /// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture). - Rotate(f32), - - /// IME Event - Ime(ImeEvent), - - /// On touch screens, report this *in addition to* - /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] - Touch { - /// Hashed device identifier (if available; may be zero). - /// Can be used to separate touches from different devices. - device_id: TouchDeviceId, - - /// Unique identifier of a finger/pen. Value is stable from touch down - /// to lift-up - id: TouchId, - - /// One of: start move end cancel. - phase: TouchPhase, - - /// Position of the touch (or where the touch was last detected) - pos: Pos2, - - /// Describes how hard the touch device was pressed. May always be `None` if the platform does - /// not support pressure sensitivity. - /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). - force: Option, - }, - - /// A raw mouse wheel event as sent by the backend. - /// - /// Used for scrolling. - MouseWheel { - /// The unit of `delta`: points, lines, or pages. - unit: MouseWheelUnit, - - /// The direction of the vector indicates how to move the _content_ that is being viewed. - /// So if you get positive values, the content being viewed should move to the right and down, - /// revealing new things to the left and up. - /// - /// A positive X-value indicates the content is being moved right, - /// as when swiping right on a touch-screen or track-pad with natural scrolling. - /// - /// A positive Y-value indicates the content is being moved down, - /// as when swiping down on a touch-screen or track-pad with natural scrolling. - delta: Vec2, - - /// The phase of the scroll, useful for trackpads. - /// - /// If unknown set this to [`TouchPhase::Move`]. - phase: TouchPhase, - - /// The state of the modifier keys at the time of the event. - modifiers: Modifiers, - }, - - /// The native window gained or lost focused (e.g. the user clicked alt-tab). - WindowFocused(bool), - - /// An assistive technology (e.g. screen reader) requested an action. - AccessKitActionRequest(accesskit::ActionRequest), - - /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. - Screenshot { - viewport_id: crate::ViewportId, - - /// Whatever was passed to [`crate::ViewportCommand::Screenshot`]. - user_data: crate::UserData, - - image: std::sync::Arc, - }, -} - -/// IME event. -/// -/// See -#[derive(Clone, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum ImeEvent { - /// Notifies when the IME was enabled. - #[deprecated = "No longer used by egui"] - Enabled, - - /// A new IME candidate is being suggested. - /// - /// An empty preedit string indicates that the IME has been dismissed, while - /// a non-empty preedit string indicates that the IME is active. - Preedit(String), - - /// IME composition ended with this final result. - /// - /// The IME is considered dismissed after this event. - Commit(String), - - /// Notifies when the IME was disabled. - #[deprecated = "No longer used by egui"] - Disabled, -} - -/// Mouse button (or similar for touch input) -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum PointerButton { - /// The primary mouse button is usually the left one. - Primary = 0, - - /// The secondary mouse button is usually the right one, - /// and most often used for context menus or other optional things. - Secondary = 1, - - /// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel). - Middle = 2, - - /// The first extra mouse button on some mice. In web typically corresponds to the Browser back button. - Extra1 = 3, - - /// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button. - Extra2 = 4, -} - -/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`]. -pub const NUM_POINTER_BUTTONS: usize = 5; - -/// State of the modifier keys. These must be fed to egui. -/// -/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`]. -/// -/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function -/// -/// ```rust -/// # let ctx = egui::Context::default(); -/// let modifiers = ctx.input(|i| i.modifiers); -/// ``` -/// -/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers -/// as on mac that is how you type special characters, -/// so those key presses are usually not reported to egui. -#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct Modifiers { - /// Either of the alt keys are down (option ⌥ on Mac). - pub alt: bool, - - /// Either of the control keys are down. - /// When checking for keyboard shortcuts, consider using [`Self::command`] instead. - pub ctrl: bool, - - /// Either of the shift keys are down. - pub shift: bool, - - /// The Mac ⌘ Command key. Should always be set to `false` on other platforms. - pub mac_cmd: bool, - - /// On Windows and Linux, set this to the same value as `ctrl`. - /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). - /// This is so that egui can, for instance, select all text by checking for `command + A` - /// and it will work on both Mac and Windows. - pub command: bool, -} - -impl std::fmt::Debug for Modifiers { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_none() { - return write!(f, "Modifiers::NONE"); - } - - let Self { - alt, - ctrl, - shift, - mac_cmd, - command, - } = *self; - - let mut debug = f.debug_struct("Modifiers"); - if alt { - debug.field("alt", &true); - } - if ctrl { - debug.field("ctrl", &true); - } - if shift { - debug.field("shift", &true); - } - if mac_cmd { - debug.field("mac_cmd", &true); - } - if command { - debug.field("command", &true); - } - debug.finish() - } -} - -impl Modifiers { - pub const NONE: Self = Self { - alt: false, - ctrl: false, - shift: false, - mac_cmd: false, - command: false, - }; - - pub const ALT: Self = Self { - alt: true, - ctrl: false, - shift: false, - mac_cmd: false, - command: false, - }; - pub const CTRL: Self = Self { - alt: false, - ctrl: true, - shift: false, - mac_cmd: false, - command: false, - }; - pub const SHIFT: Self = Self { - alt: false, - ctrl: false, - shift: true, - mac_cmd: false, - command: false, - }; - - /// The Mac ⌘ Command key - pub const MAC_CMD: Self = Self { - alt: false, - ctrl: false, - shift: false, - mac_cmd: true, - command: false, - }; - - /// On Mac: ⌘ Command key, elsewhere: Ctrl key - pub const COMMAND: Self = Self { - alt: false, - ctrl: false, - shift: false, - mac_cmd: false, - command: true, - }; - - /// ``` - /// # use egui::Modifiers; - /// assert_eq!( - /// Modifiers::CTRL | Modifiers::ALT, - /// Modifiers { ctrl: true, alt: true, ..Default::default() } - /// ); - /// assert_eq!( - /// Modifiers::ALT.plus(Modifiers::CTRL), - /// Modifiers::CTRL.plus(Modifiers::ALT), - /// ); - /// assert_eq!( - /// Modifiers::CTRL | Modifiers::ALT, - /// Modifiers::CTRL.plus(Modifiers::ALT), - /// ); - /// ``` - #[inline] - pub const fn plus(self, rhs: Self) -> Self { - Self { - alt: self.alt | rhs.alt, - ctrl: self.ctrl | rhs.ctrl, - shift: self.shift | rhs.shift, - mac_cmd: self.mac_cmd | rhs.mac_cmd, - command: self.command | rhs.command, - } - } - - #[inline] - pub fn is_none(&self) -> bool { - self == &Self::default() - } - - #[inline] - pub fn any(&self) -> bool { - !self.is_none() - } - - #[inline] - pub fn all(&self) -> bool { - self.alt && self.ctrl && self.shift && self.command - } - - /// Is shift the only pressed button? - #[inline] - pub fn shift_only(&self) -> bool { - self.shift && !(self.alt || self.command) - } - - /// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed. - #[inline] - pub fn command_only(&self) -> bool { - !self.alt && !self.shift && self.command - } - - /// Checks that the `ctrl/cmd` matches, and that the `shift/alt` of the argument is a subset - /// of the pressed key (`self`). - /// - /// This means that if the pattern has not set `shift`, then `self` can have `shift` set or not. - /// - /// The reason is that many logical keys require `shift` or `alt` on some keyboard layouts. - /// For instance, in order to press `+` on an English keyboard, you need to press `shift` and `=`, - /// but a Swedish keyboard has dedicated `+` key. - /// So if you want to make a [`KeyboardShortcut`] looking for `Cmd` + `+`, it makes sense - /// to ignore the shift key. - /// Similarly, the `Alt` key is sometimes used to type special characters. - /// - /// However, if the pattern (the argument) explicitly requires the `shift` or `alt` keys - /// to be pressed, then they must be pressed. - /// - /// # Example: - /// ``` - /// # use egui::Modifiers; - /// # let pressed_modifiers = Modifiers::default(); - /// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) { - /// // Alt and Shift are pressed, but not ctrl/command - /// } - /// ``` - /// - /// ## Behavior: - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::CTRL.matches_logically(Modifiers::CTRL)); - /// assert!(!Modifiers::CTRL.matches_logically(Modifiers::CTRL | Modifiers::SHIFT)); - /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_logically(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); - /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); - /// assert!(!Modifiers::COMMAND.matches_logically(Modifiers::MAC_CMD)); - /// ``` - pub fn matches_logically(&self, pattern: Self) -> bool { - if pattern.alt && !self.alt { - return false; - } - if pattern.shift && !self.shift { - return false; - } - - self.cmd_ctrl_matches(pattern) - } - - /// Check for equality but with proper handling of [`Self::command`]. - /// - /// `self` here are the currently pressed modifiers, - /// and the argument the pattern we are testing for. - /// - /// Note that this will require the `shift` and `alt` keys match, even though - /// these modifiers are sometimes required to produce some logical keys. - /// For instance, to press `+` on an English keyboard, you need to press `shift` and `=`, - /// but on a Swedish keyboard you can press the dedicated `+` key. - /// Therefore, you often want to use [`Self::matches_logically`] instead. - /// - /// # Example: - /// ``` - /// # use egui::Modifiers; - /// # let pressed_modifiers = Modifiers::default(); - /// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) { - /// // Alt and Shift are pressed, and nothing else - /// } - /// ``` - /// - /// ## Behavior: - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL)); - /// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT)); - /// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL)); - /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); - /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); - /// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD)); - /// ``` - pub fn matches_exact(&self, pattern: Self) -> bool { - // alt and shift must always match the pattern: - if pattern.alt != self.alt || pattern.shift != self.shift { - return false; - } - - self.cmd_ctrl_matches(pattern) - } - - /// Check if any of the modifiers match exactly. - /// - /// Returns true if the same modifier is pressed in `self` as in `pattern`, - /// for at least one modifier. - /// - /// ## Behavior: - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL)); - /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT)); - /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL)); - /// ``` - pub fn matches_any(&self, pattern: Self) -> bool { - if self.alt && pattern.alt { - return true; - } - if self.shift && pattern.shift { - return true; - } - if self.ctrl && pattern.ctrl { - return true; - } - if self.mac_cmd && pattern.mac_cmd { - return true; - } - if (self.mac_cmd || self.command || self.ctrl) && pattern.command { - return true; - } - false - } - - /// Checks only cmd/ctrl, not alt/shift. - /// - /// `self` here are the currently pressed modifiers, - /// and the argument the pattern we are testing for. - /// - /// This takes care to properly handle the difference between - /// [`Self::ctrl`], [`Self::command`] and [`Self::mac_cmd`]. - pub fn cmd_ctrl_matches(&self, pattern: Self) -> bool { - if pattern.mac_cmd { - // Mac-specific match: - if !self.mac_cmd { - return false; - } - if pattern.ctrl != self.ctrl { - return false; - } - return true; - } - - if !pattern.ctrl && !pattern.command { - // the pattern explicitly doesn't want any ctrl/command: - return !self.ctrl && !self.command; - } - - // if the pattern is looking for command, then `ctrl` may or may not be set depending on platform. - // if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform. - - if pattern.ctrl && !self.ctrl { - return false; - } - if pattern.command && !self.command { - return false; - } - - true - } - - /// Whether another set of modifiers is contained in this set of modifiers with proper handling of [`Self::command`]. - /// - /// ``` - /// # use egui::Modifiers; - /// assert!(Modifiers::default().contains(Modifiers::default())); - /// assert!(Modifiers::CTRL.contains(Modifiers::default())); - /// assert!(Modifiers::CTRL.contains(Modifiers::CTRL)); - /// assert!(Modifiers::CTRL.contains(Modifiers::COMMAND)); - /// assert!(Modifiers::MAC_CMD.contains(Modifiers::COMMAND)); - /// assert!(Modifiers::COMMAND.contains(Modifiers::MAC_CMD)); - /// assert!(Modifiers::COMMAND.contains(Modifiers::CTRL)); - /// assert!(!(Modifiers::ALT | Modifiers::CTRL).contains(Modifiers::SHIFT)); - /// assert!((Modifiers::CTRL | Modifiers::SHIFT).contains(Modifiers::CTRL)); - /// assert!(!Modifiers::CTRL.contains(Modifiers::CTRL | Modifiers::SHIFT)); - /// ``` - pub fn contains(&self, query: Self) -> bool { - if query == Self::default() { - return true; - } - - let Self { - alt, - ctrl, - shift, - mac_cmd, - command, - } = *self; - - if alt && query.alt { - return self.contains(Self { - alt: false, - ..query - }); - } - if shift && query.shift { - return self.contains(Self { - shift: false, - ..query - }); - } - - if (ctrl || command) && (query.ctrl || query.command) { - return self.contains(Self { - command: false, - ctrl: false, - ..query - }); - } - if (mac_cmd || command) && (query.mac_cmd || query.command) { - return self.contains(Self { - mac_cmd: false, - command: false, - ..query - }); - } - - false - } -} - -impl std::ops::BitOr for Modifiers { - type Output = Self; - - #[inline] - fn bitor(self, rhs: Self) -> Self { - self.plus(rhs) - } -} - -impl std::ops::BitOrAssign for Modifiers { - #[inline] - fn bitor_assign(&mut self, rhs: Self) { - *self = *self | rhs; - } -} - -impl Modifiers { - pub fn ui(&self, ui: &mut crate::Ui) { - ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac())); - } -} - -// ---------------------------------------------------------------------------- - -/// Names of different modifier keys. -/// -/// Used to name modifiers. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct ModifierNames<'a> { - pub is_short: bool, - - pub alt: &'a str, - pub ctrl: &'a str, - pub shift: &'a str, - pub mac_cmd: &'a str, - pub mac_alt: &'a str, - - /// What goes between the names - pub concat: &'a str, -} - -impl ModifierNames<'static> { - /// ⌥ ⌃ ⇧ ⌘ - NOTE: not supported by the default egui font. - pub const SYMBOLS: Self = Self { - is_short: true, - alt: "⌥", - ctrl: "⌃", - shift: "⇧", - mac_cmd: "⌘", - mac_alt: "⌥", - concat: "", - }; - - /// Alt, Ctrl, Shift, Cmd - pub const NAMES: Self = Self { - is_short: false, - alt: "Alt", - ctrl: "Ctrl", - shift: "Shift", - mac_cmd: "Cmd", - mac_alt: "Option", - concat: "+", - }; -} - -impl ModifierNames<'_> { - pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String { - let mut s = String::new(); - - let mut append_if = |modifier_is_active, modifier_name| { - if modifier_is_active { - if !s.is_empty() { - s += self.concat; - } - s += modifier_name; - } - }; - - if is_mac { - append_if(modifiers.ctrl, self.ctrl); - append_if(modifiers.shift, self.shift); - append_if(modifiers.alt, self.mac_alt); - append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd); - } else { - append_if(modifiers.ctrl || modifiers.command, self.ctrl); - append_if(modifiers.alt, self.alt); - append_if(modifiers.shift, self.shift); - } - - s - } -} - -// ---------------------------------------------------------------------------- - -/// A keyboard shortcut, e.g. `Ctrl+Alt+W`. -/// -/// Can be used with [`crate::InputState::consume_shortcut`] -/// and [`crate::Context::format_shortcut`]. -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct KeyboardShortcut { - pub modifiers: Modifiers, - - pub logical_key: Key, -} - -impl KeyboardShortcut { - pub const fn new(modifiers: Modifiers, logical_key: Key) -> Self { - Self { - modifiers, - logical_key, - } - } - - pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String { - let mut s = names.format(&self.modifiers, is_mac); - if !s.is_empty() { - s += names.concat; - } - if names.is_short { - s += self.logical_key.symbol_or_name(); - } else { - s += self.logical_key.name(); - } - s - } -} - -#[test] -fn format_kb_shortcut() { - let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F); - assert_eq!( - cmd_shift_f.format(&ModifierNames::NAMES, false), - "Ctrl+Shift+F" - ); - assert_eq!( - cmd_shift_f.format(&ModifierNames::NAMES, true), - "Shift+Cmd+F" - ); - assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⌃⇧F"); - assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F"); -} - -// ---------------------------------------------------------------------------- - -impl RawInput { - pub fn ui(&self, ui: &mut crate::Ui) { - let Self { - viewport_id, - viewports, - screen_rect, - max_texture_side, - time, - predicted_dt, - modifiers, - events, - hovered_files, - dropped_files, - focused, - system_theme, - safe_area_insets: safe_area, - } = self; - - ui.label(format!("Active viewport: {viewport_id:?}")); - let ordered_viewports = viewports - .iter() - .map(|(id, value)| (*id, value)) - .collect::>(); - for (id, viewport) in ordered_viewports { - ui.group(|ui| { - ui.label(format!("Viewport {id:?}")); - ui.push_id(id, |ui| { - viewport.ui(ui); - }); - }); - } - ui.label(format!("screen_rect: {screen_rect:?} points")); - - ui.label(format!("max_texture_side: {max_texture_side:?}")); - if let Some(time) = time { - ui.label(format!("time: {time:.3} s")); - } else { - 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}")); - ui.label(format!("system_theme: {system_theme:?}")); - ui.label(format!("safe_area: {safe_area:?}")); - ui.scope(|ui| { - ui.set_min_height(150.0); - ui.label(format!("events: {events:#?}")) - .on_hover_text("key presses etc"); - }); - } -} - -/// this is a `u64` as values of this kind can always be obtained by hashing -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct TouchDeviceId(pub u64); - -/// Unique identification of a touch occurrence (finger or pen or …). -/// A Touch ID is valid until the finger is lifted. -/// A new ID is used for the next touch. -#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct TouchId(pub u64); - -/// In what phase a touch event is in. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum TouchPhase { - /// User just placed a touch point on the touch surface - Start, - - /// User moves a touch point along the surface. This event is also sent when - /// any attributes (position, force, …) of the touch point change. - Move, - - /// User lifted the finger or pen from the surface, or slid off the edge of - /// the surface - End, - - /// Touch operation has been disrupted by something (various reasons are possible, - /// maybe a pop-up alert or any other kind of interruption which may not have - /// been intended by the user) - Cancel, -} - -/// The unit associated with the numeric value of a mouse wheel event -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum MouseWheelUnit { - /// Number of ui points (logical pixels) - Point, - - /// Number of lines - Line, - - /// Number of pages - Page, -} - -impl From for TouchId { - fn from(id: u64) -> Self { - Self(id) - } -} - -impl From for TouchId { - fn from(id: i32) -> Self { - Self(id as u64) - } -} - -impl From for TouchId { - fn from(id: u32) -> Self { - Self(id as u64) - } -} - -// ---------------------------------------------------------------------------- - -// TODO(emilk): generalize this to a proper event filter. -/// Controls which events that a focused widget will have exclusive access to. -/// -/// Currently this only controls a few special keyboard events, -/// but in the future this `struct` should be extended into a full callback thing. -/// -/// Any events not covered by the filter are given to the widget, but are not exclusive. -#[derive(Clone, Copy, Debug)] -pub struct EventFilter { - /// If `true`, pressing tab will act on the widget, - /// and NOT move focus away from the focused widget. - /// - /// Default: `false` - pub tab: bool, - - /// If `true`, pressing horizontal arrows will act on the - /// widget, and NOT move focus away from the focused widget. - /// - /// Default: `false` - pub horizontal_arrows: bool, - - /// If `true`, pressing vertical arrows will act on the - /// widget, and NOT move focus away from the focused widget. - /// - /// Default: `false` - pub vertical_arrows: bool, - - /// If `true`, pressing escape will act on the widget, - /// and NOT surrender focus from the focused widget. - /// - /// Default: `false` - pub escape: bool, -} - -#[expect(clippy::derivable_impls)] // let's be explicit -impl Default for EventFilter { - fn default() -> Self { - Self { - tab: false, - horizontal_arrows: false, - vertical_arrows: false, - escape: false, - } - } -} - -impl EventFilter { - pub fn matches(&self, event: &Event) -> bool { - if let Event::Key { key, .. } = event { - match key { - crate::Key::Tab => self.tab, - crate::Key::ArrowUp | crate::Key::ArrowDown => self.vertical_arrows, - crate::Key::ArrowRight | crate::Key::ArrowLeft => self.horizontal_arrows, - crate::Key::Escape => self.escape, - _ => true, - } - } else { - true - } - } -} - -/// The 'safe area' insets of the screen -/// -/// This represents the area taken up by the status bar, navigation controls, notches, -/// or any other items that obscure parts of the screen. -#[derive(Debug, PartialEq, Copy, Clone, Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct SafeAreaInsets(pub MarginF32); - -impl std::ops::Sub for Rect { - type Output = Self; - - fn sub(self, rhs: SafeAreaInsets) -> Self::Output { - self - rhs.0 - } -} diff --git a/crates/egui/src/data/input/dropped_file.rs b/crates/egui/src/data/input/dropped_file.rs new file mode 100644 index 000000000..39faceba8 --- /dev/null +++ b/crates/egui/src/data/input/dropped_file.rs @@ -0,0 +1,19 @@ +/// 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, + + /// Name of the file. Set by the `eframe` web backend. + pub name: String, + + /// With the `eframe` web backend, this is set to the mime-type of the file (if available). + pub mime: String, + + /// Set by the `eframe` web backend. + pub last_modified: Option, + + /// Set by the `eframe` web backend. + pub bytes: Option>, +} diff --git a/crates/egui/src/data/input/event.rs b/crates/egui/src/data/input/event.rs new file mode 100644 index 000000000..117a200b6 --- /dev/null +++ b/crates/egui/src/data/input/event.rs @@ -0,0 +1,186 @@ +use epaint::ColorImage; + +use crate::{ + Key, + emath::{Pos2, Vec2}, +}; + +use super::{ + ImeEvent, Modifiers, MouseWheelUnit, PointerButton, TouchDeviceId, TouchId, TouchPhase, +}; + +/// An input event generated by the integration. +/// +/// This only covers events that egui cares about. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum Event { + /// The integration detected a "copy" event (e.g. Cmd+C). + Copy, + + /// The integration detected a "cut" event (e.g. Cmd+X). + Cut, + + /// The integration detected a "paste" event (e.g. Cmd+V). + Paste(String), + + /// Text input, e.g. via keyboard. + /// + /// When the user presses enter/return, do not send a [`Text`](Event::Text) (just [`Key::Enter`]). + Text(String), + + /// A key was pressed or released. + /// + /// ## Note for integration authors + /// + /// Key events that has been processed by IMEs should not be sent to `egui`. + Key { + /// Most of the time, it's the logical key, heeding the active keymap -- for instance, if the user has Dvorak + /// keyboard layout, it will be taken into account. + /// + /// If it's impossible to determine the logical key on desktop platforms (say, in case of non-Latin letters), + /// `key` falls back to the value of the corresponding physical key. This is necessary for proper work of + /// standard shortcuts that only respond to Latin-based bindings (such as `Ctrl` + `V`). + key: Key, + + /// The physical key, corresponding to the actual position on the keyboard. + /// + /// This ignores keymaps, so it is not recommended to use this. + /// The only thing it makes sense for is things like games, + /// where e.g. the physical location of WSAD on QWERTY should always map to movement, + /// even if the user is using Dvorak or AZERTY. + /// + /// `eframe` does not (yet) implement this on web. + physical_key: Option, + + /// Was it pressed or released? + pressed: bool, + + /// If this is a `pressed` event, is it a key-repeat? + /// + /// On many platforms, holding down a key produces many repeated "pressed" events for it, so called key-repeats. + /// Sometimes you will want to ignore such events, and this lets you do that. + /// + /// egui will automatically detect such repeat events and mark them as such here. + /// Therefore, if you are writing an egui integration, you do not need to set this (just set it to `false`). + repeat: bool, + + /// The state of the modifier keys at the time of the event. + modifiers: Modifiers, + }, + + /// The mouse or touch moved to a new place. + PointerMoved(Pos2), + + /// The mouse moved, the units are unspecified. + /// Represents the actual movement of the mouse, without acceleration or clamped by screen edges. + /// `PointerMoved` and `MouseMoved` can be sent at the same time. + /// This event is optional. If the integration can not determine unfiltered motion it should not send this event. + MouseMoved(Vec2), + + /// A mouse button was pressed or released (or a touch started or stopped). + PointerButton { + /// Where is the pointer? + pos: Pos2, + + /// What mouse button? For touches, use [`PointerButton::Primary`]. + button: PointerButton, + + /// Was it the button/touch pressed this frame, or released? + pressed: bool, + + /// The state of the modifier keys at the time of the event. + modifiers: Modifiers, + }, + + /// The mouse left the screen, or the last/primary touch input disappeared. + /// + /// This means there is no longer a cursor on the screen for hovering etc. + /// + /// On touch-up first send `PointerButton{pressed: false, …}` followed by `PointerLeft`. + PointerGone, + + /// Zoom scale factor this frame (e.g. from a pinch gesture). + /// + /// * `zoom = 1`: no change. + /// * `zoom < 1`: pinch together + /// * `zoom > 1`: pinch spread + /// + /// Note that egui also implement zooming by holding `Ctrl` and scrolling the mouse wheel, + /// so integration need NOT emit this `Zoom` event in those cases, just [`Self::MouseWheel`]. + /// + /// As a user, check [`crate::InputState::smooth_scroll_delta`] to see if the user did any zooming this frame. + Zoom(f32), + + /// Rotation in radians this frame, measuring clockwise (e.g. from a rotation gesture). + Rotate(f32), + + /// IME Event + Ime(ImeEvent), + + /// On touch screens, report this *in addition to* + /// [`Self::PointerMoved`], [`Self::PointerButton`], [`Self::PointerGone`] + Touch { + /// Hashed device identifier (if available; may be zero). + /// Can be used to separate touches from different devices. + device_id: TouchDeviceId, + + /// Unique identifier of a finger/pen. Value is stable from touch down + /// to lift-up + id: TouchId, + + /// One of: start move end cancel. + phase: TouchPhase, + + /// Position of the touch (or where the touch was last detected) + pos: Pos2, + + /// Describes how hard the touch device was pressed. May always be `None` if the platform does + /// not support pressure sensitivity. + /// The value is in the range from 0.0 (no pressure) to 1.0 (maximum pressure). + force: Option, + }, + + /// A raw mouse wheel event as sent by the backend. + /// + /// Used for scrolling. + MouseWheel { + /// The unit of `delta`: points, lines, or pages. + unit: MouseWheelUnit, + + /// The direction of the vector indicates how to move the _content_ that is being viewed. + /// So if you get positive values, the content being viewed should move to the right and down, + /// revealing new things to the left and up. + /// + /// A positive X-value indicates the content is being moved right, + /// as when swiping right on a touch-screen or track-pad with natural scrolling. + /// + /// A positive Y-value indicates the content is being moved down, + /// as when swiping down on a touch-screen or track-pad with natural scrolling. + delta: Vec2, + + /// The phase of the scroll, useful for trackpads. + /// + /// If unknown set this to [`TouchPhase::Move`]. + phase: TouchPhase, + + /// The state of the modifier keys at the time of the event. + modifiers: Modifiers, + }, + + /// The native window gained or lost focused (e.g. the user clicked alt-tab). + WindowFocused(bool), + + /// An assistive technology (e.g. screen reader) requested an action. + AccessKitActionRequest(accesskit::ActionRequest), + + /// The reply of a screenshot requested with [`crate::ViewportCommand::Screenshot`]. + Screenshot { + viewport_id: crate::ViewportId, + + /// Whatever was passed to [`crate::ViewportCommand::Screenshot`]. + user_data: crate::UserData, + + image: std::sync::Arc, + }, +} diff --git a/crates/egui/src/data/input/event_filter.rs b/crates/egui/src/data/input/event_filter.rs new file mode 100644 index 000000000..6f0a81316 --- /dev/null +++ b/crates/egui/src/data/input/event_filter.rs @@ -0,0 +1,63 @@ +use super::Event; + +// TODO(emilk): generalize this to a proper event filter. +/// Controls which events that a focused widget will have exclusive access to. +/// +/// Currently this only controls a few special keyboard events, +/// but in the future this `struct` should be extended into a full callback thing. +/// +/// Any events not covered by the filter are given to the widget, but are not exclusive. +#[derive(Clone, Copy, Debug)] +pub struct EventFilter { + /// If `true`, pressing tab will act on the widget, + /// and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub tab: bool, + + /// If `true`, pressing horizontal arrows will act on the + /// widget, and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub horizontal_arrows: bool, + + /// If `true`, pressing vertical arrows will act on the + /// widget, and NOT move focus away from the focused widget. + /// + /// Default: `false` + pub vertical_arrows: bool, + + /// If `true`, pressing escape will act on the widget, + /// and NOT surrender focus from the focused widget. + /// + /// Default: `false` + pub escape: bool, +} + +#[expect(clippy::derivable_impls)] // let's be explicit +impl Default for EventFilter { + fn default() -> Self { + Self { + tab: false, + horizontal_arrows: false, + vertical_arrows: false, + escape: false, + } + } +} + +impl EventFilter { + pub fn matches(&self, event: &Event) -> bool { + if let Event::Key { key, .. } = event { + match key { + crate::Key::Tab => self.tab, + crate::Key::ArrowUp | crate::Key::ArrowDown => self.vertical_arrows, + crate::Key::ArrowRight | crate::Key::ArrowLeft => self.horizontal_arrows, + crate::Key::Escape => self.escape, + _ => true, + } + } else { + true + } + } +} diff --git a/crates/egui/src/data/input/hovered_file.rs b/crates/egui/src/data/input/hovered_file.rs new file mode 100644 index 000000000..0c558bf79 --- /dev/null +++ b/crates/egui/src/data/input/hovered_file.rs @@ -0,0 +1,10 @@ +/// A file about to be dropped into egui. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct HoveredFile { + /// Set by the `egui-winit` backend. + pub path: Option, + + /// With the `eframe` web backend, this is set to the mime-type of the file (if available). + pub mime: String, +} diff --git a/crates/egui/src/data/input/ime_event.rs b/crates/egui/src/data/input/ime_event.rs new file mode 100644 index 000000000..fb28c1cb4 --- /dev/null +++ b/crates/egui/src/data/input/ime_event.rs @@ -0,0 +1,25 @@ +/// IME event. +/// +/// See +#[derive(Clone, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ImeEvent { + /// Notifies when the IME was enabled. + #[deprecated = "No longer used by egui"] + Enabled, + + /// A new IME candidate is being suggested. + /// + /// An empty preedit string indicates that the IME has been dismissed, while + /// a non-empty preedit string indicates that the IME is active. + Preedit(String), + + /// IME composition ended with this final result. + /// + /// The IME is considered dismissed after this event. + Commit(String), + + /// Notifies when the IME was disabled. + #[deprecated = "No longer used by egui"] + Disabled, +} diff --git a/crates/egui/src/data/input/keyboard_shortcut.rs b/crates/egui/src/data/input/keyboard_shortcut.rs new file mode 100644 index 000000000..3998c5c45 --- /dev/null +++ b/crates/egui/src/data/input/keyboard_shortcut.rs @@ -0,0 +1,52 @@ +use crate::Key; + +use super::{ModifierNames, Modifiers}; + +/// A keyboard shortcut, e.g. `Ctrl+Alt+W`. +/// +/// Can be used with [`crate::InputState::consume_shortcut`] +/// and [`crate::Context::format_shortcut`]. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct KeyboardShortcut { + pub modifiers: Modifiers, + + pub logical_key: Key, +} + +impl KeyboardShortcut { + pub const fn new(modifiers: Modifiers, logical_key: Key) -> Self { + Self { + modifiers, + logical_key, + } + } + + pub fn format(&self, names: &ModifierNames<'_>, is_mac: bool) -> String { + let mut s = names.format(&self.modifiers, is_mac); + if !s.is_empty() { + s += names.concat; + } + if names.is_short { + s += self.logical_key.symbol_or_name(); + } else { + s += self.logical_key.name(); + } + s + } +} + +#[test] +fn format_kb_shortcut() { + let cmd_shift_f = KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, Key::F); + assert_eq!( + cmd_shift_f.format(&ModifierNames::NAMES, false), + "Ctrl+Shift+F" + ); + assert_eq!( + cmd_shift_f.format(&ModifierNames::NAMES, true), + "Shift+Cmd+F" + ); + assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, false), "⌃⇧F"); + assert_eq!(cmd_shift_f.format(&ModifierNames::SYMBOLS, true), "⇧⌘F"); +} diff --git a/crates/egui/src/data/input/mod.rs b/crates/egui/src/data/input/mod.rs new file mode 100644 index 000000000..b4e739471 --- /dev/null +++ b/crates/egui/src/data/input/mod.rs @@ -0,0 +1,33 @@ +//! The input needed by egui. + +mod dropped_file; +mod event; +mod event_filter; +mod hovered_file; +mod ime_event; +mod keyboard_shortcut; +mod modifier_names; +mod modifiers; +mod mouse_wheel_unit; +mod pointer_button; +mod raw_input; +mod safe_area_insets; +mod touch; +mod viewport_info; + +pub use self::{ + dropped_file::DroppedFile, + event::Event, + event_filter::EventFilter, + hovered_file::HoveredFile, + ime_event::ImeEvent, + keyboard_shortcut::KeyboardShortcut, + modifier_names::ModifierNames, + modifiers::Modifiers, + mouse_wheel_unit::MouseWheelUnit, + pointer_button::{NUM_POINTER_BUTTONS, PointerButton}, + raw_input::RawInput, + safe_area_insets::SafeAreaInsets, + touch::{TouchDeviceId, TouchId, TouchPhase}, + viewport_info::{ViewportEvent, ViewportInfo}, +}; diff --git a/crates/egui/src/data/input/modifier_names.rs b/crates/egui/src/data/input/modifier_names.rs new file mode 100644 index 000000000..585802530 --- /dev/null +++ b/crates/egui/src/data/input/modifier_names.rs @@ -0,0 +1,70 @@ +use super::Modifiers; + +/// Names of different modifier keys. +/// +/// Used to name modifiers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ModifierNames<'a> { + pub is_short: bool, + + pub alt: &'a str, + pub ctrl: &'a str, + pub shift: &'a str, + pub mac_cmd: &'a str, + pub mac_alt: &'a str, + + /// What goes between the names + pub concat: &'a str, +} + +impl ModifierNames<'static> { + /// ⌥ ⌃ ⇧ ⌘ - NOTE: not supported by the default egui font. + pub const SYMBOLS: Self = Self { + is_short: true, + alt: "⌥", + ctrl: "⌃", + shift: "⇧", + mac_cmd: "⌘", + mac_alt: "⌥", + concat: "", + }; + + /// Alt, Ctrl, Shift, Cmd + pub const NAMES: Self = Self { + is_short: false, + alt: "Alt", + ctrl: "Ctrl", + shift: "Shift", + mac_cmd: "Cmd", + mac_alt: "Option", + concat: "+", + }; +} + +impl ModifierNames<'_> { + pub fn format(&self, modifiers: &Modifiers, is_mac: bool) -> String { + let mut s = String::new(); + + let mut append_if = |modifier_is_active, modifier_name| { + if modifier_is_active { + if !s.is_empty() { + s += self.concat; + } + s += modifier_name; + } + }; + + if is_mac { + append_if(modifiers.ctrl, self.ctrl); + append_if(modifiers.shift, self.shift); + append_if(modifiers.alt, self.mac_alt); + append_if(modifiers.mac_cmd || modifiers.command, self.mac_cmd); + } else { + append_if(modifiers.ctrl || modifiers.command, self.ctrl); + append_if(modifiers.alt, self.alt); + append_if(modifiers.shift, self.shift); + } + + s + } +} diff --git a/crates/egui/src/data/input/modifiers.rs b/crates/egui/src/data/input/modifiers.rs new file mode 100644 index 000000000..2478ea343 --- /dev/null +++ b/crates/egui/src/data/input/modifiers.rs @@ -0,0 +1,410 @@ +use super::ModifierNames; + +/// State of the modifier keys. These must be fed to egui. +/// +/// The best way to compare [`Modifiers`] is by using [`Modifiers::matches_logically`] or [`Modifiers::matches_exact`]. +/// +/// To access the [`Modifiers`] you can use the [`crate::Context::input`] function +/// +/// ```rust +/// # let ctx = egui::Context::default(); +/// let modifiers = ctx.input(|i| i.modifiers); +/// ``` +/// +/// NOTE: For cross-platform uses, ALT+SHIFT is a bad combination of modifiers +/// as on mac that is how you type special characters, +/// so those key presses are usually not reported to egui. +#[derive(Clone, Copy, Default, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct Modifiers { + /// Either of the alt keys are down (option ⌥ on Mac). + pub alt: bool, + + /// Either of the control keys are down. + /// When checking for keyboard shortcuts, consider using [`Self::command`] instead. + pub ctrl: bool, + + /// Either of the shift keys are down. + pub shift: bool, + + /// The Mac ⌘ Command key. Should always be set to `false` on other platforms. + pub mac_cmd: bool, + + /// On Windows and Linux, set this to the same value as `ctrl`. + /// On Mac, this should be set whenever one of the ⌘ Command keys are down (same as `mac_cmd`). + /// This is so that egui can, for instance, select all text by checking for `command + A` + /// and it will work on both Mac and Windows. + pub command: bool, +} + +impl std::fmt::Debug for Modifiers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_none() { + return write!(f, "Modifiers::NONE"); + } + + let Self { + alt, + ctrl, + shift, + mac_cmd, + command, + } = *self; + + let mut debug = f.debug_struct("Modifiers"); + if alt { + debug.field("alt", &true); + } + if ctrl { + debug.field("ctrl", &true); + } + if shift { + debug.field("shift", &true); + } + if mac_cmd { + debug.field("mac_cmd", &true); + } + if command { + debug.field("command", &true); + } + debug.finish() + } +} + +impl Modifiers { + pub const NONE: Self = Self { + alt: false, + ctrl: false, + shift: false, + mac_cmd: false, + command: false, + }; + + pub const ALT: Self = Self { + alt: true, + ctrl: false, + shift: false, + mac_cmd: false, + command: false, + }; + pub const CTRL: Self = Self { + alt: false, + ctrl: true, + shift: false, + mac_cmd: false, + command: false, + }; + pub const SHIFT: Self = Self { + alt: false, + ctrl: false, + shift: true, + mac_cmd: false, + command: false, + }; + + /// The Mac ⌘ Command key + pub const MAC_CMD: Self = Self { + alt: false, + ctrl: false, + shift: false, + mac_cmd: true, + command: false, + }; + + /// On Mac: ⌘ Command key, elsewhere: Ctrl key + pub const COMMAND: Self = Self { + alt: false, + ctrl: false, + shift: false, + mac_cmd: false, + command: true, + }; + + /// ``` + /// # use egui::Modifiers; + /// assert_eq!( + /// Modifiers::CTRL | Modifiers::ALT, + /// Modifiers { ctrl: true, alt: true, ..Default::default() } + /// ); + /// assert_eq!( + /// Modifiers::ALT.plus(Modifiers::CTRL), + /// Modifiers::CTRL.plus(Modifiers::ALT), + /// ); + /// assert_eq!( + /// Modifiers::CTRL | Modifiers::ALT, + /// Modifiers::CTRL.plus(Modifiers::ALT), + /// ); + /// ``` + #[inline] + pub const fn plus(self, rhs: Self) -> Self { + Self { + alt: self.alt | rhs.alt, + ctrl: self.ctrl | rhs.ctrl, + shift: self.shift | rhs.shift, + mac_cmd: self.mac_cmd | rhs.mac_cmd, + command: self.command | rhs.command, + } + } + + #[inline] + pub fn is_none(&self) -> bool { + self == &Self::default() + } + + #[inline] + pub fn any(&self) -> bool { + !self.is_none() + } + + #[inline] + pub fn all(&self) -> bool { + self.alt && self.ctrl && self.shift && self.command + } + + /// Is shift the only pressed button? + #[inline] + pub fn shift_only(&self) -> bool { + self.shift && !(self.alt || self.command) + } + + /// true if only [`Self::ctrl`] or only [`Self::mac_cmd`] is pressed. + #[inline] + pub fn command_only(&self) -> bool { + !self.alt && !self.shift && self.command + } + + /// Checks that the `ctrl/cmd` matches, and that the `shift/alt` of the argument is a subset + /// of the pressed key (`self`). + /// + /// This means that if the pattern has not set `shift`, then `self` can have `shift` set or not. + /// + /// The reason is that many logical keys require `shift` or `alt` on some keyboard layouts. + /// For instance, in order to press `+` on an English keyboard, you need to press `shift` and `=`, + /// but a Swedish keyboard has dedicated `+` key. + /// So if you want to make a [`KeyboardShortcut`](crate::KeyboardShortcut) looking for `Cmd` + `+`, it makes sense + /// to ignore the shift key. + /// Similarly, the `Alt` key is sometimes used to type special characters. + /// + /// However, if the pattern (the argument) explicitly requires the `shift` or `alt` keys + /// to be pressed, then they must be pressed. + /// + /// # Example: + /// ``` + /// # use egui::Modifiers; + /// # let pressed_modifiers = Modifiers::default(); + /// if pressed_modifiers.matches_logically(Modifiers::ALT | Modifiers::SHIFT) { + /// // Alt and Shift are pressed, but not ctrl/command + /// } + /// ``` + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_logically(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.matches_logically(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_logically(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); + /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_logically(Modifiers::COMMAND)); + /// assert!(!Modifiers::COMMAND.matches_logically(Modifiers::MAC_CMD)); + /// ``` + pub fn matches_logically(&self, pattern: Self) -> bool { + if pattern.alt && !self.alt { + return false; + } + if pattern.shift && !self.shift { + return false; + } + + self.cmd_ctrl_matches(pattern) + } + + /// Check for equality but with proper handling of [`Self::command`]. + /// + /// `self` here are the currently pressed modifiers, + /// and the argument the pattern we are testing for. + /// + /// Note that this will require the `shift` and `alt` keys match, even though + /// these modifiers are sometimes required to produce some logical keys. + /// For instance, to press `+` on an English keyboard, you need to press `shift` and `=`, + /// but on a Swedish keyboard you can press the dedicated `+` key. + /// Therefore, you often want to use [`Self::matches_logically`] instead. + /// + /// # Example: + /// ``` + /// # use egui::Modifiers; + /// # let pressed_modifiers = Modifiers::default(); + /// if pressed_modifiers.matches_exact(Modifiers::ALT | Modifiers::SHIFT) { + /// // Alt and Shift are pressed, and nothing else + /// } + /// ``` + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_exact(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.matches_exact(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!(!(Modifiers::CTRL | Modifiers::SHIFT).matches_exact(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::CTRL)); + /// assert!((Modifiers::CTRL | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); + /// assert!((Modifiers::MAC_CMD | Modifiers::COMMAND).matches_exact(Modifiers::COMMAND)); + /// assert!(!Modifiers::COMMAND.matches_exact(Modifiers::MAC_CMD)); + /// ``` + pub fn matches_exact(&self, pattern: Self) -> bool { + // alt and shift must always match the pattern: + if pattern.alt != self.alt || pattern.shift != self.shift { + return false; + } + + self.cmd_ctrl_matches(pattern) + } + + /// Check if any of the modifiers match exactly. + /// + /// Returns true if the same modifier is pressed in `self` as in `pattern`, + /// for at least one modifier. + /// + /// ## Behavior: + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL)); + /// assert!(Modifiers::CTRL.matches_any(Modifiers::CTRL | Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).matches_any(Modifiers::CTRL)); + /// ``` + pub fn matches_any(&self, pattern: Self) -> bool { + if self.alt && pattern.alt { + return true; + } + if self.shift && pattern.shift { + return true; + } + if self.ctrl && pattern.ctrl { + return true; + } + if self.mac_cmd && pattern.mac_cmd { + return true; + } + if (self.mac_cmd || self.command || self.ctrl) && pattern.command { + return true; + } + false + } + + /// Checks only cmd/ctrl, not alt/shift. + /// + /// `self` here are the currently pressed modifiers, + /// and the argument the pattern we are testing for. + /// + /// This takes care to properly handle the difference between + /// [`Self::ctrl`], [`Self::command`] and [`Self::mac_cmd`]. + pub fn cmd_ctrl_matches(&self, pattern: Self) -> bool { + if pattern.mac_cmd { + // Mac-specific match: + if !self.mac_cmd { + return false; + } + if pattern.ctrl != self.ctrl { + return false; + } + return true; + } + + if !pattern.ctrl && !pattern.command { + // the pattern explicitly doesn't want any ctrl/command: + return !self.ctrl && !self.command; + } + + // if the pattern is looking for command, then `ctrl` may or may not be set depending on platform. + // if the pattern is looking for `ctrl`, then `command` may or may not be set depending on platform. + + if pattern.ctrl && !self.ctrl { + return false; + } + if pattern.command && !self.command { + return false; + } + + true + } + + /// Whether another set of modifiers is contained in this set of modifiers with proper handling of [`Self::command`]. + /// + /// ``` + /// # use egui::Modifiers; + /// assert!(Modifiers::default().contains(Modifiers::default())); + /// assert!(Modifiers::CTRL.contains(Modifiers::default())); + /// assert!(Modifiers::CTRL.contains(Modifiers::CTRL)); + /// assert!(Modifiers::CTRL.contains(Modifiers::COMMAND)); + /// assert!(Modifiers::MAC_CMD.contains(Modifiers::COMMAND)); + /// assert!(Modifiers::COMMAND.contains(Modifiers::MAC_CMD)); + /// assert!(Modifiers::COMMAND.contains(Modifiers::CTRL)); + /// assert!(!(Modifiers::ALT | Modifiers::CTRL).contains(Modifiers::SHIFT)); + /// assert!((Modifiers::CTRL | Modifiers::SHIFT).contains(Modifiers::CTRL)); + /// assert!(!Modifiers::CTRL.contains(Modifiers::CTRL | Modifiers::SHIFT)); + /// ``` + pub fn contains(&self, query: Self) -> bool { + if query == Self::default() { + return true; + } + + let Self { + alt, + ctrl, + shift, + mac_cmd, + command, + } = *self; + + if alt && query.alt { + return self.contains(Self { + alt: false, + ..query + }); + } + if shift && query.shift { + return self.contains(Self { + shift: false, + ..query + }); + } + + if (ctrl || command) && (query.ctrl || query.command) { + return self.contains(Self { + command: false, + ctrl: false, + ..query + }); + } + if (mac_cmd || command) && (query.mac_cmd || query.command) { + return self.contains(Self { + mac_cmd: false, + command: false, + ..query + }); + } + + false + } +} + +impl std::ops::BitOr for Modifiers { + type Output = Self; + + #[inline] + fn bitor(self, rhs: Self) -> Self { + self.plus(rhs) + } +} + +impl std::ops::BitOrAssign for Modifiers { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +impl Modifiers { + pub fn ui(&self, ui: &mut crate::Ui) { + ui.label(ModifierNames::NAMES.format(self, ui.ctx().os().is_mac())); + } +} diff --git a/crates/egui/src/data/input/mouse_wheel_unit.rs b/crates/egui/src/data/input/mouse_wheel_unit.rs new file mode 100644 index 000000000..d543d01a8 --- /dev/null +++ b/crates/egui/src/data/input/mouse_wheel_unit.rs @@ -0,0 +1,13 @@ +/// The unit associated with the numeric value of a mouse wheel event +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum MouseWheelUnit { + /// Number of ui points (logical pixels) + Point, + + /// Number of lines + Line, + + /// Number of pages + Page, +} diff --git a/crates/egui/src/data/input/pointer_button.rs b/crates/egui/src/data/input/pointer_button.rs new file mode 100644 index 000000000..cfab22a09 --- /dev/null +++ b/crates/egui/src/data/input/pointer_button.rs @@ -0,0 +1,23 @@ +/// Mouse button (or similar for touch input) +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum PointerButton { + /// The primary mouse button is usually the left one. + Primary = 0, + + /// The secondary mouse button is usually the right one, + /// and most often used for context menus or other optional things. + Secondary = 1, + + /// The tertiary mouse button is usually the middle mouse button (e.g. clicking the scroll wheel). + Middle = 2, + + /// The first extra mouse button on some mice. In web typically corresponds to the Browser back button. + Extra1 = 3, + + /// The second extra mouse button on some mice. In web typically corresponds to the Browser forward button. + Extra2 = 4, +} + +/// Number of pointer buttons supported by egui, i.e. the number of possible states of [`PointerButton`]. +pub const NUM_POINTER_BUTTONS: usize = 5; diff --git a/crates/egui/src/data/input/raw_input.rs b/crates/egui/src/data/input/raw_input.rs new file mode 100644 index 000000000..2ba2caada --- /dev/null +++ b/crates/egui/src/data/input/raw_input.rs @@ -0,0 +1,225 @@ +use crate::{OrderedViewportIdMap, Theme, ViewportId, ViewportIdMap, emath::Rect}; + +use super::{DroppedFile, Event, HoveredFile, Modifiers, SafeAreaInsets, ViewportInfo}; + +/// What the integrations provides to egui at the start of each frame. +/// +/// Set the values that make sense, leave the rest at their `Default::default()`. +/// +/// You can check if `egui` is using the inputs using +/// [`crate::Context::egui_wants_pointer_input`] and [`crate::Context::egui_wants_keyboard_input`]. +/// +/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner. +/// +/// 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)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct RawInput { + /// The id of the active viewport. + pub viewport_id: ViewportId, + + /// Information about all egui viewports. + pub viewports: ViewportIdMap, + + /// The insets used to only render content in a mobile safe area + /// + /// `None` will be treated as "same as last frame" + pub safe_area_insets: Option, + + /// Position and size of the area that egui should use, in points. + /// Usually you would set this to + /// + /// `Some(Rect::from_min_size(Default::default(), screen_size_in_points))`. + /// + /// but you could also constrain egui to some smaller portion of your window if you like. + /// + /// `None` will be treated as "same as last frame", with the default being a very big area. + pub screen_rect: Option, + + /// Maximum size of one side of the font texture. + /// + /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. + /// + /// The default is a very small (but very portable) 2048. + pub max_texture_side: Option, + + /// Monotonically increasing time, in seconds. Relative to whatever. Used for animations. + /// If `None` is provided, egui will assume a time delta of `predicted_dt` (default 1/60 seconds). + pub time: Option, + + /// Should be set to the expected time between frames when painting at vsync speeds. + /// The default for this is 1/60. + /// 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, + /// but you can check if egui is using the keyboard with [`crate::Context::egui_wants_keyboard_input`] + /// and/or the pointer (mouse/touch) with [`crate::Context::egui_is_using_pointer`]. + pub events: Vec, + + /// Dragged files hovering over egui. + pub hovered_files: Vec, + + /// Dragged files dropped into egui. + /// + /// 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, + + /// The native window has the keyboard focus (i.e. is receiving key presses). + /// + /// False when the user alt-tab away from the application, for instance. + pub focused: bool, + + /// Does the OS use dark or light mode? + /// + /// `None` means "don't know". + pub system_theme: Option, +} + +impl Default for RawInput { + fn default() -> Self { + Self { + viewport_id: ViewportId::ROOT, + viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), + screen_rect: None, + 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(), + focused: true, // integrations opt into global focus tracking + system_theme: None, + safe_area_insets: Default::default(), + } + } +} + +impl RawInput { + /// Info about the active viewport + #[inline] + pub fn viewport(&self) -> &ViewportInfo { + self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") + } + + /// Helper: move volatile (deltas and events), clone the rest. + /// + /// * [`Self::hovered_files`] is cloned. + /// * [`Self::dropped_files`] is moved. + pub fn take(&mut self) -> Self { + Self { + viewport_id: self.viewport_id, + viewports: self + .viewports + .iter_mut() + .map(|(id, info)| (*id, info.take())) + .collect(), + screen_rect: self.screen_rect.take(), + safe_area_insets: self.safe_area_insets.take(), + 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), + focused: self.focused, + system_theme: self.system_theme, + } + } + + /// Add on new input. + pub fn append(&mut self, newer: Self) { + let Self { + viewport_id: viewport_ids, + viewports, + screen_rect, + max_texture_side, + time, + predicted_dt, + modifiers, + mut events, + mut hovered_files, + mut dropped_files, + focused, + system_theme, + safe_area_insets: safe_area, + } = newer; + + self.viewport_id = viewport_ids; + self.viewports = viewports; + self.screen_rect = screen_rect.or(self.screen_rect); + 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); + self.focused = focused; + self.system_theme = system_theme; + self.safe_area_insets = safe_area; + } +} + +impl RawInput { + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + viewport_id, + viewports, + screen_rect, + max_texture_side, + time, + predicted_dt, + modifiers, + events, + hovered_files, + dropped_files, + focused, + system_theme, + safe_area_insets: safe_area, + } = self; + + ui.label(format!("Active viewport: {viewport_id:?}")); + let ordered_viewports = viewports + .iter() + .map(|(id, value)| (*id, value)) + .collect::>(); + for (id, viewport) in ordered_viewports { + ui.group(|ui| { + ui.label(format!("Viewport {id:?}")); + ui.push_id(id, |ui| { + viewport.ui(ui); + }); + }); + } + ui.label(format!("screen_rect: {screen_rect:?} points")); + + ui.label(format!("max_texture_side: {max_texture_side:?}")); + if let Some(time) = time { + ui.label(format!("time: {time:.3} s")); + } else { + 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}")); + ui.label(format!("system_theme: {system_theme:?}")); + ui.label(format!("safe_area: {safe_area:?}")); + ui.scope(|ui| { + ui.set_min_height(150.0); + ui.label(format!("events: {events:#?}")) + .on_hover_text("key presses etc"); + }); + } +} diff --git a/crates/egui/src/data/input/safe_area_insets.rs b/crates/egui/src/data/input/safe_area_insets.rs new file mode 100644 index 000000000..914d227c2 --- /dev/null +++ b/crates/egui/src/data/input/safe_area_insets.rs @@ -0,0 +1,19 @@ +use epaint::MarginF32; + +use crate::emath::Rect; + +/// The 'safe area' insets of the screen +/// +/// This represents the area taken up by the status bar, navigation controls, notches, +/// or any other items that obscure parts of the screen. +#[derive(Debug, PartialEq, Copy, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct SafeAreaInsets(pub MarginF32); + +impl std::ops::Sub for Rect { + type Output = Self; + + fn sub(self, rhs: SafeAreaInsets) -> Self::Output { + self - rhs.0 + } +} diff --git a/crates/egui/src/data/input/touch.rs b/crates/egui/src/data/input/touch.rs new file mode 100644 index 000000000..e269e437c --- /dev/null +++ b/crates/egui/src/data/input/touch.rs @@ -0,0 +1,50 @@ +/// this is a `u64` as values of this kind can always be obtained by hashing +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct TouchDeviceId(pub u64); + +/// Unique identification of a touch occurrence (finger or pen or …). +/// A Touch ID is valid until the finger is lifted. +/// A new ID is used for the next touch. +#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct TouchId(pub u64); + +/// In what phase a touch event is in. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum TouchPhase { + /// User just placed a touch point on the touch surface + Start, + + /// User moves a touch point along the surface. This event is also sent when + /// any attributes (position, force, …) of the touch point change. + Move, + + /// User lifted the finger or pen from the surface, or slid off the edge of + /// the surface + End, + + /// Touch operation has been disrupted by something (various reasons are possible, + /// maybe a pop-up alert or any other kind of interruption which may not have + /// been intended by the user) + Cancel, +} + +impl From for TouchId { + fn from(id: u64) -> Self { + Self(id) + } +} + +impl From for TouchId { + fn from(id: i32) -> Self { + Self(id as u64) + } +} + +impl From for TouchId { + fn from(id: u32) -> Self { + Self(id as u64) + } +} diff --git a/crates/egui/src/data/input/viewport_info.rs b/crates/egui/src/data/input/viewport_info.rs new file mode 100644 index 000000000..774ca1e6e --- /dev/null +++ b/crates/egui/src/data/input/viewport_info.rs @@ -0,0 +1,217 @@ +use crate::emath::{Rect, Vec2}; + +/// An input event from the backend into egui, about a specific [viewport](crate::viewport). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum ViewportEvent { + /// The user clicked the close-button on the window, or similar. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. + /// + /// This even will wake up both the child and parent viewport. + Close, +} + +/// Information about the current viewport, given as input each frame. +/// +/// `None` means "unknown". +/// +/// All units are in ui "points", which can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`]; +#[derive(Clone, Debug, Default, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ViewportInfo { + /// Parent viewport, if known. + pub parent: Option, + + /// Name of the viewport, if known. + pub title: Option, + + pub events: Vec, + + /// The OS native pixels-per-point. + /// + /// This should always be set, if known. + /// + /// On web this takes browser scaling into account, + /// and corresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. + pub native_pixels_per_point: Option, + + /// Current monitor size in egui points. + pub monitor_size: Option, + + /// The inner rectangle of the native window, in monitor space and ui points scale. + /// + /// This is the content rectangle of the viewport. + /// + /// **`eframe` notes**: + /// + /// On Android / Wayland, this will always be `None` since getting the + /// position of the window is not possible. + pub inner_rect: Option, + + /// The outer rectangle of the native window, in monitor space and ui points scale. + /// + /// This is the content rectangle plus decoration chrome. + /// + /// **`eframe` notes**: + /// + /// On Android / Wayland, this will always be `None` since getting the + /// position of the window is not possible. + pub outer_rect: Option, + + /// Are we minimized? + pub minimized: Option, + + /// Are we maximized? + pub maximized: Option, + + /// Are we in fullscreen mode? + pub fullscreen: Option, + + /// Is the window focused and able to receive input? + /// + /// This should be the same as [`RawInput::focused`](crate::RawInput::focused). + pub focused: Option, + + /// Is the window fully occluded (completely covered) by another window? + /// + /// Not all platforms support this. + /// On platforms that don't, this will be `None` or `Some(false)`. + pub occluded: Option, +} + +impl ViewportInfo { + /// Is the window considered visible for rendering purposes? + /// + /// A window is not visible if it is minimized or occluded. + /// When not visible, the UI is not painted and rendering is skipped, + /// but application logic may still be executed by some integrations. + pub fn visible(&self) -> Option { + match (self.minimized, self.occluded) { + (Some(true), _) | (_, Some(true)) => Some(false), + (Some(false), Some(false)) => Some(true), + (_, None) | (None, _) => None, + } + } + + /// This viewport has been told to close. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. + pub fn close_requested(&self) -> bool { + self.events.contains(&ViewportEvent::Close) + } + + /// Helper: move [`Self::events`], clone the other fields. + pub fn take(&mut self) -> Self { + Self { + parent: self.parent, + title: self.title.clone(), + events: std::mem::take(&mut self.events), + native_pixels_per_point: self.native_pixels_per_point, + monitor_size: self.monitor_size, + inner_rect: self.inner_rect, + outer_rect: self.outer_rect, + minimized: self.minimized, + maximized: self.maximized, + fullscreen: self.fullscreen, + focused: self.focused, + occluded: self.occluded, + } + } + + pub fn ui(&self, ui: &mut crate::Ui) { + let Self { + parent, + title, + events, + native_pixels_per_point, + monitor_size, + inner_rect, + outer_rect, + minimized, + maximized, + fullscreen, + focused, + occluded, + } = self; + + crate::Grid::new("viewport_info").show(ui, |ui| { + ui.label("Parent:"); + ui.label(opt_as_str(parent)); + ui.end_row(); + + ui.label("Title:"); + ui.label(opt_as_str(title)); + ui.end_row(); + + ui.label("Events:"); + ui.label(format!("{events:?}")); + ui.end_row(); + + ui.label("Native pixels-per-point:"); + ui.label(opt_as_str(native_pixels_per_point)); + ui.end_row(); + + ui.label("Monitor size:"); + ui.label(opt_as_str(monitor_size)); + ui.end_row(); + + ui.label("Inner rect:"); + ui.label(opt_rect_as_string(inner_rect)); + ui.end_row(); + + ui.label("Outer rect:"); + ui.label(opt_rect_as_string(outer_rect)); + ui.end_row(); + + ui.label("Minimized:"); + ui.label(opt_as_str(minimized)); + ui.end_row(); + + ui.label("Maximized:"); + ui.label(opt_as_str(maximized)); + ui.end_row(); + + ui.label("Fullscreen:"); + ui.label(opt_as_str(fullscreen)); + ui.end_row(); + + ui.label("Focused:"); + ui.label(opt_as_str(focused)); + ui.end_row(); + + ui.label("Occluded:"); + ui.label(opt_as_str(occluded)); + ui.end_row(); + + let visible = self.visible(); + + ui.label("Visible:"); + ui.label(opt_as_str(&visible)); + ui.end_row(); + + #[expect(clippy::ref_option)] + fn opt_rect_as_string(v: &Option) -> String { + v.as_ref().map_or(String::new(), |r| { + format!("Pos: {:?}, size: {:?}", r.min, r.size()) + }) + } + + #[expect(clippy::ref_option)] + fn opt_as_str(v: &Option) -> String { + v.as_ref().map_or(String::new(), |v| format!("{v:?}")) + } + }); + } +}