From a7539b270a5e1384abaaebc23d8090d56bb205a5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 10 Dec 2024 15:32:43 +0100 Subject: [PATCH 01/34] Remove `Order::PanelResizeLine` (#5455) This was a hack that is no longer in use --- crates/egui/src/layers.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index b44daa41858..7966e12ad2a 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -11,9 +11,6 @@ pub enum Order { /// Painted behind all floating windows Background, - /// Special layer between panels and windows - PanelResizeLine, - /// Normal moveable windows that you reorder by click Middle, @@ -30,10 +27,9 @@ pub enum Order { } impl Order { - const COUNT: usize = 6; + const COUNT: usize = 5; const ALL: [Self; Self::COUNT] = [ Self::Background, - Self::PanelResizeLine, Self::Middle, Self::Foreground, Self::Tooltip, @@ -44,12 +40,9 @@ impl Order { #[inline(always)] pub fn allow_interaction(&self) -> bool { match self { - Self::Background - | Self::PanelResizeLine - | Self::Middle - | Self::Foreground - | Self::Tooltip - | Self::Debug => true, + Self::Background | Self::Middle | Self::Foreground | Self::Tooltip | Self::Debug => { + true + } } } @@ -57,7 +50,6 @@ impl Order { pub fn short_debug_format(&self) -> &'static str { match self { Self::Background => "backg", - Self::PanelResizeLine => "panel", Self::Middle => "middl", Self::Foreground => "foreg", Self::Tooltip => "toolt", From 9b1ae6b880d57b30586b1512a47ce01e718f36dd Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 10 Dec 2024 16:06:29 +0100 Subject: [PATCH 02/34] Add CHANGELOG.md for egui_kittest --- CHANGELOG.md | 9 ++++++++- crates/egui_kittest/CHANGELOG.md | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 crates/egui_kittest/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eeeb1e0b87..17a2b7a9585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,14 @@ # egui changelog All notable changes to the `egui` crate will be documented in this file. -NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! +This is just the changelog for the core `egui` crate. Every crate in this repository has their own changelog: +* [`epaint` changelog](crates/epaint/CHANGELOG.md) +* [`egui-winit` changelog](crates/egui-winit/CHANGELOG.md) +* [`egui-wgpu` changelog](crates/egui-wgpu/CHANGELOG.md) +* [`egui_kittest` changelog](crates/egui_kittest/CHANGELOG.md) +* [`egui_glow` changelog](crates/egui_glow/CHANGELOG.md) +* [`ecolor` changelog](crates/ecolor/CHANGELOG.md) +* [`eframe` changelog](crates/eframe/CHANGELOG.md) 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. diff --git a/crates/egui_kittest/CHANGELOG.md b/crates/egui_kittest/CHANGELOG.md new file mode 100644 index 00000000000..8f9a8aa2837 --- /dev/null +++ b/crates/egui_kittest/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog for egui_kittest +All notable changes to the `egui_kittest` crate will be noted in this file. + + +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. + + From 53a926a428c7e47b9845505adab9ce08a9e84b6c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 10 Dec 2024 16:09:03 +0100 Subject: [PATCH 03/34] Update MSRV to 1.80 (#5457) Because some dependencies now require it, see: * https://github.com/emilk/egui/pull/5456 --- .github/workflows/deploy_web_demo.yml | 2 +- .github/workflows/rust.yml | 14 +++++++------- Cargo.toml | 11 ++++++----- clippy.toml | 2 +- crates/eframe/src/web/app_runner.rs | 2 +- crates/egui-wgpu/src/winit.rs | 2 +- crates/egui/src/containers/resize.rs | 4 ++-- crates/egui/src/grid.rs | 4 ++-- crates/egui/src/lib.rs | 2 +- crates/egui/src/widgets/color_picker.rs | 5 ++++- crates/egui/src/widgets/text_edit/state.rs | 1 + crates/egui_demo_app/src/frame_history.rs | 2 +- crates/egui_extras/src/table.rs | 2 +- crates/egui_kittest/src/wgpu.rs | 2 +- crates/epaint/src/bezier.rs | 6 +++++- crates/epaint/src/texture_handle.rs | 2 ++ examples/confirm_exit/Cargo.toml | 2 +- examples/custom_3d_glow/Cargo.toml | 2 +- examples/custom_font/Cargo.toml | 2 +- examples/custom_font_style/Cargo.toml | 2 +- examples/custom_keypad/Cargo.toml | 2 +- examples/custom_style/Cargo.toml | 2 +- examples/custom_window_frame/Cargo.toml | 2 +- examples/file_dialog/Cargo.toml | 2 +- examples/hello_world/Cargo.toml | 2 +- examples/hello_world_par/Cargo.toml | 2 +- examples/hello_world_simple/Cargo.toml | 2 +- examples/images/Cargo.toml | 2 +- examples/keyboard_events/Cargo.toml | 2 +- examples/multiple_viewports/Cargo.toml | 2 +- examples/puffin_profiler/Cargo.toml | 2 +- examples/screenshot/Cargo.toml | 2 +- examples/serial_windows/Cargo.toml | 2 +- examples/user_attention/Cargo.toml | 2 +- rust-toolchain | 2 +- scripts/check.sh | 2 +- scripts/clippy_wasm/clippy.toml | 2 +- tests/test_egui_extras_compilation/Cargo.toml | 2 +- tests/test_inline_glow_paint/Cargo.toml | 2 +- tests/test_size_pass/Cargo.toml | 2 +- tests/test_ui_stack/Cargo.toml | 2 +- tests/test_viewports/Cargo.toml | 2 +- 42 files changed, 63 insertions(+), 52 deletions(-) diff --git a/.github/workflows/deploy_web_demo.yml b/.github/workflows/deploy_web_demo.yml index c5924a3b0f0..6eb54696139 100644 --- a/.github/workflows/deploy_web_demo.yml +++ b/.github/workflows/deploy_web_demo.yml @@ -39,7 +39,7 @@ jobs: with: profile: minimal target: wasm32-unknown-unknown - toolchain: 1.79.0 + toolchain: 1.80.0 override: true - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e669fe4c543..e2a5896980e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 - name: Install packages (Linux) if: runner.os == 'Linux' @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 targets: wasm32-unknown-unknown - run: sudo apt-get update && sudo apt-get install libgtk-3-dev libatk1.0-dev @@ -155,7 +155,7 @@ jobs: - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 with: - rust-version: "1.79.0" + rust-version: "1.80.0" log-level: error command: check arguments: --target ${{ matrix.target }} @@ -170,7 +170,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 targets: aarch64-linux-android - name: Set up cargo cache @@ -189,7 +189,7 @@ jobs: - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 targets: aarch64-apple-ios - name: Set up cargo cache @@ -208,7 +208,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 @@ -232,7 +232,7 @@ jobs: lfs: true - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.79.0 + toolchain: 1.80.0 - name: Set up cargo cache uses: Swatinem/rust-cache@v2 diff --git a/Cargo.toml b/Cargo.toml index ab53c6cfbb8..6b50ac23a0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ [workspace.package] edition = "2021" license = "MIT OR Apache-2.0" -rust-version = "1.79" +rust-version = "1.80" version = "0.29.1" @@ -106,13 +106,13 @@ winit = { version = "0.30.5", default-features = false } unsafe_code = "deny" elided_lifetimes_in_paths = "warn" -future_incompatible = "warn" -nonstandard_style = "warn" -rust_2018_idioms = "warn" +future_incompatible = { level = "warn", priority = -1 } +nonstandard_style = { level = "warn", priority = -1 } +rust_2018_idioms = { level = "warn", priority = -1 } rust_2021_prelude_collisions = "warn" semicolon_in_expressions_from_macros = "warn" trivial_numeric_casts = "warn" -unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 +unsafe_op_in_unsafe_fn = "warn" # `unsafe_op_in_unsafe_fn` may become the default in future Rust versions: https://github.com/rust-lang/rust/issues/71668 unused_extern_crates = "warn" unused_import_braces = "warn" unused_lifetimes = "warn" @@ -233,6 +233,7 @@ ref_patterns = "warn" rest_pat_in_fully_bound_structs = "warn" same_functions_in_if_condition = "warn" semicolon_if_nothing_returned = "warn" +single_char_pattern = "warn" single_match_else = "warn" str_split_at_newline = "warn" str_to_string = "warn" diff --git a/clippy.toml b/clippy.toml index cd4a25cab4f..9e5fdd1e593 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ # ----------------------------------------------------------------------------- # Section identical to scripts/clippy_wasm/clippy.toml: -msrv = "1.79" +msrv = "1.80" allow-unwrap-in-tests = true diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 00cc8f0c182..2f8f78d0818 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -260,7 +260,7 @@ impl AppRunner { self.frame.info.cpu_usage = Some(cpu_usage_seconds); } - fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { + fn handle_platform_output(&self, platform_output: egui::PlatformOutput) { #[cfg(feature = "web_screen_reader")] if self.egui_ctx.options(|o| o.screen_reader) { super::screen_reader::speak(&platform_output.events_description()); diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index abf88518368..161773117b3 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -728,7 +728,7 @@ impl Painter { .retain(|id, _| active_viewports.contains(id)); } - #[allow(clippy::unused_self)] + #[allow(clippy::needless_pass_by_ref_mut, clippy::unused_self)] pub fn destroy(&mut self) { // TODO(emilk): something here? } diff --git a/crates/egui/src/containers/resize.rs b/crates/egui/src/containers/resize.rs index 458bf11229d..84f687783aa 100644 --- a/crates/egui/src/containers/resize.rs +++ b/crates/egui/src/containers/resize.rs @@ -205,7 +205,7 @@ struct Prepared { } impl Resize { - fn begin(&mut self, ui: &mut Ui) -> Prepared { + fn begin(&self, ui: &mut Ui) -> Prepared { let position = ui.available_rect_before_wrap().min; let id = self.id.unwrap_or_else(|| { let id_salt = self.id_salt.unwrap_or_else(|| Id::new("resize")); @@ -295,7 +295,7 @@ impl Resize { } } - pub fn show(mut self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { + pub fn show(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> R { let mut prepared = self.begin(ui); let ret = add_contents(&mut prepared.content_ui); self.end(ui, prepared); diff --git a/crates/egui/src/grid.rs b/crates/egui/src/grid.rs index 3c4986fcad0..0342f6f5227 100644 --- a/crates/egui/src/grid.rs +++ b/crates/egui/src/grid.rs @@ -227,7 +227,7 @@ impl GridLayout { self.col += 1; } - fn paint_row(&mut self, cursor: &Rect, painter: &Painter) { + fn paint_row(&self, cursor: &Rect, painter: &Painter) { // handle row color painting based on color-picker function let Some(color_picker) = self.color_picker.as_ref() else { return; @@ -450,7 +450,7 @@ impl Grid { ui.allocate_new_ui(ui_builder, |ui| { ui.horizontal(|ui| { let is_color = color_picker.is_some(); - let mut grid = GridLayout { + let grid = GridLayout { num_columns, color_picker, min_cell_size: vec2(min_col_width, min_row_height), diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 6d8c6a3459d..8c70ca5a812 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -3,7 +3,7 @@ //! Try the live web demo: . Read more about egui at . //! //! `egui` is in heavy development, with each new version having breaking changes. -//! You need to have rust 1.79.0 or later to use `egui`. +//! You need to have rust 1.80.0 or later to use `egui`. //! //! To quickly get started with egui, you can take a look at [`eframe_template`](https://github.com/emilk/eframe_template) //! which uses [`eframe`](https://docs.rs/eframe). diff --git a/crates/egui/src/widgets/color_picker.rs b/crates/egui/src/widgets/color_picker.rs index a00d6f9ac3b..9a605905968 100644 --- a/crates/egui/src/widgets/color_picker.rs +++ b/crates/egui/src/widgets/color_picker.rs @@ -165,8 +165,11 @@ fn color_slider_1d(ui: &mut Ui, value: &mut f32, color_at: impl Fn(f32) -> Color /// * `x_value` - X axis, either saturation or value (0.0-1.0). /// * `y_value` - Y axis, either saturation or value (0.0-1.0). /// * `color_at` - A function that dictates how the mix of saturation and value will be displayed in the 2d slider. -/// E.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: top-left: white \[s: 0.0, v: 1.0], top-right: fully saturated color \[s: 1.0, v: 1.0], bottom-right: black \[s: 0.0, v: 1.0]. /// +/// e.g.: `|x_value, y_value| HsvaGamma { h: 1.0, s: x_value, v: y_value, a: 1.0 }.into()` displays the colors as follows: +/// * top-left: white `[s: 0.0, v: 1.0]` +/// * top-right: fully saturated color `[s: 1.0, v: 1.0]` +/// * bottom-right: black `[s: 0.0, v: 1.0].` fn color_slider_2d( ui: &mut Ui, x_value: &mut f32, diff --git a/crates/egui/src/widgets/text_edit/state.rs b/crates/egui/src/widgets/text_edit/state.rs index cbbb6071484..c10a88274ef 100644 --- a/crates/egui/src/widgets/text_edit/state.rs +++ b/crates/egui/src/widgets/text_edit/state.rs @@ -89,6 +89,7 @@ impl TextEditState { self.undoer.lock().clone() } + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_undoer(&mut self, undoer: TextEditUndoer) { *self.undoer.lock() = undoer; } diff --git a/crates/egui_demo_app/src/frame_history.rs b/crates/egui_demo_app/src/frame_history.rs index 535d6d9f95f..a6a3fbeeb3f 100644 --- a/crates/egui_demo_app/src/frame_history.rs +++ b/crates/egui_demo_app/src/frame_history.rs @@ -32,7 +32,7 @@ impl FrameHistory { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } - pub fn ui(&mut self, ui: &mut egui::Ui) { + pub fn ui(&self, ui: &mut egui::Ui) { ui.label(format!( "Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time() diff --git a/crates/egui_extras/src/table.rs b/crates/egui_extras/src/table.rs index 2be2e25bb77..ec88990305b 100644 --- a/crates/egui_extras/src/table.rs +++ b/crates/egui_extras/src/table.rs @@ -1227,7 +1227,7 @@ impl<'a> TableBody<'a> { // Capture the hover information for the just created row. This is used in the next render // to ensure that the entire row is highlighted. - fn capture_hover_state(&mut self, response: &Option, row_index: usize) { + fn capture_hover_state(&self, response: &Option, row_index: usize) { let is_row_hovered = response.as_ref().map_or(false, |r| r.hovered()); if is_row_hovered { self.layout diff --git a/crates/egui_kittest/src/wgpu.rs b/crates/egui_kittest/src/wgpu.rs index d2cda112af7..4c3001fa72b 100644 --- a/crates/egui_kittest/src/wgpu.rs +++ b/crates/egui_kittest/src/wgpu.rs @@ -60,7 +60,7 @@ impl TestRenderer { } /// Render the [`Harness`] and return the resulting image. - pub fn render(&mut self, harness: &Harness<'_, State>) -> RgbaImage { + pub fn render(&self, harness: &Harness<'_, State>) -> RgbaImage { // We need to create a new renderer each time we render, since the renderer stores // textures related to the Harnesses' egui Context. // Calling the renderer from different Harnesses would cause problems if we store the renderer. diff --git a/crates/epaint/src/bezier.rs b/crates/epaint/src/bezier.rs index 05fff89298b..cab0d29f918 100644 --- a/crates/epaint/src/bezier.rs +++ b/crates/epaint/src/bezier.rs @@ -207,17 +207,21 @@ impl CubicBezierShape { /// B.x = (P3.x - 3 * P2.x + 3 * P1.x - P0.x) * t^3 + (3 * P2.x - 6 * P1.x + 3 * P0.x) * t^2 + (3 * P1.x - 3 * P0.x) * t + P0.x /// B.y = (P3.y - 3 * P2.y + 3 * P1.y - P0.y) * t^3 + (3 * P2.y - 6 * P1.y + 3 * P0.y) * t^2 + (3 * P1.y - 3 * P0.y) * t + P0.y /// Combine the above three equations and iliminate B.x and B.y, we get: + /// ```text /// t^3 * ( (P3.x - 3*P2.x + 3*P1.x - P0.x) * (P3.y - P0.y) - (P3.y - 3*P2.y + 3*P1.y - P0.y) * (P3.x - P0.x)) /// + t^2 * ( (3 * P2.x - 6 * P1.x + 3 * P0.x) * (P3.y - P0.y) - (3 * P2.y - 6 * P1.y + 3 * P0.y) * (P3.x - P0.x)) /// + t^1 * ( (3 * P1.x - 3 * P0.x) * (P3.y - P0.y) - (3 * P1.y - 3 * P0.y) * (P3.x - P0.x)) /// + (P0.x * (P3.y - P0.y) - P0.y * (P3.x - P0.x)) + P0.x * (P0.y - P3.y) + P0.y * (P3.x - P0.x) /// = 0 - /// or a * t^3 + b * t^2 + c * t + d = 0 + /// ``` + /// or `a * t^3 + b * t^2 + c * t + d = 0` /// /// let x = t - b / (3 * a), then we have: + /// ```text /// x^3 + p * x + q = 0, where: /// p = (3.0 * a * c - b^2) / (3.0 * a^2) /// q = (2.0 * b^3 - 9.0 * a * b * c + 27.0 * a^2 * d) / (27.0 * a^3) + /// ``` /// /// when p > 0, there will be one real root, two complex roots /// when p = 0, there will be two real roots, when p=q=0, there will be three real roots but all 0. diff --git a/crates/epaint/src/texture_handle.rs b/crates/epaint/src/texture_handle.rs index f4142d91510..1f640a171de 100644 --- a/crates/epaint/src/texture_handle.rs +++ b/crates/epaint/src/texture_handle.rs @@ -66,6 +66,7 @@ impl TextureHandle { } /// Assign a new image to an existing texture. + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set(&mut self, image: impl Into, options: TextureOptions) { self.tex_mngr .write() @@ -73,6 +74,7 @@ impl TextureHandle { } /// Assign a new image to a subregion of the whole texture. + #[allow(clippy::needless_pass_by_ref_mut)] // Intentionally hide interiority of mutability pub fn set_partial( &mut self, pos: [usize; 2], diff --git a/examples/confirm_exit/Cargo.toml b/examples/confirm_exit/Cargo.toml index d4a21060b76..62e8bc29c14 100644 --- a/examples/confirm_exit/Cargo.toml +++ b/examples/confirm_exit/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_3d_glow/Cargo.toml b/examples/custom_3d_glow/Cargo.toml index d1dcc056949..1ddec02f5f8 100644 --- a/examples/custom_3d_glow/Cargo.toml +++ b/examples/custom_3d_glow/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_font/Cargo.toml b/examples/custom_font/Cargo.toml index ee769cc62d5..5214bdc1f5f 100644 --- a/examples/custom_font/Cargo.toml +++ b/examples/custom_font/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_font_style/Cargo.toml b/examples/custom_font_style/Cargo.toml index f25676e87a1..241b893401d 100644 --- a/examples/custom_font_style/Cargo.toml +++ b/examples/custom_font_style/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["tami5 "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_keypad/Cargo.toml b/examples/custom_keypad/Cargo.toml index dc3c62dddb8..73c6b0e7aa8 100644 --- a/examples/custom_keypad/Cargo.toml +++ b/examples/custom_keypad/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Varphone Wong "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_style/Cargo.toml b/examples/custom_style/Cargo.toml index 6299e1aee35..f87ce0bf82a 100644 --- a/examples/custom_style/Cargo.toml +++ b/examples/custom_style/Cargo.toml @@ -3,7 +3,7 @@ name = "custom_style" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/custom_window_frame/Cargo.toml b/examples/custom_window_frame/Cargo.toml index 848189084ad..4a53ee48745 100644 --- a/examples/custom_window_frame/Cargo.toml +++ b/examples/custom_window_frame/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index dc58e0ba2e7..a2281414317 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world/Cargo.toml b/examples/hello_world/Cargo.toml index 6e7dd8d00f1..8816b425598 100644 --- a/examples/hello_world/Cargo.toml +++ b/examples/hello_world/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world_par/Cargo.toml b/examples/hello_world_par/Cargo.toml index d486e35791d..b3e00b20897 100644 --- a/examples/hello_world_par/Cargo.toml +++ b/examples/hello_world_par/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Maxim Osipenko "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/hello_world_simple/Cargo.toml b/examples/hello_world_simple/Cargo.toml index 0d77c65e316..7197c60337b 100644 --- a/examples/hello_world_simple/Cargo.toml +++ b/examples/hello_world_simple/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/images/Cargo.toml b/examples/images/Cargo.toml index 4759e2d128e..f1b4f97ddf2 100644 --- a/examples/images/Cargo.toml +++ b/examples/images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jan ProchΓ‘zka "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/keyboard_events/Cargo.toml b/examples/keyboard_events/Cargo.toml index e587764bacf..1af09849319 100644 --- a/examples/keyboard_events/Cargo.toml +++ b/examples/keyboard_events/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Jose Palazon "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/multiple_viewports/Cargo.toml b/examples/multiple_viewports/Cargo.toml index 9910aed5a2d..1644d6a72a5 100644 --- a/examples/multiple_viewports/Cargo.toml +++ b/examples/multiple_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index d2cbce19052..3389de5c928 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml index 2e74482a64e..5963ea3ad71 100644 --- a/examples/screenshot/Cargo.toml +++ b/examples/screenshot/Cargo.toml @@ -7,7 +7,7 @@ authors = [ ] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/serial_windows/Cargo.toml b/examples/serial_windows/Cargo.toml index c377524c69c..1f6d5ea431e 100644 --- a/examples/serial_windows/Cargo.toml +++ b/examples/serial_windows/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml index 3fbc75e260e..25aa473486c 100644 --- a/examples/user_attention/Cargo.toml +++ b/examples/user_attention/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["TicClick "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/rust-toolchain b/rust-toolchain index 9fdafb7a67a..38e5e90f3ac 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -5,6 +5,6 @@ # to the user in the error, instead of "error: invalid channel name '[toolchain]'". [toolchain] -channel = "1.79.0" +channel = "1.80.0" components = ["rustfmt", "clippy"] targets = ["wasm32-unknown-unknown"] diff --git a/scripts/check.sh b/scripts/check.sh index 3129f2ac114..7db1a6aa3e2 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -9,7 +9,7 @@ set -x # Checks all tests, lints etc. # Basically does what the CI does. -cargo +1.79.0 install --quiet typos-cli +cargo +1.80.0 install --quiet typos-cli export RUSTFLAGS="-D warnings" export RUSTDOCFLAGS="-D warnings" # https://github.com/emilk/egui/pull/1454 diff --git a/scripts/clippy_wasm/clippy.toml b/scripts/clippy_wasm/clippy.toml index 0f7fc92dcc7..f06033c71b9 100644 --- a/scripts/clippy_wasm/clippy.toml +++ b/scripts/clippy_wasm/clippy.toml @@ -6,7 +6,7 @@ # ----------------------------------------------------------------------------- # Section identical to the root clippy.toml: -msrv = "1.79" +msrv = "1.80" allow-unwrap-in-tests = true diff --git a/tests/test_egui_extras_compilation/Cargo.toml b/tests/test_egui_extras_compilation/Cargo.toml index 1a310566f2a..f1d67f4bb8b 100644 --- a/tests/test_egui_extras_compilation/Cargo.toml +++ b/tests/test_egui_extras_compilation/Cargo.toml @@ -3,7 +3,7 @@ name = "test_egui_extras_compilation" version = "0.1.0" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_inline_glow_paint/Cargo.toml b/tests/test_inline_glow_paint/Cargo.toml index 5ded3cc356b..bcda5b3ddea 100644 --- a/tests/test_inline_glow_paint/Cargo.toml +++ b/tests/test_inline_glow_paint/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_size_pass/Cargo.toml b/tests/test_size_pass/Cargo.toml index d6ee661e6b6..e3819a107e9 100644 --- a/tests/test_size_pass/Cargo.toml +++ b/tests/test_size_pass/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Emil Ernerfeldt "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_ui_stack/Cargo.toml b/tests/test_ui_stack/Cargo.toml index df2e2bf15c2..12ba9961be7 100644 --- a/tests/test_ui_stack/Cargo.toml +++ b/tests/test_ui_stack/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["Antoine Beyeler "] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] diff --git a/tests/test_viewports/Cargo.toml b/tests/test_viewports/Cargo.toml index cb962411558..cb877a7107a 100644 --- a/tests/test_viewports/Cargo.toml +++ b/tests/test_viewports/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" authors = ["konkitoman"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.79" +rust-version = "1.80" publish = false [lints] From f28080c67597615e829a4e0b7e6c4b80795892f2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 10 Dec 2024 17:16:38 +0100 Subject: [PATCH 04/34] Update some crates to fix CI (#5456) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > cargo update -p url ``` Locking 28 packages to latest compatible versions Adding displaydoc v0.2.5 Adding icu_collections v1.5.0 Adding icu_locid v1.5.0 Adding icu_locid_transform v1.5.0 Adding icu_locid_transform_data v1.5.0 Adding icu_normalizer v1.5.0 Adding icu_normalizer_data v1.5.0 Adding icu_properties v1.5.1 Adding icu_properties_data v1.5.0 Adding icu_provider v1.5.0 Adding icu_provider_macros v1.5.0 Updating idna v0.5.0 -> v1.0.3 Adding idna_adapter v1.2.0 Adding litemap v0.7.4 Adding stable_deref_trait v1.2.0 Adding synstructure v0.13.1 Adding tinystr v0.7.6 (latest: v0.8.0) Removing tinyvec v1.8.0 Removing tinyvec_macros v0.1.1 Removing unicode-bidi v0.3.17 Removing unicode-normalization v0.1.24 Updating url v2.5.2 -> v2.5.4 Adding utf16_iter v1.0.5 Adding utf8_iter v1.0.4 Adding write16 v1.0.0 Adding writeable v0.5.5 (latest: v0.6.0) Adding yoke v0.7.5 Adding yoke-derive v0.7.5 Adding zerofrom v0.1.5 Adding zerofrom-derive v0.1.5 Adding zerovec v0.10.4 (latest: v0.11.0) Adding zerovec-derive v0.10.3 (latest: v0.11.0) ``` holy hell that's a lot of new crates 😭 * Waiting for https://github.com/emilk/egui/pull/5457 --- Cargo.lock | 727 ++++++++++++++++++++------------ crates/egui_demo_app/Cargo.toml | 9 +- deny.toml | 1 + examples/file_dialog/Cargo.toml | 2 +- 4 files changed, 468 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2e858d4b41..3837d7ec4c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,7 +39,7 @@ dependencies = [ "atspi-common", "serde", "thiserror", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -81,7 +81,7 @@ dependencies = [ "futures-lite", "futures-util", "serde", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -258,6 +258,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle 0.6.2", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus 5.1.1", +] + [[package]] name = "async-broadcast" version = "0.7.1" @@ -336,6 +358,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.3.0" @@ -401,18 +434,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -439,11 +460,11 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zbus", + "zbus 4.4.0", "zbus-lockstep", "zbus-lockstep-macros", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -455,7 +476,7 @@ dependencies = [ "atspi-common", "atspi-proxies", "futures-lite", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -466,8 +487,8 @@ checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" dependencies = [ "atspi-common", "serde", - "zbus", - "zvariant", + "zbus 4.4.0", + "zvariant 4.2.0", ] [[package]] @@ -638,16 +659,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "calloop" version = "0.13.0" @@ -697,16 +708,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -1139,6 +1140,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dlib" version = "0.5.2" @@ -1205,7 +1217,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "pollster", + "pollster 0.4.0", "puffin", "raw-window-handle 0.6.2", "ron", @@ -1368,7 +1380,7 @@ dependencies = [ "egui_kittest", "image", "kittest", - "pollster", + "pollster 0.4.0", "wgpu", ] @@ -1633,6 +1645,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.31" @@ -1698,36 +1719,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1784,19 +1775,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - [[package]] name = "gl_generator" version = "0.14.0" @@ -1808,16 +1786,6 @@ dependencies = [ "xml-rs", ] -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - [[package]] name = "glow" version = "0.14.2" @@ -1908,17 +1876,6 @@ dependencies = [ "gl_generator", ] -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - [[package]] name = "gpu-alloc" version = "0.6.0" @@ -1958,24 +1915,6 @@ dependencies = [ "bitflags 2.6.0", ] -[[package]] -name = "gtk-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - [[package]] name = "half" version = "2.4.1" @@ -1996,12 +1935,6 @@ dependencies = [ "allocator-api2", ] -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hello_world" version = "0.1.0" @@ -2084,14 +2017,143 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -2300,6 +2362,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "litrs" version = "0.4.1" @@ -2540,17 +2608,6 @@ dependencies = [ "malloc_buf", ] -[[package]] -name = "objc-foundation" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" -dependencies = [ - "block", - "objc", - "objc_id", -] - [[package]] name = "objc-sys" version = "0.3.5" @@ -2754,15 +2811,6 @@ dependencies = [ "objc2-foundation", ] -[[package]] -name = "objc_id" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" -dependencies = [ - "objc", -] - [[package]] name = "object" version = "0.36.5" @@ -2818,18 +2866,6 @@ dependencies = [ "ttf-parser", ] -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - [[package]] name = "parking" version = "2.2.1" @@ -2977,6 +3013,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + [[package]] name = "pollster" version = "0.4.0" @@ -3256,21 +3298,20 @@ dependencies = [ [[package]] name = "rfd" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0d8ab342bcc5436e04d3a4c1e09e17d74958bfaddf8d5fad6f85607df0f994f" +checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" dependencies = [ - "block", - "dispatch", - "glib-sys", - "gobject-sys", - "gtk-sys", + "ashpd", + "block2", "js-sys", "log", - "objc", - "objc-foundation", - "objc_id", - "raw-window-handle 0.5.2", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "pollster 0.3.0", + "raw-window-handle 0.6.2", + "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -3468,15 +3509,6 @@ dependencies = [ "syn", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "serial_windows" version = "0.1.0" @@ -3617,6 +3649,12 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3659,6 +3697,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syntect" version = "5.2.0" @@ -3681,25 +3730,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" version = "3.13.0" @@ -3841,40 +3871,23 @@ dependencies = [ ] [[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] [[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "toml" -version = "0.8.19" +name = "tinytemplate" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_json", ] [[package]] @@ -3882,9 +3895,6 @@ name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -3893,8 +3903,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", - "serde", - "serde_spanned", "toml_datetime", "winnow", ] @@ -3968,27 +3976,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -4037,15 +4030,22 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "user_attention" version = "0.1.0" @@ -4099,10 +4099,16 @@ dependencies = [ ] [[package]] -name = "version-compare" -version = "0.2.0" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "version_check" @@ -4478,7 +4484,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4835,6 +4841,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x11-dl" version = "2.21.0" @@ -4927,6 +4945,30 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zbus" version = "4.4.0" @@ -4960,9 +5002,45 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1162094dc63b1629fcc44150bcceeaa80798cd28bcbe7fa987b65a034c258608" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-util", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", + "xdg-home", + "zbus_macros 5.1.1", + "zbus_names 4.1.0", + "zvariant 5.1.0", ] [[package]] @@ -4972,7 +5050,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" dependencies = [ "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -4986,7 +5064,7 @@ dependencies = [ "syn", "zbus-lockstep", "zbus_xml", - "zvariant", + "zvariant 4.2.0", ] [[package]] @@ -4999,7 +5077,22 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd2dcdce3e2727f7d74b7e33b5a89539b3cc31049562137faf7ae4eb86cd16d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names 4.1.0", + "zvariant 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -5010,7 +5103,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant 5.1.0", ] [[package]] @@ -5022,8 +5127,8 @@ dependencies = [ "quick-xml 0.30.0", "serde", "static_assertions", - "zbus_names", - "zvariant", + "zbus_names 3.0.0", + "zvariant 4.2.0", ] [[package]] @@ -5047,12 +5152,55 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zune-core" version = "0.4.12" @@ -5078,7 +5226,23 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "winnow", + "zvariant_derive 5.1.0", + "zvariant_utils 3.0.2", ] [[package]] @@ -5091,7 +5255,20 @@ dependencies = [ "proc-macro2", "quote", "syn", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils 3.0.2", ] [[package]] @@ -5104,3 +5281,17 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zvariant_utils" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn", + "winnow", +] diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index 23ef8599dba..a66b6a7587f 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -27,7 +27,12 @@ web_app = ["http", "persistence"] http = ["ehttp", "image", "poll-promise", "egui_extras/image"] image_viewer = ["image", "egui_extras/all_loaders", "rfd"] -persistence = ["eframe/persistence", "egui/persistence", "serde", "egui_extras/serde"] +persistence = [ + "eframe/persistence", + "egui_extras/serde", + "egui/persistence", + "serde", +] puffin = ["eframe/puffin", "dep:puffin", "dep:puffin_http"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] syntect = ["egui_demo_lib/syntect"] @@ -74,7 +79,7 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = { version = "0.13", optional = true } +rfd = { version = "0.15", optional = true } # web: [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/deny.toml b/deny.toml index 87ca0fb2fb5..ede37dea405 100644 --- a/deny.toml +++ b/deny.toml @@ -85,6 +85,7 @@ allow = [ "MPL-2.0", # https://www.mozilla.org/en-US/MPL/2.0/FAQ/ - see Q11. Used by webpki-roots on Linux. "OFL-1.1", # https://spdx.org/licenses/OFL-1.1.html "OpenSSL", # https://www.openssl.org/source/license.html - used on Linux + "Unicode-3.0", # https://www.unicode.org/license.txt "Unicode-DFS-2016", # https://spdx.org/licenses/Unicode-DFS-2016.html "Zlib", # https://tldrlegal.com/license/zlib-libpng-license-(zlib) ] diff --git a/examples/file_dialog/Cargo.toml b/examples/file_dialog/Cargo.toml index a2281414317..1a9f86c40c0 100644 --- a/examples/file_dialog/Cargo.toml +++ b/examples/file_dialog/Cargo.toml @@ -20,4 +20,4 @@ env_logger = { version = "0.10", default-features = false, features = [ "auto-color", "humantime", ] } -rfd = "0.13" +rfd = "0.15" From 4362a242b02beea9e7978346c7428e1a8d075d47 Mon Sep 17 00:00:00 2001 From: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:44:37 +0100 Subject: [PATCH 05/34] web_demo: make hash anchor case insensitive (#5446) * [X] I have followed the instructions in the PR template --- crates/egui_demo_app/src/wrap_app.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/egui_demo_app/src/wrap_app.rs b/crates/egui_demo_app/src/wrap_app.rs index bd313fb0c08..cbed988fa68 100644 --- a/crates/egui_demo_app/src/wrap_app.rs +++ b/crates/egui_demo_app/src/wrap_app.rs @@ -111,11 +111,19 @@ impl Anchor { Self::Rendering, ] } + + #[cfg(target_arch = "wasm32")] + fn from_str_case_insensitive(anchor: &str) -> Option { + let anchor = anchor.to_lowercase(); + Self::all().into_iter().find(|x| x.to_string() == anchor) + } } impl std::fmt::Display for Anchor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{self:?}") + let mut name = format!("{self:?}"); + name.make_ascii_lowercase(); + f.write_str(&name) } } @@ -263,11 +271,15 @@ impl eframe::App for WrapApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { #[cfg(target_arch = "wasm32")] - if let Some(anchor) = frame.info().web_info.location.hash.strip_prefix('#') { - let anchor = Anchor::all().into_iter().find(|x| x.to_string() == anchor); - if let Some(v) = anchor { - self.state.selected_anchor = v; - } + if let Some(anchor) = frame + .info() + .web_info + .location + .hash + .strip_prefix('#') + .and_then(Anchor::from_str_case_insensitive) + { + self.state.selected_anchor = anchor; } #[cfg(not(target_arch = "wasm32"))] From 36a70e12c3a8a70308a4faa15799d557a5c0a064 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Wed, 11 Dec 2024 16:55:57 +0100 Subject: [PATCH 06/34] Fix pr preview vulnerability (#5461) * Closes #5458 * [x] I have followed the instructions in the PR template --- .github/workflows/preview_build.yml | 6 ++++-- .github/workflows/preview_cleanup.yml | 9 +++++++-- .github/workflows/preview_deploy.yml | 6 +----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/preview_build.yml b/.github/workflows/preview_build.yml index 70cd5ce2aae..c44c10f0c83 100644 --- a/.github/workflows/preview_build.yml +++ b/.github/workflows/preview_build.yml @@ -42,9 +42,11 @@ jobs: - name: Generate meta.json env: PR_NUMBER: ${{ github.event.number }} - PR_BRANCH: ${{ github.head_ref }} + URL_SLUG: ${{ github.event.number }}-${{ github.head_ref }} run: | - echo "{\"pr_number\": \"$PR_NUMBER\", \"pr_branch\": \"$PR_BRANCH\"}" > meta.json + # Sanitize the URL_SLUG to only contain alphanumeric characters and dashes + URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-') + echo "{\"pr_number\": \"$PR_NUMBER\", \"url_slug\": \"$URL_SLUG\"}" > meta.json - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/preview_cleanup.yml b/.github/workflows/preview_cleanup.yml index 3aba668a2b3..6e2b94b83aa 100644 --- a/.github/workflows/preview_cleanup.yml +++ b/.github/workflows/preview_cleanup.yml @@ -15,9 +15,14 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - run: mkdir -p empty_dir - - name: Url slug variable + - name: Generate URL_SLUG + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + URL_SLUG: ${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }} run: | - echo "URL_SLUG=${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV + # Sanitize the URL_SLUG to only contain alphanumeric characters and dashes + URL_SLUG=$(echo $URL_SLUG | tr -cd '[:alnum:]-') + echo "URL_SLUG=$URL_SLUG" >> $GITHUB_ENV - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 with: diff --git a/.github/workflows/preview_deploy.yml b/.github/workflows/preview_deploy.yml index 9fdcfaf755f..f98f96da162 100644 --- a/.github/workflows/preview_deploy.yml +++ b/.github/workflows/preview_deploy.yml @@ -40,11 +40,7 @@ jobs: - name: Parse meta.json run: | echo "PR_NUMBER=$(jq -r .pr_number meta.json)" >> $GITHUB_ENV - echo "PR_BRANCH=$(jq -r .pr_branch meta.json)" >> $GITHUB_ENV - - - name: Url slug variable - run: | - echo "URL_SLUG=${{ env.PR_NUMBER }}-${{ env.PR_BRANCH }}" >> $GITHUB_ENV + echo "URL_SLUG=$(jq -r .url_slug meta.json)" >> $GITHUB_ENV - name: Deploy uses: JamesIves/github-pages-deploy-action@v4 From d3ea922cc6027c1501a07188a721fac0d6ef628d Mon Sep 17 00:00:00 2001 From: "Michael \"Scott\" McBee" Date: Thu, 12 Dec 2024 05:42:32 -0500 Subject: [PATCH 07/34] Fix: don't interact with `Area` outside its `constrain_rect` (#5459) * [x] I have followed the instructions in the PR template This PR makes an area's interact rect intersect its constrain rect. This fixes an issue where masked areas would still intercept input. Before: ![before](https://github.com/user-attachments/assets/6b790a04-8a15-44fe-a7ae-4adda189ecba) After: ![after](https://github.com/user-attachments/assets/98010d89-e680-44cb-8717-faf31b0912d3) --- crates/egui/src/containers/area.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index ab9a0b88d09..a2bfbe1905f 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -467,7 +467,7 @@ impl Area { id: interact_id, layer_id, rect: state.rect(), - interact_rect: state.rect(), + interact_rect: state.rect().intersect(constrain_rect), sense, enabled, }, From 99c1034cfcaa90415033af289c312fc6bcfabafb Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 12 Dec 2024 16:57:52 +0100 Subject: [PATCH 08/34] Improve changelog generation script --- RELEASES.md | 1 + scripts/generate_changelog.py | 40 ++++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index b3777a3aedc..677f31fddf9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -84,6 +84,7 @@ I usually do this all on the `master` branch, but doing it in a release branch i (cd crates/egui_demo_lib && cargo publish --quiet) && echo "βœ… egui_demo_lib" (cd crates/egui_glow && cargo publish --quiet) && echo "βœ… egui_glow" (cd crates/eframe && cargo publish --quiet) && echo "βœ… eframe" +(cd crates/egui_kittest && cargo publish --quiet) && echo "βœ… egui_kittest" ``` ## Announcements diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 551d9c9d29e..1621fd29793 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -214,18 +214,42 @@ def add_to_changelog_file(crate: str, content: str, version: str) -> None: file.write(content) +def calc_commit_range(new_version: str) -> str: + parts = new_version.split(".") + assert len(parts) == 3, "Expected version to be on the format X.Y.Z" + major = int(parts[0]) + minor = int(parts[1]) + patch = int(parts[2]) + + if 0 < patch: + # A patch release. + # Include changes since last patch release. + # This assumes we've cherry-picked stuff for this release. + diff_since_version = f"0.{minor}.{patch - 1}" + elif 0 < minor: + # A minor release + # The diff should span everything since the last minor release. + # The script later excludes duplicated automatically, so we don't include stuff that + # was part of intervening patch releases. + diff_since_version = f"{major}.{minor - 1}.0" + else: + # A major release + # The diff should span everything since the last major release. + # The script later excludes duplicated automatically, so we don't include stuff that + # was part of intervening minor/patch releases. + diff_since_version = f"{major - 1}.{minor}.0" + + return f"{diff_since_version}..HEAD" + + def main() -> None: parser = argparse.ArgumentParser(description="Generate a changelog.") - parser.add_argument("--commit-range", help="e.g. 0.24.0..HEAD", required=True) + parser.add_argument("--version", help="What release is this?", required=True) parser.add_argument( "--write", help="Write into the different changelogs?", action="store_true" ) - parser.add_argument("--version", help="What release is this?") args = parser.parse_args() - - if args.write and not args.version: - print("ERROR: --version is required when --write is used") - sys.exit(1) + commit_range = calc_commit_range(args.version) crate_names = [ "ecolor", @@ -251,7 +275,7 @@ def main() -> None: all_changelogs += file.read() repo = Repo(".") - commits = list(repo.iter_commits(args.commit_range)) + commits = list(repo.iter_commits(commit_range)) commits.reverse() # Most recent last commit_infos = list(map(get_commit_info, commits)) @@ -307,7 +331,7 @@ def main() -> None: unsorted_prs.append(pr_summary(pr_info)) print() - print(f"Full diff at https://github.com/emilk/egui/compare/{args.commit_range}") + print(f"Full diff at https://github.com/emilk/egui/compare/{commit_range}") print() for crate in crate_names: if crate in crate_sections: From de8ac88c0eecae42c473cc00296ecfbbea1ec521 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 12 Dec 2024 18:29:13 +0100 Subject: [PATCH 09/34] Add `Context::layer_transform_to_global` & `layer_transform_from_global` (#5465) This makes it easy to get the current transform of a layer, and uses consistent naming everywhere. `Memory::layer_transforms` is now called `Memory::to_global`, because the old name was ambiguous (transform into what direction?) --- crates/egui/src/containers/popup.rs | 15 +++---- crates/egui/src/context.rs | 41 +++++++++++++------- crates/egui/src/hit_test.rs | 6 +-- crates/egui/src/layers.rs | 14 +++---- crates/egui/src/memory/mod.rs | 25 ++++++++---- crates/egui/src/menu.rs | 7 +--- crates/egui/src/response.rs | 14 ++----- crates/egui/src/widgets/text_edit/builder.rs | 9 +++-- 8 files changed, 75 insertions(+), 56 deletions(-) diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index a90e615aca7..81bf84a2f4f 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -93,8 +93,8 @@ pub fn show_tooltip_at_pointer( pointer_rect.min.x = pointer_pos.x; // Transform global coords to layer coords: - if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { - pointer_rect = transform.inverse() * pointer_rect; + if let Some(from_global) = ctx.layer_transform_from_global(parent_layer) { + pointer_rect = from_global * pointer_rect; } show_tooltip_at_dyn( @@ -162,8 +162,8 @@ fn show_tooltip_at_dyn<'c, R>( ) -> R { // Transform layer coords to global coords: let mut widget_rect = *widget_rect; - if let Some(transform) = ctx.memory(|m| m.layer_transforms.get(&parent_layer).copied()) { - widget_rect = transform * widget_rect; + if let Some(to_global) = ctx.layer_transform_to_global(parent_layer) { + widget_rect = to_global * widget_rect; } remember_that_tooltip_was_shown(ctx); @@ -404,11 +404,12 @@ pub fn popup_above_or_below_widget( AboveOrBelow::Above => (widget_response.rect.left_top(), Align2::LEFT_BOTTOM), AboveOrBelow::Below => (widget_response.rect.left_bottom(), Align2::LEFT_TOP), }; - if let Some(transform) = parent_ui + + if let Some(to_global) = parent_ui .ctx() - .memory(|m| m.layer_transforms.get(&parent_ui.layer_id()).copied()) + .layer_transform_to_global(parent_ui.layer_id()) { - pos = transform * pos; + pos = to_global * pos; } let frame = Frame::popup(parent_ui.style()); diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index aa449849e32..9286f38025f 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -518,7 +518,7 @@ impl ContextImpl { crate::hit_test::hit_test( &viewport.prev_pass.widgets, &layers, - &self.memory.layer_transforms, + &self.memory.to_global, pos, interact_radius, ) @@ -1329,11 +1329,11 @@ impl Context { res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped; if is_interacted_with { res.interact_pointer_pos = input.pointer.interact_pos(); - if let (Some(transform), Some(pos)) = ( - memory.layer_transforms.get(&res.layer_id), + if let (Some(to_global), Some(pos)) = ( + memory.to_global.get(&res.layer_id), &mut res.interact_pointer_pos, ) { - *pos = transform.inverse() * *pos; + *pos = to_global.inverse() * *pos; } } @@ -2381,7 +2381,7 @@ impl ContextImpl { let shapes = viewport .graphics - .drain(self.memory.areas().order(), &self.memory.layer_transforms); + .drain(self.memory.areas().order(), &self.memory.to_global); let mut repaint_needed = false; @@ -2697,6 +2697,7 @@ impl Context { /// Transform the graphics of the given layer. /// /// This will also affect input. + /// The direction of the given transform is "into the global coordinate system". /// /// This is a sticky setting, remembered from one frame to the next. /// @@ -2706,13 +2707,28 @@ impl Context { pub fn set_transform_layer(&self, layer_id: LayerId, transform: TSTransform) { self.memory_mut(|m| { if transform == TSTransform::IDENTITY { - m.layer_transforms.remove(&layer_id) + m.to_global.remove(&layer_id) } else { - m.layer_transforms.insert(layer_id, transform) + m.to_global.insert(layer_id, transform) } }); } + /// Return how to transform the graphics of the given layer into the global coordinate system. + /// + /// Set this with [`Self::layer_transform_to_global`]. + pub fn layer_transform_to_global(&self, layer_id: LayerId) -> Option { + self.memory(|m| m.to_global.get(&layer_id).copied()) + } + + /// Return how to transform the graphics of the global coordinate system into the local coordinate system of the given layer. + /// + /// This returns the inverse of [`Self::layer_transform_to_global`]. + pub fn layer_transform_from_global(&self, layer_id: LayerId) -> Option { + self.layer_transform_to_global(layer_id) + .map(|t| t.inverse()) + } + /// Move all the graphics at the given layer. /// /// Is used to implement drag-and-drop preview. @@ -2777,12 +2793,11 @@ impl Context { /// /// See also [`Response::contains_pointer`]. pub fn rect_contains_pointer(&self, layer_id: LayerId, rect: Rect) -> bool { - let rect = - if let Some(transform) = self.memory(|m| m.layer_transforms.get(&layer_id).copied()) { - transform * rect - } else { - rect - }; + let rect = if let Some(to_global) = self.layer_transform_to_global(layer_id) { + to_global * rect + } else { + rect + }; if !rect.is_positive() { return false; } diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 142645f24e6..fe0ce75f9c9 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -35,7 +35,7 @@ pub struct WidgetHits { pub fn hit_test( widgets: &WidgetRects, layer_order: &[LayerId], - layer_transforms: &HashMap, + layer_to_global: &HashMap, pos: Pos2, search_radius: f32, ) -> WidgetHits { @@ -44,9 +44,9 @@ pub fn hit_test( let search_radius_sq = search_radius * search_radius; // Transform the position into the local coordinate space of each layer: - let pos_in_layers: HashMap = layer_transforms + let pos_in_layers: HashMap = layer_to_global .iter() - .map(|(layer_id, t)| (*layer_id, t.inverse() * pos)) + .map(|(layer_id, to_global)| (*layer_id, to_global.inverse() * pos)) .collect(); let mut closest_dist_sq = f32::INFINITY; diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 7966e12ad2a..595f67034f5 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -213,7 +213,7 @@ impl GraphicLayers { pub fn drain( &mut self, area_order: &[LayerId], - transforms: &ahash::HashMap, + to_global: &ahash::HashMap, ) -> Vec { crate::profile_function!(); @@ -231,10 +231,10 @@ impl GraphicLayers { for layer_id in area_order { if layer_id.order == order { if let Some(list) = order_map.get_mut(&layer_id.id) { - if let Some(transform) = transforms.get(layer_id) { + if let Some(to_global) = to_global.get(layer_id) { for clipped_shape in &mut list.0 { - clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; - clipped_shape.shape.transform(*transform); + clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect; + clipped_shape.shape.transform(*to_global); } } all_shapes.append(&mut list.0); @@ -246,10 +246,10 @@ impl GraphicLayers { for (id, list) in order_map { let layer_id = LayerId::new(order, *id); - if let Some(transform) = transforms.get(&layer_id) { + if let Some(to_global) = to_global.get(&layer_id) { for clipped_shape in &mut list.0 { - clipped_shape.clip_rect = *transform * clipped_shape.clip_rect; - clipped_shape.shape.transform(*transform); + clipped_shape.clip_rect = *to_global * clipped_shape.clip_rect; + clipped_shape.shape.transform(*to_global); } } diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 5e94e6c0915..0a44df8cc57 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -95,8 +95,13 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, - /// Transforms per layer - pub layer_transforms: HashMap, + /// Transforms per layer. + /// + /// Instead of using this directly, use: + /// * [`crate::Context::set_transform_layer`] + /// * [`crate::Context::layer_transform_to_global`] + /// * [`crate::Context::layer_transform_from_global`] + pub to_global: HashMap, // ------------------------------------------------- // Per-viewport: @@ -120,7 +125,7 @@ impl Default for Memory { focus: Default::default(), viewport_id: Default::default(), areas: Default::default(), - layer_transforms: Default::default(), + to_global: Default::default(), popup: Default::default(), everything_is_visible: Default::default(), add_fonts: Default::default(), @@ -819,7 +824,7 @@ impl Memory { /// Top-most layer at the given position. pub fn layer_id_at(&self, pos: Pos2) -> Option { self.areas() - .layer_id_at(pos, &self.layer_transforms) + .layer_id_at(pos, &self.to_global) .and_then(|layer_id| { if self.is_above_modal_layer(layer_id) { Some(layer_id) @@ -829,6 +834,12 @@ impl Memory { }) } + /// The currently set transform of a layer. + #[deprecated = "Use `Context::layer_transform_to_global` instead"] + pub fn layer_transforms(&self, layer_id: LayerId) -> Option { + self.to_global.get(&layer_id).copied() + } + /// An iterator over all layers. Back-to-front, top is last. pub fn layer_ids(&self) -> impl ExactSizeIterator + '_ { self.areas().order().iter().copied() @@ -1194,15 +1205,15 @@ impl Areas { pub fn layer_id_at( &self, pos: Pos2, - layer_transforms: &HashMap, + layer_to_global: &HashMap, ) -> Option { for layer in self.order.iter().rev() { if self.is_visible(layer) { if let Some(state) = self.areas.get(&layer.id) { let mut rect = state.rect(); if state.interactable { - if let Some(transform) = layer_transforms.get(layer) { - rect = *transform * rect; + if let Some(to_global) = layer_to_global.get(layer) { + rect = *to_global * rect; } if rect.contains(pos) { diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index 8faf8e77ceb..166e18da04b 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -406,11 +406,8 @@ impl MenuRoot { } } - if let Some(transform) = button - .ctx - .memory(|m| m.layer_transforms.get(&button.layer_id).copied()) - { - pos = transform * pos; + if let Some(to_global) = button.ctx.layer_transform_to_global(button.layer_id) { + pos = to_global * pos; } return MenuResponse::Create(pos, id); diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index f49383c70d1..18ddf793cc6 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -392,11 +392,8 @@ impl Response { pub fn drag_delta(&self) -> Vec2 { if self.dragged() { let mut delta = self.ctx.input(|i| i.pointer.delta()); - if let Some(scaling) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).map(|t| t.scaling)) - { - delta /= scaling; + if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) { + delta *= from_global.scaling; } delta } else { @@ -478,11 +475,8 @@ impl Response { pub fn hover_pos(&self) -> Option { if self.hovered() { let mut pos = self.ctx.input(|i| i.pointer.hover_pos())?; - if let Some(transform) = self - .ctx - .memory(|m| m.layer_transforms.get(&self.layer_id).copied()) - { - pos = transform.inverse() * pos; + if let Some(from_global) = self.ctx.layer_transform_from_global(self.layer_id) { + pos = from_global * pos; } Some(pos) } else { diff --git a/crates/egui/src/widgets/text_edit/builder.rs b/crates/egui/src/widgets/text_edit/builder.rs index 012d8c8f0d5..587f498b0a2 100644 --- a/crates/egui/src/widgets/text_edit/builder.rs +++ b/crates/egui/src/widgets/text_edit/builder.rs @@ -766,14 +766,15 @@ impl<'t> TextEdit<'t> { } // Set IME output (in screen coords) when text is editable and visible - let transform = ui - .memory(|m| m.layer_transforms.get(&ui.layer_id()).copied()) + let to_global = ui + .ctx() + .layer_transform_to_global(ui.layer_id()) .unwrap_or_default(); ui.ctx().output_mut(|o| { o.ime = Some(crate::output::IMEOutput { - rect: transform * rect, - cursor_rect: transform * primary_cursor_rect, + rect: to_global * rect, + cursor_rect: to_global * primary_cursor_rect, }); }); } From 6c1d695fc66611369f78212e38c2895bc3a7c442 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Thu, 12 Dec 2024 19:17:42 +0100 Subject: [PATCH 10/34] Add screenshot support for eframe web (#5438) This implements web support for taking screenshots in an eframe app (and adds a nice demo). It also updates the native screenshot implementation to work with the wgpu gl backend. The wgpu implementation is quite different than the native one because we can't block to wait for the screenshot result, so instead I use a channel to pass the result to a future frame asynchronously. * Closes * [x] I have followed the instructions in the PR template https://github.com/user-attachments/assets/67cad40b-0384-431d-96a3-075cc3cb98fb --- crates/eframe/src/native/wgpu_integration.rs | 32 +- crates/eframe/src/web/app_runner.rs | 33 +- crates/eframe/src/web/web_painter.rs | 6 + crates/eframe/src/web/web_painter_glow.rs | 37 ++- crates/eframe/src/web/web_painter_wgpu.rs | 97 ++++-- crates/egui-wgpu/src/capture.rs | 257 ++++++++++++++++ crates/egui-wgpu/src/lib.rs | 3 + crates/egui-wgpu/src/texture_copy.wgsl | 43 +++ crates/egui-wgpu/src/winit.rs | 288 +++++------------- .../src/demo/demo_app_windows.rs | 1 + crates/egui_demo_lib/src/demo/mod.rs | 1 + crates/egui_demo_lib/src/demo/screenshot.rs | 84 +++++ .../tests/snapshots/demos/Screenshot.png | 3 + 13 files changed, 613 insertions(+), 272 deletions(-) create mode 100644 crates/egui-wgpu/src/capture.rs create mode 100644 crates/egui-wgpu/src/texture_copy.wgsl create mode 100644 crates/egui_demo_lib/src/demo/screenshot.rs create mode 100644 crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index d13bed0bf41..acbd8c2f830 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -185,6 +185,7 @@ impl<'app> WgpuWinitApp<'app> { #[allow(unsafe_code, unused_mut, unused_unsafe)] let mut painter = egui_wgpu::winit::Painter::new( + egui_ctx.clone(), self.native_options.wgpu_options.clone(), self.native_options.multisampling.max(1) as _, egui_wgpu::depth_format_from_bits( @@ -593,6 +594,8 @@ impl<'app> WgpuWinitRunning<'app> { .map(|(id, viewport)| (*id, viewport.info.clone())) .collect(); + painter.handle_screenshots(&mut raw_input.events); + (viewport_ui_cb, raw_input) }; @@ -652,37 +655,14 @@ impl<'app> WgpuWinitRunning<'app> { true } }); - let screenshot_requested = !screenshot_commands.is_empty(); - let (vsync_secs, screenshot) = painter.paint_and_update_textures( + let vsync_secs = painter.paint_and_update_textures( viewport_id, pixels_per_point, app.clear_color(&egui_ctx.style().visuals), &clipped_primitives, &textures_delta, - screenshot_requested, + screenshot_commands, ); - match (screenshot_requested, screenshot) { - (false, None) => {} - (true, Some(screenshot)) => { - let screenshot = Arc::new(screenshot); - for user_data in screenshot_commands { - egui_winit - .egui_input_mut() - .events - .push(egui::Event::Screenshot { - viewport_id, - user_data, - image: screenshot.clone(), - }); - } - } - (true, None) => { - log::error!("Bug in egui_wgpu: screenshot requested, but no screenshot was taken"); - } - (false, Some(_)) => { - log::warn!("Bug in egui_wgpu: Got screenshot without requesting it"); - } - } for action in viewport.actions_requested.drain() { match action { @@ -1024,7 +1004,7 @@ fn render_immediate_viewport( [0.0, 0.0, 0.0, 0.0], &clipped_primitives, &textures_delta, - false, + vec![], ); egui_winit.handle_platform_output(window, platform_output); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 2f8f78d0818..d8abd3d4bd7 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,4 +1,5 @@ -use egui::TexturesDelta; +use egui::{TexturesDelta, UserData, ViewportCommand}; +use std::mem; use crate::{epi, App}; @@ -16,6 +17,9 @@ pub struct AppRunner { last_save_time: f64, pub(crate) text_agent: TextAgent, + // If not empty, the painter should capture the next frame + screenshot_commands: Vec, + // Output for the last run: textures_delta: TexturesDelta, clipped_primitives: Option>, @@ -36,7 +40,8 @@ impl AppRunner { app_creator: epi::AppCreator<'static>, text_agent: TextAgent, ) -> Result { - let painter = super::ActiveWebPainter::new(canvas, &web_options).await?; + let egui_ctx = egui::Context::default(); + let painter = super::ActiveWebPainter::new(egui_ctx.clone(), canvas, &web_options).await?; let info = epi::IntegrationInfo { web_info: epi::WebInfo { @@ -47,7 +52,6 @@ impl AppRunner { }; let storage = LocalStorage::default(); - let egui_ctx = egui::Context::default(); egui_ctx.set_os(egui::os::OperatingSystem::from_user_agent( &super::user_agent().unwrap_or_default(), )); @@ -110,6 +114,7 @@ impl AppRunner { needs_repaint, last_save_time: now_sec(), text_agent, + screenshot_commands: vec![], textures_delta: Default::default(), clipped_primitives: None, }; @@ -205,6 +210,8 @@ impl AppRunner { pub fn logic(&mut self) { // We sometimes miss blur/focus events due to the text agent, so let's just poll each frame: self.update_focus(); + // We might have received a screenshot + self.painter.handle_screenshots(&mut self.input.raw.events); let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx()); let mut raw_input = self.input.new_frame(canvas_size); @@ -225,12 +232,19 @@ impl AppRunner { if viewport_output.len() > 1 { log::warn!("Multiple viewports not yet supported on the web"); } - for viewport_output in viewport_output.values() { - for command in &viewport_output.commands { - // TODO(emilk): handle some of the commands - log::warn!( - "Unhandled egui viewport command: {command:?} - not implemented in web backend" - ); + for (_viewport_id, viewport_output) in viewport_output { + for command in viewport_output.commands { + match command { + ViewportCommand::Screenshot(user_data) => { + self.screenshot_commands.push(user_data); + } + _ => { + // TODO(emilk): handle some of the commands + log::warn!( + "Unhandled egui viewport command: {command:?} - not implemented in web backend" + ); + } + } } } @@ -250,6 +264,7 @@ impl AppRunner { &clipped_primitives, self.egui_ctx.pixels_per_point(), &textures_delta, + mem::take(&mut self.screenshot_commands), ) { log::error!("Failed to paint: {}", super::string_from_js_value(&err)); } diff --git a/crates/eframe/src/web/web_painter.rs b/crates/eframe/src/web/web_painter.rs index b5164b915c0..fe751bf169f 100644 --- a/crates/eframe/src/web/web_painter.rs +++ b/crates/eframe/src/web/web_painter.rs @@ -1,3 +1,4 @@ +use egui::{Event, UserData}; use wasm_bindgen::JsValue; /// Renderer for a browser canvas. @@ -16,14 +17,19 @@ pub(crate) trait WebPainter { fn max_texture_side(&self) -> usize; /// Update all internal textures and paint gui. + /// When `capture` isn't empty, the rendered screen should be captured. + /// Once the screenshot is ready, the screenshot should be returned via [`Self::handle_screenshots`]. fn paint_and_update_textures( &mut self, clear_color: [f32; 4], clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture: Vec, ) -> Result<(), JsValue>; + fn handle_screenshots(&mut self, events: &mut Vec); + /// Destroy all resources. fn destroy(&mut self); } diff --git a/crates/eframe/src/web/web_painter_glow.rs b/crates/eframe/src/web/web_painter_glow.rs index e13cb0018cd..876a6d78e2d 100644 --- a/crates/eframe/src/web/web_painter_glow.rs +++ b/crates/eframe/src/web/web_painter_glow.rs @@ -1,9 +1,10 @@ +use egui::{Event, UserData, ViewportId}; +use egui_glow::glow; +use std::sync::Arc; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_glow::glow; - use crate::{WebGlContextOption, WebOptions}; use super::web_painter::WebPainter; @@ -11,6 +12,7 @@ use super::web_painter::WebPainter; pub(crate) struct WebPainterGlow { canvas: HtmlCanvasElement, painter: egui_glow::Painter, + screenshots: Vec<(egui::ColorImage, Vec)>, } impl WebPainterGlow { @@ -18,7 +20,11 @@ impl WebPainterGlow { self.painter.gl() } - pub async fn new(canvas: HtmlCanvasElement, options: &WebOptions) -> Result { + pub async fn new( + _ctx: egui::Context, + canvas: HtmlCanvasElement, + options: &WebOptions, + ) -> Result { let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas, options.webgl_context_option)?; #[allow(clippy::arc_with_non_send_sync)] @@ -27,7 +33,11 @@ impl WebPainterGlow { let painter = egui_glow::Painter::new(gl, shader_prefix, None, options.dithering) .map_err(|err| format!("Error starting glow painter: {err}"))?; - Ok(Self { canvas, painter }) + Ok(Self { + canvas, + painter, + screenshots: Vec::new(), + }) } } @@ -46,6 +56,7 @@ impl WebPainter for WebPainterGlow { clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture: Vec, ) -> Result<(), JsValue> { let canvas_dimension = [self.canvas.width(), self.canvas.height()]; @@ -57,6 +68,11 @@ impl WebPainter for WebPainterGlow { self.painter .paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives); + if !capture.is_empty() { + let image = self.painter.read_screen_rgba(canvas_dimension); + self.screenshots.push((image, capture)); + } + for &id in &textures_delta.free { self.painter.free_texture(id); } @@ -67,6 +83,19 @@ impl WebPainter for WebPainterGlow { fn destroy(&mut self) { self.painter.destroy(); } + + fn handle_screenshots(&mut self, events: &mut Vec) { + for (image, data) in self.screenshots.drain(..) { + let image = Arc::new(image); + for data in data { + events.push(Event::Screenshot { + viewport_id: ViewportId::default(), + image: image.clone(), + user_data: data, + }); + } + } + } } /// Returns glow context and shader prefix. diff --git a/crates/eframe/src/web/web_painter_wgpu.rs b/crates/eframe/src/web/web_painter_wgpu.rs index ec487622e38..591d4224d3b 100644 --- a/crates/eframe/src/web/web_painter_wgpu.rs +++ b/crates/eframe/src/web/web_painter_wgpu.rs @@ -1,14 +1,13 @@ use std::sync::Arc; +use super::web_painter::WebPainter; +use crate::WebOptions; +use egui::{Event, UserData, ViewportId}; +use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; +use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; use wasm_bindgen::JsValue; use web_sys::HtmlCanvasElement; -use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup}; - -use crate::WebOptions; - -use super::web_painter::WebPainter; - pub(crate) struct WebPainterWgpu { canvas: HtmlCanvasElement, surface: wgpu::Surface<'static>, @@ -17,6 +16,10 @@ pub(crate) struct WebPainterWgpu { on_surface_error: Arc SurfaceErrorAction>, depth_format: Option, depth_texture_view: Option, + screen_capture_state: Option, + capture_tx: CaptureSender, + capture_rx: CaptureReceiver, + ctx: egui::Context, } impl WebPainterWgpu { @@ -54,6 +57,7 @@ impl WebPainterWgpu { #[allow(unused)] // only used if `wgpu` is the only active feature. pub async fn new( + ctx: egui::Context, canvas: web_sys::HtmlCanvasElement, options: &WebOptions, ) -> Result { @@ -119,17 +123,21 @@ impl WebPainterWgpu { .await .map_err(|err| err.to_string())?; + let default_configuration = surface + .get_default_config(&render_state.adapter, 0, 0) // Width/height is set later. + .ok_or("The surface isn't supported by this adapter")?; + let surface_configuration = wgpu::SurfaceConfiguration { format: render_state.target_format, present_mode: options.wgpu_options.present_mode, view_formats: vec![render_state.target_format], - ..surface - .get_default_config(&render_state.adapter, 0, 0) // Width/height is set later. - .ok_or("The surface isn't supported by this adapter")? + ..default_configuration }; log::debug!("wgpu painter initialized."); + let (capture_tx, capture_rx) = capture_channel(); + Ok(Self { canvas, render_state: Some(render_state), @@ -138,6 +146,10 @@ impl WebPainterWgpu { depth_format, depth_texture_view: None, on_surface_error: options.wgpu_options.on_surface_error.clone(), + screen_capture_state: None, + capture_tx, + capture_rx, + ctx, }) } } @@ -159,7 +171,10 @@ impl WebPainter for WebPainterWgpu { clipped_primitives: &[egui::ClippedPrimitive], pixels_per_point: f32, textures_delta: &egui::TexturesDelta, + capture_data: Vec, ) -> Result<(), JsValue> { + let capture = !capture_data.is_empty(); + let size_in_pixels = [self.canvas.width(), self.canvas.height()]; let Some(render_state) = &self.render_state else { @@ -203,7 +218,7 @@ impl WebPainter for WebPainterWgpu { // Resize surface if needed let is_zero_sized_surface = size_in_pixels[0] == 0 || size_in_pixels[1] == 0; - let frame = if is_zero_sized_surface { + let frame_and_capture_buffer = if is_zero_sized_surface { None } else { if size_in_pixels[0] != self.surface_configuration.width @@ -220,7 +235,7 @@ impl WebPainter for WebPainterWgpu { ); } - let frame = match self.surface.get_current_texture() { + let output_frame = match self.surface.get_current_texture() { Ok(frame) => frame, Err(err) => match (*self.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { @@ -236,12 +251,23 @@ impl WebPainter for WebPainterWgpu { { let renderer = render_state.renderer.read(); - let frame_view = frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()); + + let target_texture = if capture { + let capture_state = self.screen_capture_state.get_or_insert_with(|| { + CaptureState::new(&render_state.device, &output_frame.texture) + }); + capture_state.update(&render_state.device, &output_frame.texture); + + &capture_state.texture + } else { + &output_frame.texture + }; + let target_view = + target_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &frame_view, + view: &target_view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { @@ -280,7 +306,19 @@ impl WebPainter for WebPainterWgpu { ); } - Some(frame) + let mut capture_buffer = None; + + if capture { + if let Some(capture_state) = &mut self.screen_capture_state { + capture_buffer = Some(capture_state.copy_textures( + &render_state.device, + &output_frame, + &mut encoder, + )); + } + }; + + Some((output_frame, capture_buffer)) }; { @@ -295,13 +333,38 @@ impl WebPainter for WebPainterWgpu { .queue .submit(user_cmd_bufs.into_iter().chain([encoder.finish()])); - if let Some(frame) = frame { + if let Some((frame, capture_buffer)) = frame_and_capture_buffer { + if let Some(capture_buffer) = capture_buffer { + if let Some(capture_state) = &self.screen_capture_state { + capture_state.read_screen_rgba( + self.ctx.clone(), + capture_buffer, + capture_data, + self.capture_tx.clone(), + ViewportId::ROOT, + ); + } + } + frame.present(); } Ok(()) } + fn handle_screenshots(&mut self, events: &mut Vec) { + for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() { + let screenshot = Arc::new(screenshot); + for data in user_data { + events.push(Event::Screenshot { + viewport_id, + user_data: data, + image: screenshot.clone(), + }); + } + } + } + fn destroy(&mut self) { self.render_state = None; } diff --git a/crates/egui-wgpu/src/capture.rs b/crates/egui-wgpu/src/capture.rs new file mode 100644 index 00000000000..1ce780d054f --- /dev/null +++ b/crates/egui-wgpu/src/capture.rs @@ -0,0 +1,257 @@ +use egui::{UserData, ViewportId}; +use epaint::ColorImage; +use std::sync::{mpsc, Arc}; +use wgpu::{BindGroupLayout, MultisampleState, StoreOp}; + +/// A texture and a buffer for reading the rendered frame back to the cpu. +/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed +/// flag for the surface texture on all platforms. This means that anytime we want to +/// capture the frame, we first render it to this texture, and then we can copy it to +/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy), +/// from where we can pull it back +/// to the cpu. +pub struct CaptureState { + padding: BufferPadding, + pub texture: wgpu::Texture, + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, +} + +pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec, ColorImage)>; +pub type CaptureSender = mpsc::Sender<(ViewportId, Vec, ColorImage)>; +pub use mpsc::channel as capture_channel; + +impl CaptureState { + pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self { + let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl")); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("texture_copy"), + layout: None, + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + compilation_options: Default::default(), + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + compilation_options: Default::default(), + targets: &[Some(surface_texture.format().into())], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + multisample: MultisampleState::default(), + multiview: None, + cache: None, + }); + + let bind_group_layout = pipeline.get_bind_group_layout(0); + + let (texture, padding, bind_group) = + Self::create_texture(device, surface_texture, &bind_group_layout); + + Self { + padding, + texture, + pipeline, + bind_group, + } + } + + fn create_texture( + device: &wgpu::Device, + surface_texture: &wgpu::Texture, + layout: &BindGroupLayout, + ) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) { + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("egui_screen_capture_texture"), + size: surface_texture.size(), + mip_level_count: surface_texture.mip_level_count(), + sample_count: surface_texture.sample_count(), + dimension: surface_texture.dimension(), + format: surface_texture.format(), + usage: wgpu::TextureUsages::RENDER_ATTACHMENT + | wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_SRC, + view_formats: &[], + }); + + let padding = BufferPadding::new(surface_texture.width()); + + let view = texture.create_view(&Default::default()); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }], + label: None, + }); + + (texture, padding, bind_group) + } + + /// Updates the [`CaptureState`] if the size of the surface texture has changed + pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) { + if self.texture.size() != texture.size() { + let (new_texture, padding, bind_group) = + Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0)); + self.texture = new_texture; + self.padding = padding; + self.bind_group = bind_group; + } + } + + /// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer. + /// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu. + pub fn copy_textures( + &mut self, + device: &wgpu::Device, + output_frame: &wgpu::SurfaceTexture, + encoder: &mut wgpu::CommandEncoder, + ) -> wgpu::Buffer { + debug_assert_eq!( + self.texture.size(), + output_frame.texture.size(), + "Texture sizes must match, `CaptureState::update` was probably not called" + ); + + // It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but + // for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video) + // it might make sense to revisit this and implement a more efficient solution. + #[allow(clippy::arc_with_non_send_sync)] + let buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("egui_screen_capture_buffer"), + size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64, + usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, + mapped_at_creation: false, + }); + let padding = self.padding; + let tex = &mut self.texture; + + let tex_extent = tex.size(); + + encoder.copy_texture_to_buffer( + tex.as_image_copy(), + wgpu::ImageCopyBuffer { + buffer: &buffer, + layout: wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(padding.padded_bytes_per_row), + rows_per_image: None, + }, + }, + tex_extent, + ); + + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("texture_copy"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &output_frame.texture.create_view(&Default::default()), + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.draw(0..3, 0..1); + + buffer + } + + /// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu + /// This function is non-blocking and will send the data to the given sender when it's ready. + /// Pass in the buffer returned from [`CaptureState::copy_textures`]. + /// Make sure to call this after the encoder has been submitted. + pub fn read_screen_rgba( + &self, + ctx: egui::Context, + buffer: wgpu::Buffer, + data: Vec, + tx: CaptureSender, + viewport_id: ViewportId, + ) { + #[allow(clippy::arc_with_non_send_sync)] + let buffer = Arc::new(buffer); + let buffer_clone = buffer.clone(); + let buffer_slice = buffer_clone.slice(..); + let format = self.texture.format(); + let tex_extent = self.texture.size(); + let padding = self.padding; + let to_rgba = match format { + wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], + wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], + _ => { + log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", format); + return; + } + }; + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + if let Err(err) = result { + log::error!("Failed to map buffer for reading: {:?}", err); + return; + } + let buffer_slice = buffer.slice(..); + + let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize); + for padded_row in buffer_slice + .get_mapped_range() + .chunks(padding.padded_bytes_per_row as usize) + { + let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; + for color in row.chunks(4) { + pixels.push(epaint::Color32::from_rgba_premultiplied( + color[to_rgba[0]], + color[to_rgba[1]], + color[to_rgba[2]], + color[to_rgba[3]], + )); + } + } + buffer.unmap(); + + tx.send(( + viewport_id, + data, + ColorImage { + size: [tex_extent.width as usize, tex_extent.height as usize], + pixels, + }, + )) + .ok(); + ctx.request_repaint(); + }); + } +} + +#[derive(Copy, Clone)] +struct BufferPadding { + unpadded_bytes_per_row: u32, + padded_bytes_per_row: u32, +} + +impl BufferPadding { + fn new(width: u32) -> Self { + let bytes_per_pixel = std::mem::size_of::() as u32; + let unpadded_bytes_per_row = width * bytes_per_pixel; + let padded_bytes_per_row = + wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); + Self { + unpadded_bytes_per_row, + padded_bytes_per_row, + } + } +} diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 29dc8d8f5c2..54c76f05461 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -26,6 +26,9 @@ mod renderer; pub use renderer::*; use wgpu::{Adapter, Device, Instance, Queue}; +/// Helpers for capturing screenshots of the UI. +pub mod capture; + /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. #[cfg(feature = "winit")] pub mod winit; diff --git a/crates/egui-wgpu/src/texture_copy.wgsl b/crates/egui-wgpu/src/texture_copy.wgsl new file mode 100644 index 00000000000..4096d164cc3 --- /dev/null +++ b/crates/egui-wgpu/src/texture_copy.wgsl @@ -0,0 +1,43 @@ +struct VertexOutput { + @builtin(position) position: vec4, +}; + +var positions: array = array( + vec2f(-1.0, -3.0), + vec2f(-1.0, 1.0), + vec2f(3.0, 1.0) +); + +// meant to be called with 3 vertex indices: 0, 1, 2 +// draws one large triangle over the clip space like this: +// (the asterisks represent the clip space bounds) +//-1,1 1,1 +// --------------------------------- +// | * . +// | * . +// | * . +// | * . +// | * . +// | * . +// |*************** +// | . 1,-1 +// | . +// | . +// | . +// | . +// |. +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput { + var result: VertexOutput; + result.position = vec4f(positions[vertex_index], 0.0, 1.0); + return result; +} + +@group(0) +@binding(0) +var r_color: texture_2d; + +@fragment +fn fs_main(vertex: VertexOutput) -> @location(0) vec4 { + return textureLoad(r_color, vec2i(vertex.position.xy), 0); +} diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index 161773117b3..cf7c041f002 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -1,77 +1,16 @@ #![allow(clippy::missing_errors_doc)] #![allow(clippy::undocumented_unsafe_blocks)] -use std::{num::NonZeroU32, sync::Arc}; - -use egui::{ViewportId, ViewportIdMap, ViewportIdSet}; - +use crate::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState}; use crate::{renderer, RenderState, SurfaceErrorAction, WgpuConfiguration}; +use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet}; +use std::{num::NonZeroU32, sync::Arc}; struct SurfaceState { surface: wgpu::Surface<'static>, alpha_mode: wgpu::CompositeAlphaMode, width: u32, height: u32, - supports_screenshot: bool, -} - -/// A texture and a buffer for reading the rendered frame back to the cpu. -/// The texture is required since [`wgpu::TextureUsages::COPY_DST`] is not an allowed -/// flag for the surface texture on all platforms. This means that anytime we want to -/// capture the frame, we first render it to this texture, and then we can copy it to -/// both the surface texture and the buffer, from where we can pull it back to the cpu. -struct CaptureState { - texture: wgpu::Texture, - buffer: wgpu::Buffer, - padding: BufferPadding, -} - -impl CaptureState { - fn new(device: &Arc, surface_texture: &wgpu::Texture) -> Self { - let texture = device.create_texture(&wgpu::TextureDescriptor { - label: Some("egui_screen_capture_texture"), - size: surface_texture.size(), - mip_level_count: surface_texture.mip_level_count(), - sample_count: surface_texture.sample_count(), - dimension: surface_texture.dimension(), - format: surface_texture.format(), - usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, - view_formats: &[], - }); - - let padding = BufferPadding::new(surface_texture.width()); - - let buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("egui_screen_capture_buffer"), - size: (padding.padded_bytes_per_row * texture.height()) as u64, - usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, - mapped_at_creation: false, - }); - - Self { - texture, - buffer, - padding, - } - } -} - -struct BufferPadding { - unpadded_bytes_per_row: u32, - padded_bytes_per_row: u32, -} - -impl BufferPadding { - fn new(width: u32) -> Self { - let bytes_per_pixel = std::mem::size_of::() as u32; - let unpadded_bytes_per_row = width * bytes_per_pixel; - let padded_bytes_per_row = - wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT); - Self { - unpadded_bytes_per_row, - padded_bytes_per_row, - } - } } /// Everything you need to paint egui with [`wgpu`] on [`winit`]. @@ -80,6 +19,7 @@ impl BufferPadding { /// /// NOTE: all egui viewports share the same painter. pub struct Painter { + context: Context, configuration: WgpuConfiguration, msaa_samples: u32, support_transparent_backbuffer: bool, @@ -94,6 +34,8 @@ pub struct Painter { depth_texture_view: ViewportIdMap, msaa_texture_view: ViewportIdMap, surfaces: ViewportIdMap, + capture_tx: CaptureSender, + capture_rx: CaptureReceiver, } impl Painter { @@ -110,6 +52,7 @@ impl Painter { /// a [`winit::window::Window`] with a valid `.raw_window_handle()` /// associated. pub fn new( + context: Context, configuration: WgpuConfiguration, msaa_samples: u32, depth_format: Option, @@ -126,7 +69,10 @@ impl Painter { crate::WgpuSetup::Existing { instance, .. } => instance.clone(), }; + let (capture_tx, capture_rx) = capture_channel(); + Self { + context, configuration, msaa_samples, support_transparent_backbuffer, @@ -140,6 +86,9 @@ impl Painter { depth_texture_view: Default::default(), surfaces: Default::default(), msaa_texture_view: Default::default(), + + capture_tx, + capture_rx, } } @@ -157,17 +106,11 @@ impl Painter { ) { crate::profile_function!(); - let usage = if surface_state.supports_screenshot { - wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST - } else { - wgpu::TextureUsages::RENDER_ATTACHMENT - }; - let width = surface_state.width; let height = surface_state.height; let mut surf_config = wgpu::SurfaceConfiguration { - usage, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format: render_state.target_format, present_mode: config.present_mode, alpha_mode: surface_state.alpha_mode, @@ -292,8 +235,6 @@ impl Painter { } else { wgpu::CompositeAlphaMode::Auto }; - let supports_screenshot = - !matches!(render_state.adapter.get_info().backend, wgpu::Backend::Gl); self.surfaces.insert( viewport_id, SurfaceState { @@ -301,7 +242,6 @@ impl Painter { width: size.width, height: size.height, alpha_mode, - supports_screenshot, }, ); let Some(width) = NonZeroU32::new(size.width) else { @@ -417,109 +357,12 @@ impl Painter { } } - // CaptureState only needs to be updated when the size of the two textures don't match and we want to - // capture a frame - fn update_capture_state( - screen_capture_state: &mut Option, - surface_texture: &wgpu::SurfaceTexture, - render_state: &RenderState, - ) { - let surface_texture = &surface_texture.texture; - match screen_capture_state { - Some(capture_state) => { - if capture_state.texture.size() != surface_texture.size() { - *capture_state = CaptureState::new(&render_state.device, surface_texture); - } - } - None => { - *screen_capture_state = - Some(CaptureState::new(&render_state.device, surface_texture)); - } - } - } - - // Handles copying from the CaptureState texture to the surface texture and the cpu - fn read_screen_rgba( - screen_capture_state: &CaptureState, - render_state: &RenderState, - output_frame: &wgpu::SurfaceTexture, - ) -> Option { - let CaptureState { - texture: tex, - buffer, - padding, - } = screen_capture_state; - - let device = &render_state.device; - let queue = &render_state.queue; - - let tex_extent = tex.size(); - - let mut encoder = device.create_command_encoder(&Default::default()); - encoder.copy_texture_to_buffer( - tex.as_image_copy(), - wgpu::ImageCopyBuffer { - buffer, - layout: wgpu::ImageDataLayout { - offset: 0, - bytes_per_row: Some(padding.padded_bytes_per_row), - rows_per_image: None, - }, - }, - tex_extent, - ); - - encoder.copy_texture_to_texture( - tex.as_image_copy(), - output_frame.texture.as_image_copy(), - tex.size(), - ); - - let id = queue.submit(Some(encoder.finish())); - let buffer_slice = buffer.slice(..); - let (sender, receiver) = std::sync::mpsc::channel(); - buffer_slice.map_async(wgpu::MapMode::Read, move |v| { - drop(sender.send(v)); - }); - device.poll(wgpu::Maintain::WaitForSubmissionIndex(id)); - receiver.recv().ok()?.ok()?; - - let to_rgba = match tex.format() { - wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3], - wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3], - _ => { - log::error!("Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {:?}", tex.format()); - return None; - } - }; - - let mut pixels = Vec::with_capacity((tex.width() * tex.height()) as usize); - for padded_row in buffer_slice - .get_mapped_range() - .chunks(padding.padded_bytes_per_row as usize) - { - let row = &padded_row[..padding.unpadded_bytes_per_row as usize]; - for color in row.chunks(4) { - pixels.push(epaint::Color32::from_rgba_premultiplied( - color[to_rgba[0]], - color[to_rgba[1]], - color[to_rgba[2]], - color[to_rgba[3]], - )); - } - } - buffer.unmap(); - - Some(epaint::ColorImage { - size: [tex.width() as usize, tex.height() as usize], - pixels, - }) - } - /// Returns two things: /// /// The approximate number of seconds spent on vsync-waiting (if any), /// and the captures captured screenshot if it was requested. + /// + /// If `capture_data` isn't empty, a screenshot will be captured. pub fn paint_and_update_textures( &mut self, viewport_id: ViewportId, @@ -527,17 +370,18 @@ impl Painter { clear_color: [f32; 4], clipped_primitives: &[epaint::ClippedPrimitive], textures_delta: &epaint::textures::TexturesDelta, - capture: bool, - ) -> (f32, Option) { + capture_data: Vec, + ) -> f32 { crate::profile_function!(); + let capture = !capture_data.is_empty(); let mut vsync_sec = 0.0; let Some(render_state) = self.render_state.as_mut() else { - return (vsync_sec, None); + return vsync_sec; }; let Some(surface_state) = self.surfaces.get(&viewport_id) else { - return (vsync_sec, None); + return vsync_sec; }; let mut encoder = @@ -573,15 +417,6 @@ impl Painter { ) }; - let capture = match (capture, surface_state.supports_screenshot) { - (false, _) => false, - (true, true) => true, - (true, false) => { - log::error!("The active render surface doesn't support taking screenshots."); - false - } - }; - let output_frame = { crate::profile_scope!("get_current_texture"); // This is what vsync-waiting happens on my Mac. @@ -596,40 +431,35 @@ impl Painter { Err(err) => match (*self.configuration.on_surface_error)(err) { SurfaceErrorAction::RecreateSurface => { Self::configure_surface(surface_state, render_state, &self.configuration); - return (vsync_sec, None); + return vsync_sec; } SurfaceErrorAction::SkipFrame => { - return (vsync_sec, None); + return vsync_sec; } }, }; + let mut capture_buffer = None; { let renderer = render_state.renderer.read(); - let frame_view = if capture { - Self::update_capture_state( - &mut self.screen_capture_state, - &output_frame, - render_state, - ); - self.screen_capture_state - .as_ref() - .map_or_else( - || &output_frame.texture, - |capture_state| &capture_state.texture, - ) - .create_view(&wgpu::TextureViewDescriptor::default()) + + let target_texture = if capture { + let capture_state = self.screen_capture_state.get_or_insert_with(|| { + CaptureState::new(&render_state.device, &output_frame.texture) + }); + capture_state.update(&render_state.device, &output_frame.texture); + + &capture_state.texture } else { - output_frame - .texture - .create_view(&wgpu::TextureViewDescriptor::default()) + &output_frame.texture }; + let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default()); let (view, resolve_target) = (self.msaa_samples > 1) .then_some(self.msaa_texture_view.get(&viewport_id)) .flatten() - .map_or((&frame_view, None), |texture_view| { - (texture_view, Some(&frame_view)) + .map_or((&target_view, None), |texture_view| { + (texture_view, Some(&target_view)) }); let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { @@ -671,6 +501,16 @@ impl Painter { clipped_primitives, &screen_descriptor, ); + + if capture { + if let Some(capture_state) = &mut self.screen_capture_state { + capture_buffer = Some(capture_state.copy_textures( + &render_state.device, + &output_frame, + &mut encoder, + )); + } + } } let encoded = { @@ -699,15 +539,17 @@ impl Painter { } } - let screenshot = if capture { - self.screen_capture_state - .as_ref() - .and_then(|screen_capture_state| { - Self::read_screen_rgba(screen_capture_state, render_state, &output_frame) - }) - } else { - None - }; + if let Some(capture_buffer) = capture_buffer { + if let Some(screen_capture_state) = &mut self.screen_capture_state { + screen_capture_state.read_screen_rgba( + self.context.clone(), + capture_buffer, + capture_data, + self.capture_tx.clone(), + viewport_id, + ); + } + } { crate::profile_scope!("present"); @@ -717,7 +559,21 @@ impl Painter { vsync_sec += start.elapsed().as_secs_f32(); } - (vsync_sec, screenshot) + vsync_sec + } + + /// Call this at the beginning of each frame to receive the requested screenshots. + pub fn handle_screenshots(&self, events: &mut Vec) { + for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() { + let screenshot = Arc::new(screenshot); + for data in user_data { + events.push(Event::Screenshot { + viewport_id, + user_data: data, + image: screenshot.clone(), + }); + } + } } pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) { diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 5b57c675b5a..2cfcdfaeeba 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -38,6 +38,7 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index 8c9034868e4..c00725fbd59 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -24,6 +24,7 @@ pub mod painting; pub mod pan_zoom; pub mod panels; pub mod password; +pub mod screenshot; pub mod scrolling; pub mod sliders; pub mod strip_demo; diff --git a/crates/egui_demo_lib/src/demo/screenshot.rs b/crates/egui_demo_lib/src/demo/screenshot.rs new file mode 100644 index 00000000000..eb62611c863 --- /dev/null +++ b/crates/egui_demo_lib/src/demo/screenshot.rs @@ -0,0 +1,84 @@ +use egui::{Image, UserData, ViewportCommand, Widget}; +use std::sync::Arc; + +/// Showcase [`ViewportCommand::Screenshot`]. +#[derive(PartialEq, Eq, Default)] +pub struct Screenshot { + image: Option<(Arc, egui::TextureHandle)>, + continuous: bool, +} + +impl crate::Demo for Screenshot { + fn name(&self) -> &'static str { + "πŸ“· Screenshot" + } + + fn show(&mut self, ctx: &egui::Context, open: &mut bool) { + egui::Window::new(self.name()) + .open(open) + .resizable(false) + .default_width(250.0) + .show(ctx, |ui| { + use crate::View as _; + self.ui(ui); + }); + } +} + +impl crate::View for Screenshot { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.set_width(300.0); + ui.vertical_centered(|ui| { + ui.add(crate::egui_github_link_file!()); + }); + + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("This demo showcases how to take screenshots via "); + ui.code("ViewportCommand::Screenshot"); + ui.label("."); + }); + + ui.horizontal_top(|ui| { + let capture = ui.button("πŸ“· Take Screenshot").clicked(); + ui.checkbox(&mut self.continuous, "Capture continuously"); + if capture || self.continuous { + ui.ctx() + .send_viewport_cmd(ViewportCommand::Screenshot(UserData::default())); + } + }); + + let image = ui.ctx().input(|i| { + i.events + .iter() + .filter_map(|e| { + if let egui::Event::Screenshot { image, .. } = e { + Some(image.clone()) + } else { + None + } + }) + .last() + }); + + if let Some(image) = image { + self.image = Some(( + image.clone(), + ui.ctx() + .load_texture("screenshot_demo", image, Default::default()), + )); + } + + if let Some((_, texture)) = &self.image { + Image::new(texture).shrink_to_fit().ui(ui); + } else { + ui.group(|ui| { + ui.set_width(ui.available_width()); + ui.set_height(100.0); + ui.centered_and_justified(|ui| { + ui.label("No screenshot taken yet."); + }); + }); + } + } +} diff --git a/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png new file mode 100644 index 00000000000..56978de2001 --- /dev/null +++ b/crates/egui_demo_lib/tests/snapshots/demos/Screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:579a7a66f86ade628e9f469b0014e9010aa56312ad5bd1e8de2faaae7e0d1af6 +size 23770 From ea89c2935ef6bf1b9c4d2feed37f75101e03e726 Mon Sep 17 00:00:00 2001 From: Jay Oster Date: Thu, 12 Dec 2024 10:24:26 -0800 Subject: [PATCH 11/34] Android support for eframe (#5318) Android support is "almost there". This PR pushes it just a bit further by allowing `eframe` to be used on Android. It works by smuggling the `AndroidApp` required by `winit` through `NativeOptions`. The example isn't great because it doesn't leave space on the display for Android's top status bar or the lower navigation bar. I don't know what to do about that, yet. This is as far as I've managed to get it working. Another problem is that the development environment setup is completely awful for Android unless you happen to already be a full-time Android developer with everything configured on your build host. As a Rustacean, this makes me very sad. I've had some luck moving all of that mess to a container, adapted from https://github.com/SergioRibera/docker-rust-android. It takes care of all of the build dependencies, Android SDK, and the `cargo-apk` patches for bugs that I hit while getting the example to work on my device. (I also had to install an adb driver on my host and downloaded the Android platform-tools to get access to `adb`. An alternative is exposing the USB device to Docker. On Windows hosts, that means [installing `usbipd`](https://learn.microsoft.com/en-us/windows/wsl/connect-usb). A second alternative is using an `mtp` client to upload the APK as a file with USB file transfer enabled, then manually install it through the device's file manager.) I'm not including the docker stuff in this PR, but here are the files and instructions for future reference (and it will probably simplify manual testing and CI, FWIW!)
Dockerfile ```dockerfile FROM rust:1.76.0-slim # Variable arguments ARG JAVA_VERSION=17 ARG NDK_VERSION=25.1.8937393 ARG BUILDTOOLS_VERSION=30.0.0 ARG PLATFORM_VERSION=android-30 ARG CLITOOLS_VERSION=8512546_latest # Install Android requirements RUN apt-get update -yqq && \ apt-get install -y --no-install-recommends \ libcurl4-openssl-dev libssl-dev pkg-config build-essential git python3 wget zip unzip openjdk-${JAVA_VERSION}-jdk && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Install android targets RUN rustup target add armv7-linux-androideabi aarch64-linux-android # Install cargo-apk RUN git clone -b fix/bin-targets-workspace-members https://github.com/parasyte/cargo-apk.git /tmp/cargo-apk && \ cargo install --path /tmp/cargo-apk/cargo-apk # Generate Environment Variables ENV JAVA_VERSION=${JAVA_VERSION} ENV ANDROID_HOME=/opt/Android ENV NDK_HOME=/opt/Android/ndk/${NDK_VERSION} ENV ANDROID_NDK_ROOT=${NDK_HOME} ENV PATH=$PATH:${ANDROID_HOME}:${ANDROID_NDK_ROOT}:${ANDROID_HOME}/build-tools/${BUILDTOOLS_VERSION}:${ANDROID_HOME}/cmdline-tools/bin # Install command line tools RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \ wget -qc "https://dl.google.com/android/repository/commandlinetools-linux-${CLITOOLS_VERSION}.zip" -P /tmp && \ unzip -d ${ANDROID_HOME} /tmp/commandlinetools-linux-${CLITOOLS_VERSION}.zip && \ rm -fr /tmp/commandlinetools-linux-${CLITOOLS_VERSION}.zip # Install sdk requirements RUN echo y | sdkmanager --sdk_root=${ANDROID_HOME} --install \ "build-tools;${BUILDTOOLS_VERSION}" "ndk;${NDK_VERSION}" "platforms;${PLATFORM_VERSION}" # Create APK keystore for debug profile # Adapted from https://github.com/rust-mobile/cargo-apk/blob/caa806283dc26733ad8232dce1fa4896c566f7b8/ndk-build/src/ndk.rs#L393-L423 RUN keytool -genkey -v -keystore ${HOME}/.android/debug.keystore -storepass android -alias androiddebugkey \ -keypass android -dname 'CN=Android Debug,O=Android,C=US' -keyalg RSA -keysize 2048 -validity 10000 # Cleanup RUN rm -rf /tmp/* WORKDIR /src ENTRYPOINT [ "cargo", "apk", "build" ] ```
.dockerignore ```ignore # Ignore everything, only the Dockerfile is needed to build the container * ```
```sh docker build -t rust-android:latest . docker run --rm -it -v "$PWD:/src" rust-android:latest -p hello_android adb install target/debug/apk/hello_android.apk ``` * Part of #2066 * [x] I have followed the instructions in the PR template --- Cargo.lock | 38 ++++++++++++++++ crates/eframe/src/epi.rs | 16 +++++++ crates/eframe/src/native/run.rs | 11 +++++ examples/hello_android/Cargo.toml | 32 +++++++++++++ examples/hello_android/README.md | 20 +++++++++ examples/hello_android/screenshot.png | 3 ++ examples/hello_android/src/lib.rs | 65 +++++++++++++++++++++++++++ 7 files changed, 185 insertions(+) create mode 100644 examples/hello_android/Cargo.toml create mode 100644 examples/hello_android/README.md create mode 100644 examples/hello_android/screenshot.png create mode 100644 examples/hello_android/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 3837d7ec4c0..ebe0aa08f70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,6 +189,23 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" +[[package]] +name = "android_log-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" + +[[package]] +name = "android_logger" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -1473,6 +1490,16 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -1935,6 +1962,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hello_android" +version = "0.1.0" +dependencies = [ + "android_logger", + "eframe", + "egui_extras", + "log", + "winit", +] + [[package]] name = "hello_world" version = "0.1.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 9f4f6dde8b1..53051f398e4 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -364,6 +364,16 @@ pub struct NativeOptions { /// /// Defaults to true. pub dithering: bool, + + /// Android application for `winit`'s event loop. + /// + /// This value is required on Android to correctly create the event loop. See + /// [`EventLoopBuilder::build`] and [`with_android_app`] for details. + /// + /// [`EventLoopBuilder::build`]: winit::event_loop::EventLoopBuilder::build + /// [`with_android_app`]: winit::platform::android::EventLoopBuilderExtAndroid::with_android_app + #[cfg(target_os = "android")] + pub android_app: Option, } #[cfg(not(target_arch = "wasm32"))] @@ -383,6 +393,9 @@ impl Clone for NativeOptions { persistence_path: self.persistence_path.clone(), + #[cfg(target_os = "android")] + android_app: self.android_app.clone(), + ..*self } } @@ -424,6 +437,9 @@ impl Default for NativeOptions { persistence_path: None, dithering: true, + + #[cfg(target_os = "android")] + android_app: None, } } } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 43dd07ee0da..219edd56625 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -17,9 +17,20 @@ use crate::{ // ---------------------------------------------------------------------------- fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result> { + #[cfg(target_os = "android")] + use winit::platform::android::EventLoopBuilderExtAndroid as _; + crate::profile_function!(); let mut builder = winit::event_loop::EventLoop::with_user_event(); + #[cfg(target_os = "android")] + let mut builder = + builder.with_android_app(native_options.android_app.take().ok_or_else(|| { + crate::Error::AppCreation(Box::from( + "`NativeOptions` is missing required `android_app`", + )) + })?); + if let Some(hook) = std::mem::take(&mut native_options.event_loop_builder) { hook(&mut builder); } diff --git a/examples/hello_android/Cargo.toml b/examples/hello_android/Cargo.toml new file mode 100644 index 00000000000..dcb0a5a5c01 --- /dev/null +++ b/examples/hello_android/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "hello_android" +version = "0.1.0" +authors = ["Emil Ernerfeldt "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.76" +publish = false + +# `unsafe_code` is required for `#[no_mangle]`, disable workspace lints to workaround lint error. +# [lints] +# workspace = true + +[lib] +crate-type = ["cdylib"] + + +[dependencies] +eframe = { workspace = true, features = [ + "default", + "android-native-activity", +] } + +# For image support: +egui_extras = { workspace = true, features = ["default", "image"] } + +log = { workspace = true } +winit = { workspace = true } +android_logger = "0.14" + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android" ] diff --git a/examples/hello_android/README.md b/examples/hello_android/README.md new file mode 100644 index 00000000000..fe14eb9face --- /dev/null +++ b/examples/hello_android/README.md @@ -0,0 +1,20 @@ +Hello world example for Android. + +Use `cargo-apk` to build and run. Requires a patch to workaround [an upstream bug](https://github.com/rust-mobile/cargo-subcommand/issues/29). + +One-time setup: + +```sh +cargo install \ + --git https://github.com/parasyte/cargo-apk.git \ + --rev 282639508eeed7d73f2e1eaeea042da2716436d5 \ + cargo-apk +``` + +Build and run: + +```sh +cargo apk run -p hello_android +``` + +![](screenshot.png) diff --git a/examples/hello_android/screenshot.png b/examples/hello_android/screenshot.png new file mode 100644 index 00000000000..91179fa2f41 --- /dev/null +++ b/examples/hello_android/screenshot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7add91d7d6b73f48e98f20d84cba3bd3a950cf97aa31f5e9fa93da9af98e876c +size 120019 diff --git a/examples/hello_android/src/lib.rs b/examples/hello_android/src/lib.rs new file mode 100644 index 00000000000..adda66ca5f7 --- /dev/null +++ b/examples/hello_android/src/lib.rs @@ -0,0 +1,65 @@ +#![cfg(target_os = "android")] +#![allow(rustdoc::missing_crate_level_docs)] // it's an example + +use android_logger::Config; +use eframe::egui; +use log::LevelFilter; +use winit::platform::android::activity::AndroidApp; + +#[no_mangle] +fn android_main(app: AndroidApp) { + // Log to android output + android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); + + let options = eframe::NativeOptions { + android_app: Some(app), + ..Default::default() + }; + eframe::run_native( + "My egui App", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) + .unwrap() +} + +struct MyApp { + name: String, + age: u32, +} + +impl Default for MyApp { + fn default() -> Self { + Self { + name: "Arthur".to_owned(), + age: 42, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.heading("My egui Application"); + ui.horizontal(|ui| { + let name_label = ui.label("Your name: "); + ui.text_edit_singleline(&mut self.name) + .labelled_by(name_label.id); + }); + ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); + if ui.button("Increment").clicked() { + self.age += 1; + } + ui.label(format!("Hello '{}', age {}", self.name, self.age)); + + ui.image(egui::include_image!( + "../../../crates/egui/assets/ferris.png" + )); + }); + } +} From ba060a2c878bfefa5aead8e5fe0b9e69ab4c9048 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:47:41 +0100 Subject: [PATCH 12/34] Drag-and-drop: keep cursor set by user, if any (#5467) We used to always set the cursor to `Grabbing` when a drag and drop payload is set, but this shadows user code trying to set an alternative cursor (e.g. `NoDrop`). This PR no only change the cursor to `Grabbing` if is way previously set to `Default`. --- crates/egui/src/drag_and_drop.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/drag_and_drop.rs b/crates/egui/src/drag_and_drop.rs index e1997800834..fc9f29f5e03 100644 --- a/crates/egui/src/drag_and_drop.rs +++ b/crates/egui/src/drag_and_drop.rs @@ -57,7 +57,13 @@ impl DragAndDrop { if abort_dnd_due_to_mouse_release { Self::clear_payload(ctx); } else { - ctx.set_cursor_icon(CursorIcon::Grabbing); + // We set the cursor icon only if its default, as the user code might have + // explicitly set it already. + ctx.output_mut(|o| { + if o.cursor_icon == CursorIcon::Default { + o.cursor_icon = CursorIcon::Grabbing; + } + }); } } } From 9aae14cdf4e50907805303c60cc96b6b2fff788e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 13 Dec 2024 07:43:35 +0100 Subject: [PATCH 13/34] Shorter `Debug` formatting of `LayerId` --- crates/egui/src/layers.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 595f67034f5..1133f13a97b 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -60,7 +60,7 @@ impl Order { /// An identifier for a paint layer. /// Also acts as an identifier for [`crate::Area`]:s. -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct LayerId { pub order: Order, @@ -102,6 +102,13 @@ impl LayerId { } } +impl std::fmt::Debug for LayerId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { order, id } = self; + write!(f, "LayerId {{ {order:?} {id:?} }}") + } +} + /// A unique identifier of a specific [`Shape`] in a [`PaintList`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] From 3af907919be1fdaa228f10dbe7b5da3bf0e16510 Mon Sep 17 00:00:00 2001 From: Ted de Munnik Date: Mon, 16 Dec 2024 09:15:54 +0100 Subject: [PATCH 14/34] Use `profiling` crate to support more profiler backends (#5150) Hey! I am not sure if this is something that's been considered before and decided against (I couldn't find any PR's or issues). This change removes the internal profiling macros in library crates and the `puffin` feature and replaces it with similar functions in the [profiling](https://github.com/aclysma/profiling) crate. This crate provides a layer of abstraction over various profiler instrumentation crates and allows library users to pick their favorite (supported) profiler. An additional benefit for puffin users is that dependencies of egui are included in the instrumentation output too (mainly wgpu which uses the profiling crate), so more details might be available when profiling. A breaking change is that instead of using the `puffin` feature on egui, users that want to profile the crate with puffin instead have to enable the `profile-with-puffin` feature on the profiling crate. Similarly they could instead choose to use `profile-with-tracy` etc. I tried to add a 'tracy' feature to egui_demo_app in order to showcase , however the /scripts/check.sh currently breaks on mutually exclusive features (which this introduces), so I decided against including it for the initial PR. I'm happy to iterate more on this if there is interest in taking this PR though. Screenshot showing the additional info for wgpu now available when using puffin ![image](https://github.com/user-attachments/assets/49fc0e7e-8f88-40cb-a69e-74ca2e3f90f3) --- Cargo.lock | 30 +++++++--- Cargo.toml | 1 + crates/eframe/Cargo.toml | 15 +---- crates/eframe/src/epi.rs | 7 +-- crates/eframe/src/icon_data.rs | 6 +- crates/eframe/src/lib.rs | 41 ++++--------- crates/eframe/src/native/app_icon.rs | 8 +-- crates/eframe/src/native/epi_integration.rs | 36 ++++++----- crates/eframe/src/native/file_storage.rs | 14 ++--- crates/eframe/src/native/glow_integration.rs | 48 +++++++-------- crates/eframe/src/native/run.rs | 14 ++--- crates/eframe/src/native/wgpu_integration.rs | 32 +++++----- crates/eframe/src/native/winit_integration.rs | 2 +- crates/egui-wgpu/Cargo.toml | 8 +-- crates/egui-wgpu/src/lib.rs | 38 ++---------- crates/egui-wgpu/src/renderer.rs | 46 +++++++------- crates/egui-wgpu/src/winit.rs | 20 +++---- crates/egui-winit/Cargo.toml | 5 +- crates/egui-winit/src/clipboard.rs | 4 +- crates/egui-winit/src/lib.rs | 59 +++++------------- crates/egui-winit/src/window_settings.rs | 7 +-- crates/egui/Cargo.toml | 6 +- crates/egui/src/context.rs | 60 ++++++++++--------- crates/egui/src/hit_test.rs | 2 +- crates/egui/src/input_state/mod.rs | 2 +- crates/egui/src/interaction.rs | 2 +- crates/egui/src/layers.rs | 2 +- crates/egui/src/lib.rs | 42 ++++--------- crates/egui/src/memory/mod.rs | 2 +- crates/egui/src/pass_state.rs | 2 +- crates/egui/src/util/id_type_map.rs | 12 ++-- crates/egui/src/viewport.rs | 4 +- crates/egui_demo_app/Cargo.toml | 6 +- crates/egui_demo_app/src/main.rs | 1 + crates/egui_extras/Cargo.toml | 7 +-- crates/egui_extras/src/image.rs | 5 +- crates/egui_extras/src/lib.rs | 30 ---------- crates/egui_extras/src/syntax_highlighting.rs | 7 +-- crates/egui_glow/Cargo.toml | 6 +- crates/egui_glow/src/lib.rs | 30 ---------- crates/egui_glow/src/painter.rs | 25 ++++---- crates/epaint/Cargo.toml | 7 +-- crates/epaint/src/lib.rs | 30 ---------- crates/epaint/src/mesh.rs | 4 +- crates/epaint/src/tessellator.rs | 14 ++--- examples/puffin_profiler/Cargo.toml | 5 +- 46 files changed, 272 insertions(+), 482 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ebe0aa08f70..8d43ee086ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1235,7 +1235,7 @@ dependencies = [ "parking_lot", "percent-encoding", "pollster 0.4.0", - "puffin", + "profiling", "raw-window-handle 0.6.2", "ron", "serde", @@ -1263,7 +1263,7 @@ dependencies = [ "epaint", "log", "nohash-hasher", - "puffin", + "profiling", "ron", "serde", ] @@ -1278,7 +1278,7 @@ dependencies = [ "egui", "epaint", "log", - "puffin", + "profiling", "thiserror", "type-map", "web-time", @@ -1296,7 +1296,7 @@ dependencies = [ "document-features", "egui", "log", - "puffin", + "profiling", "raw-window-handle 0.6.2", "serde", "smithay-clipboard", @@ -1321,6 +1321,7 @@ dependencies = [ "image", "log", "poll-promise", + "profiling", "puffin", "puffin_http", "rfd", @@ -1360,7 +1361,7 @@ dependencies = [ "image", "log", "mime_guess2", - "puffin", + "profiling", "resvg", "serde", "syntect", @@ -1380,7 +1381,7 @@ dependencies = [ "glutin-winit", "log", "memoffset", - "puffin", + "profiling", "wasm-bindgen", "web-sys", "winit", @@ -1528,7 +1529,7 @@ dependencies = [ "log", "nohash-hasher", "parking_lot", - "puffin", + "profiling", "rayon", "serde", ] @@ -3109,6 +3110,20 @@ name = "profiling" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", + "puffin", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] [[package]] name = "puffin" @@ -3147,6 +3162,7 @@ dependencies = [ "eframe", "env_logger", "log", + "profiling", "puffin", "puffin_http", ] diff --git a/Cargo.toml b/Cargo.toml index 6b50ac23a0a..5c00189d068 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ log = { version = "0.4", features = ["std"] } nohash-hasher = "0.2" parking_lot = "0.12" pollster = "0.4" +profiling = {version = "1.0", default-features = false } puffin = "0.19" puffin_http = "0.16" raw-window-handle = "0.6.0" diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 87ab1d3b2c9..c07dc9d0d3a 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -71,19 +71,6 @@ persistence = [ "serde", ] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## `eframe` will call `puffin::GlobalProfiler::lock().new_frame()` for you -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = [ - "dep:puffin", - "egui/puffin", - "egui_glow?/puffin", - "egui-wgpu?/puffin", - "egui-winit/puffin", -] - ## Enables wayland support and fixes clipboard issue. wayland = ["egui-winit/wayland", "egui-wgpu?/wayland", "egui_glow?/wayland", "glutin?/wayland", "glutin-winit?/wayland"] @@ -127,6 +114,7 @@ ahash.workspace = true document-features.workspace = true log.workspace = true parking_lot.workspace = true +profiling.workspace = true raw-window-handle.workspace = true static_assertions = "1.1.0" web-time.workspace = true @@ -157,7 +145,6 @@ pollster = { workspace = true, optional = true } # needed for wgpu glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] } glutin-winit = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] } home = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } wgpu = { workspace = true, optional = true, features = [ # Let's enable some backends so that users can use `eframe` out-of-the-box # without having to explicitly opt-in to backends diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 53051f398e4..45d8335018e 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -788,8 +788,7 @@ pub struct IntegrationInfo { /// /// This includes [`App::update`] as well as rendering (except for vsync waiting). /// - /// For a more detailed view of cpu usage, use the [`puffin`](https://crates.io/crates/puffin) - /// profiler together with the `puffin` feature of `eframe`. + /// For a more detailed view of cpu usage, connect your preferred profiler by enabling it's feature in [`profiling`](https://crates.io/crates/profiling). /// /// `None` if this is the first frame. pub cpu_usage: Option, @@ -831,7 +830,7 @@ impl Storage for DummyStorage { /// Get and deserialize the [RON](https://github.com/ron-rs/ron) stored at the given key. #[cfg(feature = "ron")] pub fn get_value(storage: &dyn Storage, key: &str) -> Option { - crate::profile_function!(key); + profiling::function_scope!(key); storage .get_string(key) .and_then(|value| match ron::from_str(&value) { @@ -847,7 +846,7 @@ pub fn get_value(storage: &dyn Storage, key: &st /// Serialize the given value as [RON](https://github.com/ron-rs/ron) and store with the given key. #[cfg(feature = "ron")] pub fn set_value(storage: &mut dyn Storage, key: &str, value: &T) { - crate::profile_function!(key); + profiling::function_scope!(key); match ron::ser::to_string(value) { Ok(string) => storage.set_string(key, string), Err(err) => log::error!("eframe failed to encode data using ron: {}", err), diff --git a/crates/eframe/src/icon_data.rs b/crates/eframe/src/icon_data.rs index ed514d00e1f..4851bee64b3 100644 --- a/crates/eframe/src/icon_data.rs +++ b/crates/eframe/src/icon_data.rs @@ -22,7 +22,7 @@ pub trait IconDataExt { /// # Errors /// If this is not a valid png. pub fn from_png_bytes(png_bytes: &[u8]) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let image = image::load_from_memory(png_bytes)?; Ok(from_image(image)) } @@ -38,7 +38,7 @@ fn from_image(image: image::DynamicImage) -> IconData { impl IconDataExt for IconData { fn to_image(&self) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Self { rgba, width, @@ -48,7 +48,7 @@ impl IconDataExt for IconData { } fn to_png_bytes(&self) -> Result, String> { - crate::profile_function!(); + profiling::function_scope!(); let image = self.to_image()?; let mut png_bytes: Vec = Vec::new(); image diff --git a/crates/eframe/src/lib.rs b/crates/eframe/src/lib.rs index 80da6235792..7b342a4c21f 100644 --- a/crates/eframe/src/lib.rs +++ b/crates/eframe/src/lib.rs @@ -129,6 +129,17 @@ //! ## Feature flags #![doc = document_features::document_features!()] //! +//! ## Instrumentation +//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation. +//! You can enable features on the profiling crates in your application to add instrumentation for all +//! crates that support it, including egui. See the profiling crate docs for more information. +//! ```toml +//! [dependencies] +//! profiling = "1.0" +//! [features] +//! profile-with-puffin = ["profiling/profile-with-puffin"] +//! ``` +//! #![warn(missing_docs)] // let's keep eframe well-documented #![allow(clippy::needless_doctest_main)] @@ -445,33 +456,3 @@ impl std::fmt::Display for Error { /// Short for `Result`. pub type Result = std::result::Result; - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index 840bf367b27..8591ba2a8cc 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -59,7 +59,7 @@ enum AppIconStatus { /// Since window creation can be lazy, call this every frame until it's either successfully or gave up. /// (See [`AppIconStatus`]) fn set_title_and_icon(_title: &str, _icon_data: Option<&IconData>) -> AppIconStatus { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(target_os = "windows")] { @@ -201,7 +201,7 @@ fn set_app_icon_windows(icon_data: &IconData) -> AppIconStatus { #[allow(unsafe_code)] fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconStatus { use crate::icon_data::IconDataExt as _; - crate::profile_function!(); + profiling::function_scope!(); use objc2::ClassType; use objc2_app_kit::{NSApplication, NSImage}; @@ -237,7 +237,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS log::trace!("NSImage::initWithData…"); let app_icon = NSImage::initWithData(NSImage::alloc(), &data); - crate::profile_scope!("setApplicationIconImage_"); + profiling::scope!("setApplicationIconImage_"); log::trace!("setApplicationIconImage…"); app.setApplicationIconImage(app_icon.as_deref()); } @@ -246,7 +246,7 @@ fn set_title_and_icon_mac(title: &str, icon_data: Option<&IconData>) -> AppIconS if let Some(main_menu) = app.mainMenu() { if let Some(item) = main_menu.itemAtIndex(0) { if let Some(app_menu) = item.submenu() { - crate::profile_scope!("setTitle_"); + profiling::scope!("setTitle_"); app_menu.setTitle(&NSString::from_str(title)); } } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 5f9d555e3a5..03b5f2dcd46 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -19,7 +19,7 @@ pub fn viewport_builder( native_options: &mut epi::NativeOptions, window_settings: Option, ) -> ViewportBuilder { - crate::profile_function!(); + profiling::function_scope!(); let mut viewport_builder = native_options.viewport.clone(); @@ -67,7 +67,7 @@ pub fn viewport_builder( #[cfg(not(target_os = "ios"))] if native_options.centered { - crate::profile_scope!("center"); + profiling::scope!("center"); if let Some(monitor) = event_loop .primary_monitor() .or_else(|| event_loop.available_monitors().next()) @@ -94,8 +94,7 @@ pub fn apply_window_settings( window: &winit::window::Window, window_settings: Option, ) { - crate::profile_function!(); - + profiling::function_scope!(); if let Some(window_settings) = window_settings { window_settings.initialize_window(window); } @@ -103,12 +102,11 @@ pub fn apply_window_settings( #[cfg(not(target_os = "ios"))] fn largest_monitor_point_size(egui_zoom_factor: f32, event_loop: &ActiveEventLoop) -> egui::Vec2 { - crate::profile_function!(); - + profiling::function_scope!(); let mut max_size = egui::Vec2::ZERO; let available_monitors = { - crate::profile_scope!("available_monitors"); + profiling::scope!("available_monitors"); event_loop.available_monitors() }; @@ -238,7 +236,7 @@ impl EpiIntegration { egui_winit: &mut egui_winit::State, event: &winit::event::WindowEvent, ) -> EventResponse { - crate::profile_function!(egui_winit::short_window_event_description(event)); + profiling::function_scope!(egui_winit::short_window_event_description(event)); use winit::event::{ElementState, MouseButton, WindowEvent}; @@ -276,10 +274,10 @@ impl EpiIntegration { let full_output = self.egui_ctx.run(raw_input, |egui_ctx| { if let Some(viewport_ui_cb) = viewport_ui_cb { // Child viewport - crate::profile_scope!("viewport_callback"); + profiling::scope!("viewport_callback"); viewport_ui_cb(egui_ctx); } else { - crate::profile_scope!("App::update"); + profiling::scope!("App::update"); app.update(egui_ctx, &mut self.frame); } }); @@ -306,7 +304,7 @@ impl EpiIntegration { } pub fn post_rendering(&mut self, window: &winit::window::Window) { - crate::profile_function!(); + profiling::function_scope!(); if std::mem::take(&mut self.is_first_frame) { // We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279 window.set_visible(true); @@ -332,11 +330,11 @@ impl EpiIntegration { pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) { #[cfg(feature = "persistence")] if let Some(storage) = self.frame.storage_mut() { - crate::profile_function!(); + profiling::function_scope!(); if let Some(window) = _window { if self.persist_window { - crate::profile_scope!("native_window"); + profiling::scope!("native_window"); epi::set_value( storage, STORAGE_WINDOW_KEY, @@ -345,23 +343,23 @@ impl EpiIntegration { } } if _app.persist_egui_memory() { - crate::profile_scope!("egui_memory"); + profiling::scope!("egui_memory"); self.egui_ctx .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem)); } { - crate::profile_scope!("App::save"); + profiling::scope!("App::save"); _app.save(storage); } - crate::profile_scope!("Storage::flush"); + profiling::scope!("Storage::flush"); storage.flush(); } } } fn load_default_egui_icon() -> egui::IconData { - crate::profile_function!(); + profiling::function_scope!(); crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap() } @@ -372,7 +370,7 @@ const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; const STORAGE_WINDOW_KEY: &str = "window"; pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_WINDOW_KEY) @@ -382,7 +380,7 @@ pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option) -> Option { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "persistence")] { epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY) diff --git a/crates/eframe/src/native/file_storage.rs b/crates/eframe/src/native/file_storage.rs index c47a71e6867..346c46b4254 100644 --- a/crates/eframe/src/native/file_storage.rs +++ b/crates/eframe/src/native/file_storage.rs @@ -100,7 +100,7 @@ pub struct FileStorage { impl Drop for FileStorage { fn drop(&mut self) { if let Some(join_handle) = self.last_save_join_handle.take() { - crate::profile_scope!("wait_for_save"); + profiling::scope!("wait_for_save"); join_handle.join().ok(); } } @@ -109,7 +109,7 @@ impl Drop for FileStorage { impl FileStorage { /// Store the state in this .ron file. pub(crate) fn from_ron_filepath(ron_filepath: impl Into) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let ron_filepath: PathBuf = ron_filepath.into(); log::debug!("Loading app state from {:?}…", ron_filepath); Self { @@ -122,7 +122,7 @@ impl FileStorage { /// Find a good place to put the files that the OS likes. pub fn from_app_id(app_id: &str) -> Option { - crate::profile_function!(app_id); + profiling::function_scope!(); if let Some(data_dir) = storage_dir(app_id) { if let Err(err) = std::fs::create_dir_all(&data_dir) { log::warn!( @@ -155,7 +155,7 @@ impl crate::Storage for FileStorage { fn flush(&mut self) { if self.dirty { - crate::profile_function!(); + profiling::scope!("FileStorage::flush"); self.dirty = false; let file_path = self.ron_filepath.clone(); @@ -184,7 +184,7 @@ impl crate::Storage for FileStorage { } fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { - crate::profile_function!(); + profiling::function_scope!(); if let Some(parent_dir) = file_path.parent() { if !parent_dir.exists() { @@ -199,7 +199,7 @@ fn save_to_disk(file_path: &PathBuf, kv: &HashMap) { let mut writer = std::io::BufWriter::new(file); let config = Default::default(); - crate::profile_scope!("ron::serialize"); + profiling::scope!("ron::serialize"); if let Err(err) = ron::ser::to_writer_pretty(&mut writer, &kv, config) .and_then(|_| writer.flush().map_err(|err| err.into())) { @@ -220,7 +220,7 @@ fn read_ron(ron_path: impl AsRef) -> Option where T: serde::de::DeserializeOwned, { - crate::profile_function!(); + profiling::function_scope!(); match std::fs::File::open(ron_path) { Ok(file) => { let reader = std::io::BufReader::new(file); diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 51c0cadce2a..f17c6ad5091 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -129,7 +129,7 @@ impl<'app> GlowWinitApp<'app> { native_options: NativeOptions, app_creator: AppCreator<'app>, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); Self { repaint_proxy: Arc::new(egui::mutex::Mutex::new(event_loop.create_proxy())), app_name: app_name.to_owned(), @@ -146,8 +146,7 @@ impl<'app> GlowWinitApp<'app> { storage: Option<&dyn Storage>, native_options: &mut NativeOptions, ) -> Result<(GlutinWindowContext, egui_glow::Painter)> { - crate::profile_function!(); - + profiling::function_scope!(); let window_settings = epi_integration::load_window_settings(storage); let winit_window_builder = epi_integration::viewport_builder( @@ -172,7 +171,7 @@ impl<'app> GlowWinitApp<'app> { } let gl = unsafe { - crate::profile_scope!("glow::Context::from_loader_function"); + profiling::scope!("glow::Context::from_loader_function"); Arc::new(glow::Context::from_loader_function(|s| { let s = std::ffi::CString::new(s) .expect("failed to construct C string from string for gl proc address"); @@ -195,7 +194,7 @@ impl<'app> GlowWinitApp<'app> { &mut self, event_loop: &ActiveEventLoop, ) -> Result<&mut GlowWinitRunning<'app>> { - crate::profile_function!(); + profiling::function_scope!(); let storage = if let Some(file) = &self.native_options.persistence_path { epi_integration::create_storage_with_file(file) @@ -308,7 +307,7 @@ impl<'app> GlowWinitApp<'app> { raw_display_handle: window.display_handle().map(|h| h.as_raw()), raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; - crate::profile_scope!("app_creator"); + profiling::scope!("app_creator"); app_creator(&cc).map_err(crate::Error::AppCreation)? }; @@ -369,7 +368,7 @@ impl<'app> WinitApp for GlowWinitApp<'app> { fn save_and_destroy(&mut self) { if let Some(mut running) = self.running.take() { - crate::profile_function!(); + profiling::function_scope!(); running.integration.save( running.app.as_mut(), @@ -486,7 +485,7 @@ impl<'app> GlowWinitRunning<'app> { event_loop: &ActiveEventLoop, window_id: WindowId, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Some(viewport_id) = self .glutin @@ -498,8 +497,7 @@ impl<'app> GlowWinitRunning<'app> { return Ok(EventResult::Wait); }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); + profiling::finish_frame!(); let mut frame_timer = crate::stopwatch::Stopwatch::new(); frame_timer.start(); @@ -698,7 +696,7 @@ impl<'app> GlowWinitRunning<'app> { { // vsync - don't count as frame-time: frame_timer.pause(); - crate::profile_scope!("swap_buffers"); + profiling::scope!("swap_buffers"); let context = current_gl_context .as_ref() .ok_or(egui_glow::PainterError::from( @@ -726,7 +724,7 @@ impl<'app> GlowWinitRunning<'app> { if window.is_minimized() == Some(true) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); + profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } @@ -857,7 +855,7 @@ fn change_gl_context( not_current_gl_context: &mut Option, gl_surface: &glutin::surface::Surface, ) { - crate::profile_function!(); + profiling::function_scope!(); if !cfg!(target_os = "windows") { // According to https://github.com/emilk/egui/issues/4289 @@ -866,7 +864,7 @@ fn change_gl_context( // See https://github.com/emilk/egui/issues/4173 if let Some(current_gl_context) = current_gl_context { - crate::profile_scope!("is_current"); + profiling::scope!("is_current"); if gl_surface.is_current(current_gl_context) { return; // Early-out to save a lot of time. } @@ -876,7 +874,7 @@ fn change_gl_context( let not_current = if let Some(not_current_context) = not_current_gl_context.take() { not_current_context } else { - crate::profile_scope!("make_not_current"); + profiling::scope!("make_not_current"); current_gl_context .take() .unwrap() @@ -884,7 +882,7 @@ fn change_gl_context( .unwrap() }; - crate::profile_scope!("make_current"); + profiling::scope!("make_current"); *current_gl_context = Some(not_current.make_current(gl_surface).unwrap()); } @@ -896,7 +894,7 @@ impl GlutinWindowContext { native_options: &NativeOptions, event_loop: &ActiveEventLoop, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); // There is a lot of complexity with opengl creation, // so prefer extensive logging to get all the help we can to debug issues. @@ -952,7 +950,7 @@ impl GlutinWindowContext { ))); let (window, gl_config) = { - crate::profile_scope!("DisplayBuilder::build"); + profiling::scope!("DisplayBuilder::build"); display_builder .build( @@ -995,7 +993,7 @@ impl GlutinWindowContext { .build(glutin_raw_window_handle); let gl_context_result = unsafe { - crate::profile_scope!("create_context"); + profiling::scope!("create_context"); gl_config .display() .create_context(&gl_config, &context_attributes) @@ -1070,7 +1068,7 @@ impl GlutinWindowContext { /// /// Errors will be logged. fn initialize_all_windows(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!(); + profiling::function_scope!(); let viewports: Vec = self.viewports.keys().copied().collect(); @@ -1088,7 +1086,7 @@ impl GlutinWindowContext { viewport_id: ViewportId, event_loop: &ActiveEventLoop, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let viewport = self .viewports @@ -1268,7 +1266,7 @@ impl GlutinWindowContext { egui_ctx: &egui::Context, viewport_output: &ViewportIdMap, ) { - crate::profile_function!(); + profiling::function_scope!(); for ( viewport_id, @@ -1329,7 +1327,7 @@ fn initialize_or_update_viewport( mut builder: ViewportBuilder, viewport_ui_cb: Option>, ) -> &mut Viewport { - crate::profile_function!(); + profiling::function_scope!(); if builder.icon.is_none() { // Inherit icon from parent @@ -1393,7 +1391,7 @@ fn render_immediate_viewport( beginning: Instant, immediate_viewport: ImmediateViewport<'_>, ) { - crate::profile_function!(); + profiling::function_scope!(); let ImmediateViewport { ids, @@ -1516,7 +1514,7 @@ fn render_immediate_viewport( ); { - crate::profile_scope!("swap_buffers"); + profiling::scope!("swap_buffers"); if let Err(err) = gl_surface.swap_buffers(current_gl_context) { log::error!("swap_buffers failed: {err}"); } diff --git a/crates/eframe/src/native/run.rs b/crates/eframe/src/native/run.rs index 219edd56625..e328877a4be 100644 --- a/crates/eframe/src/native/run.rs +++ b/crates/eframe/src/native/run.rs @@ -20,7 +20,7 @@ fn create_event_loop(native_options: &mut epi::NativeOptions) -> Result Result WinitAppWrapper { impl ApplicationHandler for WinitAppWrapper { fn suspended(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!("Event::Suspended"); + profiling::scope!("Event::Suspended"); event_loop_context::with_event_loop_context(event_loop, move || { let event_result = self.winit_app.suspended(event_loop); @@ -195,7 +195,7 @@ impl ApplicationHandler for WinitAppWrapper { } fn resumed(&mut self, event_loop: &ActiveEventLoop) { - crate::profile_function!("Event::Resumed"); + profiling::scope!("Event::Resumed"); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { @@ -219,7 +219,7 @@ impl ApplicationHandler for WinitAppWrapper { device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { - crate::profile_function!(egui_winit::short_device_event_description(&event)); + profiling::function_scope!(egui_winit::short_device_event_description(&event)); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { @@ -229,7 +229,7 @@ impl ApplicationHandler for WinitAppWrapper { } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { - crate::profile_function!(match &event { + profiling::function_scope!(match &event { UserEvent::RequestRepaint { .. } => "UserEvent::RequestRepaint", #[cfg(feature = "accesskit")] UserEvent::AccessKitActionRequest(_) => "UserEvent::AccessKitActionRequest", @@ -285,7 +285,7 @@ impl ApplicationHandler for WinitAppWrapper { window_id: WindowId, event: winit::event::WindowEvent, ) { - crate::profile_function!(egui_winit::short_window_event_description(&event)); + profiling::function_scope!(egui_winit::short_window_event_description(&event)); // Nb: Make sure this guard is dropped after this function returns. event_loop_context::with_event_loop_context(event_loop, move || { diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index acbd8c2f830..93643c2230e 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -102,7 +102,7 @@ impl<'app> WgpuWinitApp<'app> { native_options: NativeOptions, app_creator: AppCreator<'app>, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "__screenshot")] assert!( @@ -181,8 +181,7 @@ impl<'app> WgpuWinitApp<'app> { window: Window, builder: ViewportBuilder, ) -> crate::Result<&mut WgpuWinitRunning<'app>> { - crate::profile_function!(); - + profiling::function_scope!(); #[allow(unsafe_code, unused_mut, unused_unsafe)] let mut painter = egui_wgpu::winit::Painter::new( egui_ctx.clone(), @@ -199,7 +198,7 @@ impl<'app> WgpuWinitApp<'app> { let window = Arc::new(window); { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone())))?; } @@ -268,7 +267,7 @@ impl<'app> WgpuWinitApp<'app> { raw_window_handle: window.window_handle().map(|h| h.as_raw()), }; let app = { - crate::profile_scope!("user_app_creator"); + profiling::scope!("user_app_creator"); app_creator(&cc).map_err(crate::Error::AppCreation)? }; @@ -490,7 +489,7 @@ impl<'app> WinitApp for WgpuWinitApp<'app> { impl<'app> WgpuWinitRunning<'app> { fn save_and_destroy(&mut self) { - crate::profile_function!(); + profiling::function_scope!(); let mut shared = self.shared.borrow_mut(); if let Some(Viewport { window, .. }) = shared.viewports.get(&ViewportId::ROOT) { @@ -508,7 +507,7 @@ impl<'app> WgpuWinitRunning<'app> { /// This is called both for the root viewport, and all deferred viewports fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let Some(viewport_id) = self .shared @@ -520,8 +519,7 @@ impl<'app> WgpuWinitRunning<'app> { return Ok(EventResult::Wait); }; - #[cfg(feature = "puffin")] - puffin::GlobalProfiler::lock().new_frame(); + profiling::finish_frame!(); let Self { app, @@ -533,7 +531,7 @@ impl<'app> WgpuWinitRunning<'app> { frame_timer.start(); let (viewport_ui_cb, raw_input) = { - crate::profile_scope!("Prepare"); + profiling::scope!("Prepare"); let mut shared_lock = shared.borrow_mut(); let SharedState { @@ -577,7 +575,7 @@ impl<'app> WgpuWinitRunning<'app> { egui_winit::update_viewport_info(info, &integration.egui_ctx, window, false); { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); pollster::block_on(painter.set_window(viewport_id, Some(window.clone())))?; } @@ -719,7 +717,7 @@ impl<'app> WgpuWinitRunning<'app> { if window.is_minimized() == Some(true) { // On Mac, a minimized Window uses up all CPU: // https://github.com/emilk/egui/issues/325 - crate::profile_scope!("minimized_sleep"); + profiling::scope!("minimized_sleep"); std::thread::sleep(std::time::Duration::from_millis(10)); } } @@ -846,7 +844,7 @@ impl Viewport { return; // we already have one } - crate::profile_function!(); + profiling::function_scope!(); let viewport_id = self.ids.this; @@ -887,7 +885,7 @@ fn create_window( storage: Option<&dyn Storage>, native_options: &mut NativeOptions, ) -> Result<(Window, ViewportBuilder), winit::error::OsError> { - crate::profile_function!(); + profiling::function_scope!(); let window_settings = epi_integration::load_window_settings(storage); let viewport_builder = epi_integration::viewport_builder( @@ -908,7 +906,7 @@ fn render_immediate_viewport( shared: &RefCell, immediate_viewport: ImmediateViewport<'_>, ) { - crate::profile_function!(); + profiling::function_scope!(); let ImmediateViewport { ids, @@ -988,7 +986,7 @@ fn render_immediate_viewport( }; { - crate::profile_scope!("set_window"); + profiling::scope!("set_window"); if let Err(err) = pollster::block_on(painter.set_window(ids.this, Some(window.clone()))) { log::error!( "when rendering viewport_id={:?}, set_window Error {err}", @@ -1096,7 +1094,7 @@ fn initialize_or_update_viewport<'a>( viewport_ui_cb: Option>, painter: &mut egui_wgpu::winit::Painter, ) -> &'a mut Viewport { - crate::profile_function!(); + profiling::function_scope!(); if builder.icon.is_none() { // Inherit icon from parent diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index e9d214103b8..2b6c54a67f9 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -11,7 +11,7 @@ use egui_winit::accesskit_winit; /// Create an egui context, restoring it from storage if possible. pub fn create_egui_context(storage: Option<&dyn crate::Storage>) -> egui::Context { - crate::profile_function!(); + profiling::function_scope!(); pub const IS_DESKTOP: bool = cfg!(any( target_os = "freebsd", diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 4575ce0069b..2f4330236d0 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -33,9 +33,6 @@ rustdoc-args = ["--generate-link-to-definition"] [features] default = ["fragile-send-sync-non-atomic-wasm"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin"] - ## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` winit = ["dep:winit", "winit/rwh_06"] @@ -60,6 +57,7 @@ ahash.workspace = true bytemuck.workspace = true document-features.workspace = true log.workspace = true +profiling.workspace = true thiserror.workspace = true type-map.workspace = true web-time.workspace = true @@ -68,7 +66,3 @@ wgpu = { workspace = true, features = ["wgsl"] } # Optional dependencies: winit = { workspace = true, optional = true, default-features = false } - -# Native: -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -puffin = { workspace = true, optional = true } diff --git a/crates/egui-wgpu/src/lib.rs b/crates/egui-wgpu/src/lib.rs index 54c76f05461..a91662f45b1 100644 --- a/crates/egui-wgpu/src/lib.rs +++ b/crates/egui-wgpu/src/lib.rs @@ -96,7 +96,7 @@ impl RenderState { msaa_samples: u32, dithering: bool, ) -> Result { - crate::profile_scope!("RenderState::create"); // async yield give bad names using `profile_function` + profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function` // This is always an empty list on web. #[cfg(not(target_arch = "wasm32"))] @@ -109,7 +109,7 @@ impl RenderState { device_descriptor, } => { let adapter = { - crate::profile_scope!("request_adapter"); + profiling::scope!("request_adapter"); instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference, @@ -164,7 +164,7 @@ impl RenderState { let trace_path = std::env::var("WGPU_TRACE"); let (device, queue) = { - crate::profile_scope!("request_device"); + profiling::scope!("request_device"); adapter .request_device( &(*device_descriptor)(&adapter), @@ -187,7 +187,7 @@ impl RenderState { }; let capabilities = { - crate::profile_scope!("get_capabilities"); + profiling::scope!("get_capabilities"); surface.get_capabilities(&adapter).formats }; let target_format = crate::preferred_framebuffer_format(&capabilities)?; @@ -474,33 +474,3 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String { summary } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index b6d49d22d4f..2c1fa0428f1 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -214,14 +214,14 @@ impl Renderer { msaa_samples: u32, dithering: bool, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let shader = wgpu::ShaderModuleDescriptor { label: Some("egui"), source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("egui.wgsl"))), }; let module = { - crate::profile_scope!("create_shader_module"); + profiling::scope!("create_shader_module"); device.create_shader_module(shader) }; @@ -236,7 +236,7 @@ impl Renderer { }); let uniform_bind_group_layout = { - crate::profile_scope!("create_bind_group_layout"); + profiling::scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_uniform_bind_group_layout"), entries: &[wgpu::BindGroupLayoutEntry { @@ -253,7 +253,7 @@ impl Renderer { }; let uniform_bind_group = { - crate::profile_scope!("create_bind_group"); + profiling::scope!("create_bind_group"); device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("egui_uniform_bind_group"), layout: &uniform_bind_group_layout, @@ -269,7 +269,7 @@ impl Renderer { }; let texture_bind_group_layout = { - crate::profile_scope!("create_bind_group_layout"); + profiling::scope!("create_bind_group_layout"); device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("egui_texture_bind_group_layout"), entries: &[ @@ -308,7 +308,7 @@ impl Renderer { }); let pipeline = { - crate::profile_scope!("create_render_pipeline"); + profiling::scope!("create_render_pipeline"); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("egui_pipeline"), layout: Some(&pipeline_layout), @@ -420,7 +420,7 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = screen_descriptor.pixels_per_point; let size_in_pixels = screen_descriptor.size_in_pixels; @@ -506,7 +506,7 @@ impl Renderer { let viewport_px = info.viewport_in_pixels(); if viewport_px.width_px > 0 && viewport_px.height_px > 0 { - crate::profile_scope!("callback"); + profiling::scope!("callback"); needs_reset = true; @@ -544,7 +544,7 @@ impl Renderer { id: epaint::TextureId, image_delta: &epaint::ImageDelta, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = image_delta.image.width() as u32; let height = image_delta.image.height() as u32; @@ -570,14 +570,14 @@ impl Renderer { image.pixels.len(), "Mismatch between texture size and texel count" ); - crate::profile_scope!("font -> sRGBA"); + profiling::scope!("font -> sRGBA"); Cow::Owned(image.srgba_pixels(None).collect::>()) } }; let data_bytes: &[u8] = bytemuck::cast_slice(data_color32.as_slice()); let queue_write_data_to_texture = |texture, origin| { - crate::profile_scope!("write_texture"); + profiling::scope!("write_texture"); queue.write_texture( wgpu::ImageCopyTexture { texture, @@ -631,7 +631,7 @@ impl Renderer { } else { // allocate a new texture let texture = { - crate::profile_scope!("create_texture"); + profiling::scope!("create_texture"); device.create_texture(&wgpu::TextureDescriptor { label, size, @@ -756,7 +756,7 @@ impl Renderer { texture: &wgpu::TextureView, sampler_descriptor: wgpu::SamplerDescriptor<'_>, ) -> epaint::TextureId { - crate::profile_function!(); + profiling::function_scope!(); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { compare: None, @@ -804,7 +804,7 @@ impl Renderer { sampler_descriptor: wgpu::SamplerDescriptor<'_>, id: epaint::TextureId, ) { - crate::profile_function!(); + profiling::function_scope!(); let Texture { bind_group: user_texture_binding, @@ -849,7 +849,7 @@ impl Renderer { paint_jobs: &[epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); let screen_size_in_points = screen_descriptor.screen_size_in_points(); @@ -859,7 +859,7 @@ impl Renderer { _padding: Default::default(), }; if uniform_buffer_content != self.previous_uniform_buffer_content { - crate::profile_scope!("update uniforms"); + profiling::scope!("update uniforms"); queue.write_buffer( &self.uniform_buffer, 0, @@ -871,7 +871,7 @@ impl Renderer { // Determine how many vertices & indices need to be rendered, and gather prepare callbacks let mut callbacks = Vec::new(); let (vertex_count, index_count) = { - crate::profile_scope!("count_vertices_indices"); + profiling::scope!("count_vertices_indices"); paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| { match &clipped_primitive.primitive { Primitive::Mesh(mesh) => { @@ -890,7 +890,7 @@ impl Renderer { }; if index_count > 0 { - crate::profile_scope!("indices", index_count.to_string()); + profiling::scope!("indices", index_count.to_string().as_str()); self.index_buffer.slices.clear(); @@ -928,7 +928,7 @@ impl Renderer { } } if vertex_count > 0 { - crate::profile_scope!("vertices", vertex_count.to_string()); + profiling::scope!("vertices", vertex_count.to_string().as_str()); self.vertex_buffer.slices.clear(); @@ -969,7 +969,7 @@ impl Renderer { let mut user_cmd_bufs = Vec::new(); { - crate::profile_scope!("prepare callbacks"); + profiling::scope!("prepare callbacks"); for callback in &callbacks { user_cmd_bufs.extend(callback.prepare( device, @@ -981,7 +981,7 @@ impl Renderer { } } { - crate::profile_scope!("finish prepare callbacks"); + profiling::scope!("finish prepare callbacks"); for callback in &callbacks { user_cmd_bufs.extend(callback.finish_prepare( device, @@ -1026,7 +1026,7 @@ fn create_sampler( } fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { - crate::profile_function!(); + profiling::function_scope!(); device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_vertex_buffer"), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, @@ -1036,7 +1036,7 @@ fn create_vertex_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { } fn create_index_buffer(device: &wgpu::Device, size: u64) -> wgpu::Buffer { - crate::profile_function!(); + profiling::function_scope!(); device.create_buffer(&wgpu::BufferDescriptor { label: Some("egui_index_buffer"), usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, diff --git a/crates/egui-wgpu/src/winit.rs b/crates/egui-wgpu/src/winit.rs index cf7c041f002..dea2e7fa329 100644 --- a/crates/egui-wgpu/src/winit.rs +++ b/crates/egui-wgpu/src/winit.rs @@ -104,7 +104,7 @@ impl Painter { render_state: &RenderState, config: &WgpuConfiguration, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = surface_state.width; let height = surface_state.height; @@ -156,7 +156,7 @@ impl Painter { viewport_id: ViewportId, window: Option>, ) -> Result<(), crate::WgpuError> { - crate::profile_scope!("Painter::set_window"); // profile_function gives bad names for async functions + profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions if let Some(window) = window { let size = window.inner_size(); @@ -182,7 +182,7 @@ impl Painter { viewport_id: ViewportId, window: Option<&winit::window::Window>, ) -> Result<(), crate::WgpuError> { - crate::profile_scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions + profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions if let Some(window) = window { let size = window.inner_size(); @@ -273,7 +273,7 @@ impl Painter { width_in_pixels: NonZeroU32, height_in_pixels: NonZeroU32, ) { - crate::profile_function!(); + profiling::function_scope!(); let width = width_in_pixels.get(); let height = height_in_pixels.get(); @@ -344,7 +344,7 @@ impl Painter { width_in_pixels: NonZeroU32, height_in_pixels: NonZeroU32, ) { - crate::profile_function!(); + profiling::function_scope!(); if self.surfaces.contains_key(&viewport_id) { self.resize_and_generate_depth_texture_view_and_msaa_view( @@ -372,7 +372,7 @@ impl Painter { textures_delta: &epaint::textures::TexturesDelta, capture_data: Vec, ) -> f32 { - crate::profile_function!(); + profiling::function_scope!(); let capture = !capture_data.is_empty(); let mut vsync_sec = 0.0; @@ -418,7 +418,7 @@ impl Painter { }; let output_frame = { - crate::profile_scope!("get_current_texture"); + profiling::scope!("get_current_texture"); // This is what vsync-waiting happens on my Mac. let start = web_time::Instant::now(); let output_frame = surface_state.surface.get_current_texture(); @@ -514,13 +514,13 @@ impl Painter { } let encoded = { - crate::profile_scope!("CommandEncoder::finish"); + profiling::scope!("CommandEncoder::finish"); encoder.finish() }; // Submit the commands: both the main buffer and user-defined ones. { - crate::profile_scope!("Queue::submit"); + profiling::scope!("Queue::submit"); // wgpu doesn't document where vsync can happen. Maybe here? let start = web_time::Instant::now(); render_state @@ -552,7 +552,7 @@ impl Painter { } { - crate::profile_scope!("present"); + profiling::scope!("present"); // wgpu doesn't document where vsync can happen. Maybe here? let start = web_time::Instant::now(); output_frame.present(); diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index 2f65548757d..c584db85e70 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -45,9 +45,6 @@ clipboard = ["arboard", "smithay-clipboard"] ## Enable opening links in a browser when an egui hyperlink is clicked. links = ["webbrowser"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin", "egui/puffin"] - ## Allow serialization of [`WindowSettings`] using [`serde`](https://docs.rs/serde). serde = ["egui/serde", "dep:serde"] @@ -62,6 +59,7 @@ egui = { workspace = true, default-features = false, features = ["log"] } ahash.workspace = true log.workspace = true +profiling.workspace = true raw-window-handle.workspace = true web-time.workspace = true winit = { workspace = true, default-features = false } @@ -74,7 +72,6 @@ accesskit_winit = { version = "0.23", optional = true } ## Enable this when generating docs. document-features = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } serde = { workspace = true, optional = true } webbrowser = { version = "1.0.0", optional = true } diff --git a/crates/egui-winit/src/clipboard.rs b/crates/egui-winit/src/clipboard.rs index 44e3840b64f..c4192f78d55 100644 --- a/crates/egui-winit/src/clipboard.rs +++ b/crates/egui-winit/src/clipboard.rs @@ -112,7 +112,7 @@ impl Clipboard { #[cfg(all(feature = "arboard", not(target_os = "android")))] fn init_arboard() -> Option { - crate::profile_function!(); + profiling::function_scope!(); log::trace!("Initializing arboard clipboard…"); match arboard::Clipboard::new() { @@ -139,7 +139,7 @@ fn init_smithay_clipboard( ) -> Option { #![allow(clippy::undocumented_unsafe_blocks)] - crate::profile_function!(); + profiling::function_scope!(); if let Some(RawDisplayHandle::Wayland(display)) = raw_display_handle { log::trace!("Initializing smithay clipboard…"); diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 1cb2d502c5d..50ff2d31b4b 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -25,9 +25,6 @@ pub use window_settings::WindowSettings; use ahash::HashSet; use raw_window_handle::HasDisplayHandle; -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; - use winit::{ dpi::{PhysicalPosition, PhysicalSize}, event::ElementState, @@ -121,7 +118,7 @@ impl State { theme: Option, max_texture_side: Option, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let egui_input = egui::RawInput { focused: false, // winit will tell us when we have focus @@ -172,7 +169,7 @@ impl State { window: &Window, event_loop_proxy: winit::event_loop::EventLoopProxy, ) { - crate::profile_function!(); + profiling::function_scope!(); self.accesskit = Some(accesskit_winit::Adapter::with_event_loop_proxy( window, @@ -233,7 +230,7 @@ impl State { /// Use [`update_viewport_info`] to update the info for each /// viewport. pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput { - crate::profile_function!(); + profiling::function_scope!(); self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); @@ -268,7 +265,7 @@ impl State { window: &Window, event: &winit::event::WindowEvent, ) -> EventResponse { - crate::profile_function!(short_window_event_description(event)); + profiling::function_scope!(short_window_event_description(event)); #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_mut() { @@ -823,7 +820,7 @@ impl State { window: &Window, platform_output: egui::PlatformOutput, ) { - crate::profile_function!(); + profiling::function_scope!(); let egui::PlatformOutput { cursor_icon, @@ -851,7 +848,7 @@ impl State { let allow_ime = ime.is_some(); if self.allow_ime != allow_ime { self.allow_ime = allow_ime; - crate::profile_scope!("set_ime_allowed"); + profiling::scope!("set_ime_allowed"); window.set_ime_allowed(allow_ime); } @@ -862,7 +859,7 @@ impl State { || self.egui_ctx.input(|i| !i.events.is_empty()) { self.ime_rect_px = Some(ime_rect_px); - crate::profile_scope!("set_ime_cursor_area"); + profiling::scope!("set_ime_cursor_area"); window.set_ime_cursor_area( winit::dpi::PhysicalPosition { x: ime_rect_px.min.x, @@ -881,7 +878,7 @@ impl State { #[cfg(feature = "accesskit")] if let Some(accesskit) = self.accesskit.as_mut() { if let Some(update) = accesskit_update { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); accesskit.update_if_active(|| update); } } @@ -953,8 +950,7 @@ pub fn update_viewport_info( window: &Window, is_init: bool, ) { - crate::profile_function!(); - + profiling::function_scope!(); let pixels_per_point = pixels_per_point(egui_ctx, window); let has_a_position = match window.is_minimized() { @@ -975,7 +971,7 @@ pub fn update_viewport_info( }; let monitor_size = { - crate::profile_scope!("monitor_size"); + profiling::scope!("monitor_size"); if let Some(monitor) = window.current_monitor() { let size = monitor.size().to_logical::(pixels_per_point.into()); Some(egui::vec2(size.width, size.height)) @@ -1326,7 +1322,7 @@ fn process_viewport_command( info: &mut ViewportInfo, actions_requested: &mut HashSet, ) { - crate::profile_function!(); + profiling::function_scope!(); use winit::window::ResizeDirection; @@ -1542,7 +1538,7 @@ pub fn create_window( event_loop: &ActiveEventLoop, viewport_builder: &ViewportBuilder, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let window_attributes = create_winit_window_attributes(egui_ctx, event_loop, viewport_builder.clone()); @@ -1556,7 +1552,7 @@ pub fn create_winit_window_attributes( event_loop: &ActiveEventLoop, viewport_builder: ViewportBuilder, ) -> winit::window::WindowAttributes { - crate::profile_function!(); + profiling::function_scope!(); // We set sizes and positions in egui:s own ui points, which depends on the egui // zoom_factor and the native pixels per point, so we need to know that here. @@ -1752,7 +1748,7 @@ fn to_winit_icon(icon: &egui::IconData) -> Option { if icon.is_empty() { None } else { - crate::profile_function!(); + profiling::function_scope!(); match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) { Ok(winit_icon) => Some(winit_icon), Err(err) => { @@ -1867,30 +1863,3 @@ pub fn short_window_event_description(event: &winit::event::WindowEvent) -> &'st WindowEvent::PanGesture { .. } => "WindowEvent::PanGesture", } } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index 627d88158c0..168a086c70f 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -56,7 +56,7 @@ impl WindowSettings { event_loop: &winit::event_loop::ActiveEventLoop, mut viewport_builder: ViewportBuilder, ) -> ViewportBuilder { - crate::profile_function!(); + profiling::function_scope!(); // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere // See [`winit::window::WindowBuilder::with_position`] for details. @@ -143,8 +143,7 @@ fn find_active_monitor( window_size_pts: egui::Vec2, position_px: &egui::Pos2, ) -> Option { - crate::profile_function!(); - + profiling::function_scope!(); let monitors = event_loop.available_monitors(); // default to primary monitor, in case the correct monitor was disconnected. @@ -178,7 +177,7 @@ fn clamp_pos_to_monitors( window_size_pts: egui::Vec2, position_px: &mut egui::Pos2, ) { - crate::profile_function!(); + profiling::function_scope!(); let Some(active_monitor) = find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px) diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index 9bb1dc0f4e4..a9408da2f6d 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -62,10 +62,6 @@ mint = ["epaint/mint"] ## Enable persistence of memory (window positions etc). persistence = ["serde", "epaint/serde", "ron"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin", "epaint/puffin"] ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## @@ -85,6 +81,7 @@ epaint = { workspace = true, default-features = false } ahash.workspace = true nohash-hasher.workspace = true +profiling.workspace = true #! ### Optional dependencies accesskit = { version = "0.17.0", optional = true } @@ -95,7 +92,6 @@ backtrace = { workspace = true, optional = true } document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9286f38025f..8ab83c21935 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -109,13 +109,13 @@ struct Plugins { impl Plugins { fn call(ctx: &Context, _cb_name: &str, callbacks: &[NamedContextCallback]) { - crate::profile_scope!("plugins", _cb_name); + profiling::scope!("plugins", _cb_name); for NamedContextCallback { debug_name: _name, callback, } in callbacks { - crate::profile_scope!("plugin", _name); + profiling::scope!("plugin", _name); (callback)(ctx); } } @@ -549,7 +549,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] if self.is_accesskit_enabled { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); use crate::pass_state::AccessKitPassState; let id = crate::accesskit_root_id(); let mut root_node = accesskit::Node::new(accesskit::Role::Window); @@ -568,8 +568,7 @@ impl ContextImpl { /// Load fonts unless already loaded. fn update_fonts_mut(&mut self) { - crate::profile_function!(); - + profiling::function_scope!(); let input = &self.viewport().input; let pixels_per_point = input.pixels_per_point(); let max_texture_side = input.max_texture_side; @@ -616,7 +615,7 @@ impl ContextImpl { log::trace!("Creating new Fonts for pixels_per_point={pixels_per_point}"); is_new = true; - crate::profile_scope!("Fonts::new"); + profiling::scope!("Fonts::new"); Fonts::new( pixels_per_point, max_texture_side, @@ -625,12 +624,12 @@ impl ContextImpl { }); { - crate::profile_scope!("Fonts::begin_pass"); + profiling::scope!("Fonts::begin_pass"); fonts.begin_pass(pixels_per_point, max_texture_side); } if is_new && self.memory.options.preload_font_glyphs { - crate::profile_scope!("preload_font_glyphs"); + profiling::scope!("preload_font_glyphs"); // Preload the most common characters for the most common fonts. // This is not very important to do, but may save a few GPU operations. for font_id in self.memory.options.style().text_styles.values() { @@ -812,8 +811,7 @@ impl Context { /// ``` #[must_use] pub fn run(&self, mut new_input: RawInput, mut run_ui: impl FnMut(&Self)) -> FullOutput { - crate::profile_function!(); - + profiling::function_scope!(); let viewport_id = new_input.viewport_id; let max_passes = self.write(|ctx| ctx.memory.options.max_passes.get()); @@ -821,9 +819,13 @@ impl Context { debug_assert_eq!(output.platform_output.num_completed_passes, 0); loop { - crate::profile_scope!( + profiling::scope!( "pass", - output.platform_output.num_completed_passes.to_string() + output + .platform_output + .num_completed_passes + .to_string() + .as_str() ); // We must move the `num_passes` (back) to the viewport output so that [`Self::will_discard`] @@ -886,7 +888,7 @@ impl Context { /// // handle full_output /// ``` pub fn begin_pass(&self, new_input: RawInput) { - crate::profile_function!(); + profiling::function_scope!(); self.write(|ctx| ctx.begin_pass(new_input)); @@ -1760,7 +1762,7 @@ impl Context { /// The new fonts will become active at the start of the next pass. /// This will overwrite the existing fonts. pub fn set_fonts(&self, font_definitions: FontDefinitions) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = self.pixels_per_point(); @@ -1788,7 +1790,7 @@ impl Context { /// The new font will become active at the start of the next pass. /// This will keep the existing fonts. pub fn add_font(&self, new_font: FontInsert) { - crate::profile_function!(); + profiling::function_scope!(); let pixels_per_point = self.pixels_per_point(); @@ -2152,7 +2154,7 @@ impl Context { /// Call at the end of each frame if you called [`Context::begin_pass`]. #[must_use] pub fn end_pass(&self) -> FullOutput { - crate::profile_function!(); + profiling::function_scope!(); if self.options(|o| o.zoom_with_keyboard) { crate::gui_zoom::zoom_with_keyboard(self); @@ -2342,7 +2344,7 @@ impl ContextImpl { // https://github.com/emilk/egui/issues/3664 // at the cost of a lot of performance. // (This will override any smaller delta that was uploaded above.) - crate::profile_scope!("full_font_atlas_update"); + profiling::scope!("full_font_atlas_update"); let full_delta = ImageDelta::full(fonts.image(), TextureAtlas::texture_options()); tex_mngr.set(TextureId::default(), full_delta); } @@ -2356,7 +2358,7 @@ impl ContextImpl { #[cfg(feature = "accesskit")] { - crate::profile_scope!("accesskit"); + profiling::scope!("accesskit"); let state = viewport.this_pass.accesskit_state.take(); if let Some(state) = state { let root_id = crate::accesskit_root_id().accesskit_id(); @@ -2386,7 +2388,7 @@ impl ContextImpl { let mut repaint_needed = false; if self.memory.options.repaint_on_widget_change { - crate::profile_function!("compare-widget-rects"); + profiling::scope!("compare-widget-rects"); if viewport.prev_pass.widgets != viewport.this_pass.widgets { repaint_needed = true; // Some widget has moved } @@ -2525,7 +2527,7 @@ impl Context { shapes: Vec, pixels_per_point: f32, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); // A tempting optimization is to reuse the tessellation from last frame if the // shapes are the same, but just comparing the shapes takes about 50% of the time @@ -2552,7 +2554,7 @@ impl Context { let paint_stats = PaintStats::from_shapes(&shapes); let clipped_primitives = { - crate::profile_scope!("tessellator::tessellate_shapes"); + profiling::scope!("tessellator::tessellate_shapes"); tessellator::Tessellator::new( pixels_per_point, tessellation_options, @@ -3368,7 +3370,7 @@ impl Context { pub fn forget_image(&self, uri: &str) { use load::BytesLoader as _; - crate::profile_function!(); + profiling::function_scope!(); let loaders = self.loaders(); @@ -3390,7 +3392,7 @@ impl Context { pub fn forget_all_images(&self) { use load::BytesLoader as _; - crate::profile_function!(); + profiling::function_scope!(); let loaders = self.loaders(); @@ -3425,7 +3427,7 @@ impl Context { /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Loading pub fn try_load_bytes(&self, uri: &str) -> load::BytesLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let bytes_loaders = loaders.bytes.lock(); @@ -3462,7 +3464,7 @@ impl Context { /// [not_supported]: crate::load::LoadError::NotSupported /// [custom]: crate::load::LoadError::Loading pub fn try_load_image(&self, uri: &str, size_hint: load::SizeHint) -> load::ImageLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let image_loaders = loaders.image.lock(); @@ -3513,7 +3515,7 @@ impl Context { texture_options: TextureOptions, size_hint: load::SizeHint, ) -> load::TextureLoadResult { - crate::profile_function!(uri); + profiling::function_scope!(uri); let loaders = self.loaders(); let texture_loaders = loaders.texture.lock(); @@ -3531,7 +3533,7 @@ impl Context { /// The loaders of bytes, images, and textures. pub fn loaders(&self) -> Arc { - crate::profile_function!(); + profiling::function_scope!(); self.read(|this| this.loaders.clone()) } } @@ -3663,7 +3665,7 @@ impl Context { viewport_builder: ViewportBuilder, viewport_ui_cb: impl Fn(&Self, ViewportClass) + Send + Sync + 'static, ) { - crate::profile_function!(); + profiling::function_scope!(); if self.embed_viewports() { viewport_ui_cb(self, ViewportClass::Embedded); @@ -3715,7 +3717,7 @@ impl Context { builder: ViewportBuilder, mut viewport_ui_cb: impl FnMut(&Self, ViewportClass) -> T, ) -> T { - crate::profile_function!(); + profiling::function_scope!(); if self.embed_viewports() { return viewport_ui_cb(self, ViewportClass::Embedded); diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index fe0ce75f9c9..0d85621220e 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -39,7 +39,7 @@ pub fn hit_test( pos: Pos2, search_radius: f32, ) -> WidgetHits { - crate::profile_function!(); + profiling::function_scope!(); let search_radius_sq = search_radius * search_radius; diff --git a/crates/egui/src/input_state/mod.rs b/crates/egui/src/input_state/mod.rs index 99c166cf443..048e880e3d4 100644 --- a/crates/egui/src/input_state/mod.rs +++ b/crates/egui/src/input_state/mod.rs @@ -269,7 +269,7 @@ impl InputState { pixels_per_point: f32, options: &crate::Options, ) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let time = new.time.unwrap_or(self.time + new.predicted_dt as f64); let unstable_dt = (time - self.time) as f32; diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 7cbadbf1193..afac1602836 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -113,7 +113,7 @@ pub(crate) fn interact( input: &InputState, interaction: &mut InteractionState, ) -> InteractionSnapshot { - crate::profile_function!(); + profiling::function_scope!(); if let Some(id) = interaction.potential_click_id { if !widgets.contains(id) { diff --git a/crates/egui/src/layers.rs b/crates/egui/src/layers.rs index 1133f13a97b..81a60812ece 100644 --- a/crates/egui/src/layers.rs +++ b/crates/egui/src/layers.rs @@ -222,7 +222,7 @@ impl GraphicLayers { area_order: &[LayerId], to_global: &ahash::HashMap, ) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); let mut all_shapes: Vec<_> = Default::default(); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 8c70ca5a812..1afaada95e1 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -388,6 +388,18 @@ //! ## Installing additional fonts //! The default egui fonts only support latin and cryllic characters, and some emojis. //! To use egui with e.g. asian characters you need to install your own font (`.ttf` or `.otf`) using [`Context::set_fonts`]. +//! +//! ## Instrumentation +//! This crate supports using the [profiling](https://crates.io/crates/profiling) crate for instrumentation. +//! You can enable features on the profiling crates in your application to add instrumentation for all +//! crates that support it, including egui. See the profiling crate docs for more information. +//! ```toml +//! [dependencies] +//! profiling = "1.0" +//! [features] +//! profile-with-puffin = ["profiling/profile-with-puffin"] +//! ``` +//! #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] @@ -691,33 +703,3 @@ pub fn __run_test_ui(add_contents: impl Fn(&mut Ui)) { pub fn accesskit_root_id() -> Id { Id::new("accesskit_root") } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 0a44df8cc57..2d1449c1694 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -779,7 +779,7 @@ impl Focus { impl Memory { pub(crate) fn begin_pass(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) { - crate::profile_function!(); + profiling::function_scope!(); self.viewport_id = new_raw_input.viewport_id; diff --git a/crates/egui/src/pass_state.rs b/crates/egui/src/pass_state.rs index da42e0932ae..5501220f769 100644 --- a/crates/egui/src/pass_state.rs +++ b/crates/egui/src/pass_state.rs @@ -248,7 +248,7 @@ impl Default for PassState { impl PassState { pub(crate) fn begin_pass(&mut self, screen_rect: Rect) { - crate::profile_function!(); + profiling::function_scope!(); let Self { used_ids, widgets, diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index 7e812ae5572..6e382f9917f 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -574,7 +574,7 @@ struct PersistedMap(Vec<(u64, SerializedElement)>); #[cfg(feature = "persistence")] impl PersistedMap { fn from_map(map: &IdTypeMap) -> Self { - crate::profile_function!(); + profiling::function_scope!(); use std::collections::BTreeMap; @@ -593,7 +593,7 @@ impl PersistedMap { let max_bytes_per_type = map.max_bytes_per_type; { - crate::profile_scope!("gather"); + profiling::scope!("gather"); for (hash, element) in &map.map { if let Some(element) = element.to_serialize() { let stats = types_map.entry(element.type_id).or_default(); @@ -610,7 +610,7 @@ impl PersistedMap { let mut persisted = vec![]; { - crate::profile_scope!("gc"); + profiling::scope!("gc"); for stats in types_map.values() { let mut bytes_written = 0; @@ -634,7 +634,7 @@ impl PersistedMap { } fn into_map(self) -> IdTypeMap { - crate::profile_function!(); + profiling::function_scope!(); let map = self .0 .into_iter() @@ -671,7 +671,7 @@ impl serde::Serialize for IdTypeMap { where S: serde::Serializer, { - crate::profile_scope!("IdTypeMap::serialize"); + profiling::scope!("IdTypeMap::serialize"); PersistedMap::from_map(self).serialize(serializer) } } @@ -682,7 +682,7 @@ impl<'de> serde::Deserialize<'de> for IdTypeMap { where D: serde::Deserializer<'de>, { - crate::profile_scope!("IdTypeMap::deserialize"); + profiling::scope!("IdTypeMap::deserialize"); ::deserialize(deserializer).map(PersistedMap::into_map) } } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index d8b26429c77..adafeca43a8 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -188,7 +188,7 @@ impl std::fmt::Debug for IconData { impl From for epaint::ColorImage { fn from(icon: IconData) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let IconData { rgba, width, @@ -200,7 +200,7 @@ impl From for epaint::ColorImage { impl From<&IconData> for epaint::ColorImage { fn from(icon: &IconData) -> Self { - crate::profile_function!(); + profiling::function_scope!(); let IconData { rgba, width, diff --git a/crates/egui_demo_app/Cargo.toml b/crates/egui_demo_app/Cargo.toml index a66b6a7587f..2a1661fcbde 100644 --- a/crates/egui_demo_app/Cargo.toml +++ b/crates/egui_demo_app/Cargo.toml @@ -8,6 +8,9 @@ rust-version.workspace = true publish = false default-run = "egui_demo_app" +[package.metadata.cargo-machete] +ignored = ["profiling"] + [lints] workspace = true @@ -33,7 +36,7 @@ persistence = [ "egui/persistence", "serde", ] -puffin = ["eframe/puffin", "dep:puffin", "dep:puffin_http"] +puffin = ["dep:puffin", "dep:puffin_http", "profiling/profile-with-puffin"] serde = ["dep:serde", "egui_demo_lib/serde", "egui/serde"] syntect = ["egui_demo_lib/syntect"] @@ -54,6 +57,7 @@ egui = { workspace = true, features = ["callstack", "default", "log"] } egui_demo_lib = { workspace = true, features = ["default", "chrono"] } egui_extras = { workspace = true, features = ["default", "image"] } log.workspace = true +profiling.workspace = true # Optional dependencies: diff --git a/crates/egui_demo_app/src/main.rs b/crates/egui_demo_app/src/main.rs index 9f42b422d04..6a2bb2da796 100644 --- a/crates/egui_demo_app/src/main.rs +++ b/crates/egui_demo_app/src/main.rs @@ -51,6 +51,7 @@ fn main() -> eframe::Result { ..Default::default() }; + eframe::run_native( "egui demo app", options, diff --git a/crates/egui_extras/Cargo.toml b/crates/egui_extras/Cargo.toml index b13a518e8d0..41fbcf0a462 100644 --- a/crates/egui_extras/Cargo.toml +++ b/crates/egui_extras/Cargo.toml @@ -53,11 +53,6 @@ http = ["dep:ehttp"] ## ``` image = ["dep:image"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin", "egui/puffin"] - ## Derive serde Serialize/Deserialize on stateful structs serde = ["egui/serde", "dep:serde"] @@ -74,6 +69,7 @@ egui = { workspace = true, default-features = false } ahash.workspace = true enum-map = { version = "2", features = ["serde"] } log.workspace = true +profiling.workspace = true #! ### Optional dependencies @@ -96,7 +92,6 @@ image = { workspace = true, optional = true } # file feature mime_guess2 = { version = "2", optional = true, default-features = false } -puffin = { workspace = true, optional = true } syntect = { version = "5", optional = true, default-features = false, features = [ "default-fancy", diff --git a/crates/egui_extras/src/image.rs b/crates/egui_extras/src/image.rs index 1d2f6afa480..46d9df170a0 100644 --- a/crates/egui_extras/src/image.rs +++ b/crates/egui_extras/src/image.rs @@ -199,7 +199,7 @@ impl RetainedImage { /// On invalid image or unsupported image format. #[cfg(feature = "image")] pub fn load_image_bytes(image_bytes: &[u8]) -> Result { - crate::profile_function!(); + profiling::function_scope!(); let image = image::load_from_memory(image_bytes).map_err(|err| match err { image::ImageError::Unsupported(err) => match err.kind() { image::error::UnsupportedErrorKind::Format(format) => { @@ -245,7 +245,8 @@ pub fn load_svg_bytes_with_size( use resvg::tiny_skia::{IntSize, Pixmap}; use resvg::usvg::{Options, Tree, TreeParsing}; - crate::profile_function!(); + profiling::function_scope!(); + let opt = Options::default(); let mut rtree = Tree::from_data(svg_bytes, &opt).map_err(|err| err.to_string())?; diff --git a/crates/egui_extras/src/lib.rs b/crates/egui_extras/src/lib.rs index ab2dde735b9..6f339010224 100644 --- a/crates/egui_extras/src/lib.rs +++ b/crates/egui_extras/src/lib.rs @@ -37,36 +37,6 @@ pub use loaders::install_image_loaders; // --------------------------------------------------------------------------- -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::profile_function; - -// --------------------------------------------------------------------------- - /// Panic in debug builds, log otherwise. macro_rules! log_or_panic { ($fmt: literal) => {$crate::log_or_panic!($fmt,)}; diff --git a/crates/egui_extras/src/syntax_highlighting.rs b/crates/egui_extras/src/syntax_highlighting.rs index 9275d345b5b..027ba5ee9da 100644 --- a/crates/egui_extras/src/syntax_highlighting.rs +++ b/crates/egui_extras/src/syntax_highlighting.rs @@ -403,7 +403,7 @@ struct Highlighter { #[cfg(feature = "syntect")] impl Default for Highlighter { fn default() -> Self { - crate::profile_function!(); + profiling::function_scope!(); Self { ps: syntect::parsing::SyntaxSet::load_defaults_newlines(), ts: syntect::highlighting::ThemeSet::load_defaults(), @@ -437,8 +437,7 @@ impl Highlighter { #[cfg(feature = "syntect")] fn highlight_impl(&self, theme: &CodeTheme, text: &str, language: &str) -> Option { - crate::profile_function!(); - + profiling::function_scope!(); use syntect::easy::HighlightLines; use syntect::highlighting::FontStyle; use syntect::util::LinesWithEndings; @@ -512,7 +511,7 @@ impl Highlighter { mut text: &str, language: &str, ) -> Option { - crate::profile_function!(); + profiling::function_scope!(); let language = Language::new(language)?; diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index 2bb184c833b..7a402d4067a 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -39,9 +39,6 @@ clipboard = ["egui-winit?/clipboard"] ## enable opening links in a browser when an egui hyperlink is clicked. links = ["egui-winit?/links"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -puffin = ["dep:puffin", "egui-winit?/puffin", "egui/puffin"] - ## Enable [`winit`](https://docs.rs/winit) integration. On Linux, requires either `wayland` or `x11` winit = ["egui-winit", "dep:winit"] @@ -61,14 +58,13 @@ bytemuck.workspace = true glow.workspace = true log.workspace = true memoffset = "0.9" +profiling.workspace = true #! ### Optional dependencies ## Enable this when generating docs. document-features = { workspace = true, optional = true } # Native: -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -puffin = { workspace = true, optional = true } winit = { workspace = true, optional = true, default-features = false, features = ["rwh_06"] } # Web: diff --git a/crates/egui_glow/src/lib.rs b/crates/egui_glow/src/lib.rs index 0e1a98102af..430f1287ec2 100644 --- a/crates/egui_glow/src/lib.rs +++ b/crates/egui_glow/src/lib.rs @@ -110,33 +110,3 @@ pub fn check_for_gl_error_impl(gl: &glow::Context, file: &str, line: u32, contex } } } - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/egui_glow/src/painter.rs b/crates/egui_glow/src/painter.rs index 86d86bedf7c..bec46cf085f 100644 --- a/crates/egui_glow/src/painter.rs +++ b/crates/egui_glow/src/painter.rs @@ -144,7 +144,7 @@ impl Painter { shader_version: Option, dithering: bool, ) -> Result { - crate::profile_function!(); + profiling::function_scope!(); crate::check_for_gl_error_even_in_release!(&gl, "before Painter::new"); // some useful debug info. all three of them are present in gl 1.1. @@ -366,7 +366,7 @@ impl Painter { clipped_primitives: &[egui::ClippedPrimitive], textures_delta: &egui::TexturesDelta, ) { - crate::profile_function!(); + profiling::function_scope!(); for (id, image_delta) in &textures_delta.set { self.set_texture(*id, image_delta); @@ -405,7 +405,7 @@ impl Painter { pixels_per_point: f32, clipped_primitives: &[egui::ClippedPrimitive], ) { - crate::profile_function!(); + profiling::function_scope!(); self.assert_not_destroyed(); unsafe { self.prepare_painting(screen_size_px, pixels_per_point) }; @@ -423,7 +423,7 @@ impl Painter { } Primitive::Callback(callback) => { if callback.rect.is_positive() { - crate::profile_scope!("callback"); + profiling::scope!("callback"); let info = egui::PaintCallbackInfo { viewport: callback.rect, @@ -508,7 +508,7 @@ impl Painter { // ------------------------------------------------------------------------ pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) { - crate::profile_function!(); + profiling::function_scope!(); self.assert_not_destroyed(); @@ -540,7 +540,7 @@ impl Painter { ); let data: Vec = { - crate::profile_scope!("font -> sRGBA"); + profiling::scope!("font -> sRGBA"); image .srgba_pixels(None) .flat_map(|a| a.to_array()) @@ -559,7 +559,7 @@ impl Painter { options: egui::TextureOptions, data: &[u8], ) { - crate::profile_function!(); + profiling::function_scope!(); assert_eq!(data.len(), w * h * 4); assert!( w <= self.max_texture_side && h <= self.max_texture_side, @@ -610,7 +610,7 @@ impl Painter { let level = 0; if let Some([x, y]) = pos { - crate::profile_scope!("gl.tex_sub_image_2d"); + profiling::scope!("gl.tex_sub_image_2d"); self.gl.tex_sub_image_2d( glow::TEXTURE_2D, level, @@ -625,7 +625,7 @@ impl Painter { check_for_gl_error!(&self.gl, "tex_sub_image_2d"); } else { let border = 0; - crate::profile_scope!("gl.tex_image_2d"); + profiling::scope!("gl.tex_image_2d"); self.gl.tex_image_2d( glow::TEXTURE_2D, level, @@ -675,7 +675,7 @@ impl Painter { } pub fn read_screen_rgba(&self, [w, h]: [u32; 2]) -> egui::ColorImage { - crate::profile_function!(); + profiling::function_scope!(); let mut pixels = vec![0_u8; (w * h * 4) as usize]; unsafe { @@ -700,8 +700,7 @@ impl Painter { } pub fn read_screen_rgb(&self, [w, h]: [u32; 2]) -> Vec { - crate::profile_function!(); - + profiling::function_scope!(); let mut pixels = vec![0_u8; (w * h * 3) as usize]; unsafe { self.gl.read_pixels( @@ -748,7 +747,7 @@ impl Painter { } pub fn clear(gl: &glow::Context, screen_size_in_pixels: [u32; 2], clear_color: [f32; 4]) { - crate::profile_function!(); + profiling::function_scope!(); unsafe { gl.disable(glow::SCISSOR_TEST); diff --git a/crates/epaint/Cargo.toml b/crates/epaint/Cargo.toml index 7815189dc5a..0a4afde26b5 100644 --- a/crates/epaint/Cargo.toml +++ b/crates/epaint/Cargo.toml @@ -55,11 +55,6 @@ log = ["dep:log"] ## [`mint`](https://docs.rs/mint) enables interoperability with other math libraries such as [`glam`](https://docs.rs/glam) and [`nalgebra`](https://docs.rs/nalgebra). mint = ["emath/mint"] -## Enable profiling with the [`puffin`](https://docs.rs/puffin) crate. -## -## Only enabled on native, because of the low resolution (1ms) of clocks in browsers. -puffin = ["dep:puffin"] - ## Enable parallel tessellation using [`rayon`](https://docs.rs/rayon). ## ## This can help performance for graphics-intense applications. @@ -79,6 +74,7 @@ ab_glyph = "0.2.11" ahash.workspace = true nohash-hasher.workspace = true parking_lot.workspace = true # Using parking_lot over std::sync::Mutex gives 50% speedups in some real-world scenarios. +profiling = { workspace = true} #! ### Optional dependencies bytemuck = { workspace = true, optional = true, features = ["derive"] } @@ -87,7 +83,6 @@ bytemuck = { workspace = true, optional = true, features = ["derive"] } document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } -puffin = { workspace = true, optional = true } rayon = { version = "1.7", optional = true } ## Allow serialization using [`serde`](https://docs.rs/serde) . diff --git a/crates/epaint/src/lib.rs b/crates/epaint/src/lib.rs index 4dee42381cd..89664cdb844 100644 --- a/crates/epaint/src/lib.rs +++ b/crates/epaint/src/lib.rs @@ -143,33 +143,3 @@ pub enum Primitive { /// Was epaint compiled with the `rayon` feature? pub const HAS_RAYON: bool = cfg!(feature = "rayon"); - -// --------------------------------------------------------------------------- - -mod profiling_scopes { - #![allow(unused_macros)] - #![allow(unused_imports)] - - /// Profiling macro for feature "puffin" - macro_rules! profile_function { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_function!($($arg)*); - }; - } - pub(crate) use profile_function; - - /// Profiling macro for feature "puffin" - macro_rules! profile_scope { - ($($arg: tt)*) => { - #[cfg(feature = "puffin")] - #[cfg(not(target_arch = "wasm32"))] // Disabled on web because of the coarse 1ms clock resolution there. - puffin::profile_scope!($($arg)*); - }; - } - pub(crate) use profile_scope; -} - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::{profile_function, profile_scope}; diff --git a/crates/epaint/src/mesh.rs b/crates/epaint/src/mesh.rs index ba37e715856..2447cad293e 100644 --- a/crates/epaint/src/mesh.rs +++ b/crates/epaint/src/mesh.rs @@ -85,7 +85,7 @@ impl Mesh { /// Are all indices within the bounds of the contained vertices? pub fn is_valid(&self) -> bool { - crate::profile_function!(); + profiling::function_scope!(); if let Ok(n) = u32::try_from(self.vertices.len()) { self.indices.iter().all(|&i| i < n) @@ -111,7 +111,7 @@ impl Mesh { /// /// Panics when `other` mesh has a different texture. pub fn append(&mut self, other: Self) { - crate::profile_function!(); + profiling::function_scope!(); debug_assert!(other.is_valid()); if self.is_empty() { diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index fdbe270914e..9821e4021e1 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1363,7 +1363,7 @@ impl Tessellator { self.tessellate_ellipse(ellipse, out); } Shape::Mesh(mesh) => { - crate::profile_scope!("mesh"); + profiling::scope!("mesh"); if self.options.validate_meshes && !mesh.is_valid() { debug_assert!(false, "Invalid Mesh in Shape::Mesh"); @@ -1596,7 +1596,7 @@ impl Tessellator { return; } - crate::profile_function!(); + profiling::function_scope!(); let PathShape { points, @@ -1977,7 +1977,7 @@ impl Tessellator { /// A list of clip rectangles with matching [`Mesh`]. #[allow(unused_mut)] pub fn tessellate_shapes(&mut self, mut shapes: Vec) -> Vec { - crate::profile_function!(); + profiling::function_scope!(); #[cfg(feature = "rayon")] if self.options.parallel_tessellation { @@ -1987,7 +1987,7 @@ impl Tessellator { let mut clipped_primitives: Vec = Vec::default(); { - crate::profile_scope!("tessellate"); + profiling::scope!("tessellate"); for clipped_shape in shapes { self.tessellate_clipped_shape(clipped_shape, &mut clipped_primitives); } @@ -2024,7 +2024,7 @@ impl Tessellator { /// then replace the original shape with their tessellated meshes. #[cfg(feature = "rayon")] fn parallel_tessellation_of_large_shapes(&self, shapes: &mut [ClippedShape]) { - crate::profile_function!(); + profiling::function_scope!(); use rayon::prelude::*; @@ -2054,7 +2054,7 @@ impl Tessellator { .enumerate() .filter(|(_, clipped_shape)| should_parallelize(&clipped_shape.shape)) .map(|(index, clipped_shape)| { - crate::profile_scope!("tessellate_big_shape"); + profiling::scope!("tessellate_big_shape"); // TODO(emilk): reuse tessellator in a thread local let mut tessellator = (*self).clone(); let mut mesh = Mesh::default(); @@ -2063,7 +2063,7 @@ impl Tessellator { }) .collect(); - crate::profile_scope!("distribute results", tessellated.len().to_string()); + profiling::scope!("distribute results", tessellated.len().to_string()); for (index, mesh) in tessellated { shapes[index].shape = Shape::Mesh(mesh); } diff --git a/examples/puffin_profiler/Cargo.toml b/examples/puffin_profiler/Cargo.toml index 3389de5c928..d0e9e485a8d 100644 --- a/examples/puffin_profiler/Cargo.toml +++ b/examples/puffin_profiler/Cargo.toml @@ -7,6 +7,9 @@ edition = "2021" rust-version = "1.80" publish = false +[package.metadata.cargo-machete] +ignored = ["profiling"] + [lints] workspace = true @@ -18,7 +21,6 @@ wgpu = ["eframe/wgpu"] [dependencies] eframe = { workspace = true, features = [ "default", - "puffin", "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO ] } env_logger = { version = "0.10", default-features = false, features = [ @@ -28,3 +30,4 @@ env_logger = { version = "0.10", default-features = false, features = [ log = { workspace = true } puffin = "0.19" puffin_http = "0.16" +profiling = {workspace = true, features = ["profile-with-puffin"] } From f7efb2186d529ddc9e69f7173c6ae3ae5f403d0b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 09:33:25 +0100 Subject: [PATCH 15/34] Improve hit-test of thin widgets, and widgets across layers (#5468) When there are multiple layers (e.g. with custom transforms) close to each other, the hit test code used to only consider widgets in the layer directly under the mouse. This can make it difficult to hit thin widgets just on the outside of a transform layer. This PR fixes that. It also prioritizes thin widgets, so that if there is both a thin widget and a thick widget under the mouse cursor, you will always hit the thin widgets, even if the thin widgets is layered behind the thick one. This makes it easier to hit thin resize-handles. In theory this should allow us to make `resize_grab_radius_side` and `resize_grab_radius_corner` smaller in a future PR, if we want to. --- crates/egui/src/context.rs | 40 +++---- crates/egui/src/hit_test.rs | 209 ++++++++++++++++++++++++--------- crates/egui/src/interaction.rs | 6 +- crates/egui/src/memory/mod.rs | 97 +++++++++------ crates/egui/src/widget_rect.rs | 25 +++- 5 files changed, 258 insertions(+), 119 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 8ab83c21935..322007113ba 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -498,19 +498,8 @@ impl ContextImpl { viewport.this_pass.begin_pass(screen_rect); { - let area_order = self.memory.areas().order_map(); - let mut layers: Vec = viewport.prev_pass.widgets.layer_ids().collect(); - - layers.sort_by(|a, b| { - if a.order == b.order { - // Maybe both are windows, so respect area order: - area_order.get(a).cmp(&area_order.get(b)) - } else { - // comparing e.g. background to tooltips - a.order.cmp(&b.order) - } - }); + layers.sort_by(|&a, &b| self.memory.areas().compare_order(a, b)); viewport.hits = if let Some(pos) = viewport.input.pointer.interact_pos() { let interact_radius = self.memory.options.style().interaction.interact_radius; @@ -2248,7 +2237,8 @@ impl Context { for id in contains_pointer { let mut widget_text = format!("{id:?}"); if let Some(rect) = widget_rects.get(id) { - widget_text += &format!(" {:?} {:?}", rect.rect, rect.sense); + widget_text += + &format!(" {:?} {:?} {:?}", rect.layer_id, rect.rect, rect.sense); } if let Some(info) = widget_rects.info(id) { widget_text += &format!(" {info:?}"); @@ -2274,11 +2264,17 @@ impl Context { if self.style().debug.show_widget_hits { let hits = self.write(|ctx| ctx.viewport().hits.clone()); let WidgetHits { + close, contains_pointer, click, drag, } = hits; + if false { + for widget in &close { + paint_widget(widget, "close", Color32::from_gray(70)); + } + } if true { for widget in &contains_pointer { paint_widget(widget, "contains_pointer", Color32::BLUE); @@ -3161,28 +3157,26 @@ impl Context { self.memory_mut(|mem| *mem.areas_mut() = Default::default()); } }); - ui.indent("areas", |ui| { - ui.label("Visible areas, ordered back to front."); - ui.label("Hover to highlight"); + ui.indent("layers", |ui| { + ui.label("Layers, ordered back to front."); let layers_ids: Vec = self.memory(|mem| mem.areas().order().to_vec()); for layer_id in layers_ids { - let area = AreaState::load(self, layer_id.id); - if let Some(area) = area { + if let Some(area) = AreaState::load(self, layer_id.id) { let is_visible = self.memory(|mem| mem.areas().is_visible(&layer_id)); if !is_visible { continue; } let text = format!("{} - {:?}", layer_id.short_debug_format(), area.rect(),); // TODO(emilk): `Sense::hover_highlight()` - if ui - .add(Label::new(RichText::new(text).monospace()).sense(Sense::click())) - .hovered - && is_visible - { + let response = + ui.add(Label::new(RichText::new(text).monospace()).sense(Sense::click())); + if response.hovered && is_visible { ui.ctx() .debug_painter() .debug_rect(area.rect(), Color32::RED, ""); } + } else { + ui.monospace(layer_id.short_debug_format()); } } }); diff --git a/crates/egui/src/hit_test.rs b/crates/egui/src/hit_test.rs index 0d85621220e..8741f5f8b41 100644 --- a/crates/egui/src/hit_test.rs +++ b/crates/egui/src/hit_test.rs @@ -2,7 +2,7 @@ use ahash::HashMap; use emath::TSTransform; -use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects}; +use crate::{ahash, emath, LayerId, Pos2, Rect, WidgetRect, WidgetRects}; /// Result of a hit-test against [`WidgetRects`]. /// @@ -12,11 +12,18 @@ use crate::{ahash, emath, LayerId, Pos2, WidgetRect, WidgetRects}; /// or if we're currently already dragging something. #[derive(Clone, Debug, Default)] pub struct WidgetHits { + /// All widgets close to the pointer, back-to-front. + /// + /// This is a superset of all other widgets in this struct. + pub close: Vec, + /// All widgets that contains the pointer, back-to-front. /// - /// i.e. both a Window and the button in it can contain the pointer. + /// i.e. both a Window and the Button in it can contain the pointer. /// /// Some of these may be widgets in a layer below the top-most layer. + /// + /// This will be used for hovering. pub contains_pointer: Vec, /// If the user would start a clicking now, this is what would be clicked. @@ -63,6 +70,7 @@ pub fn hit_test( } let pos_in_layer = pos_in_layers.get(&w.layer_id).copied().unwrap_or(pos); + // TODO(emilk): we should probably do the distance testing in global space instead let dist_sq = w.interact_rect.distance_sq_to_pos(pos_in_layer); // In tie, pick last = topmost. @@ -76,51 +84,103 @@ pub fn hit_test( .copied() .collect(); - // We need to pick one single layer for the interaction. - if let Some(closest_hit) = closest_hit { - // Select the top layer, and ignore widgets in any other layer: - let top_layer = closest_hit.layer_id; - close.retain(|w| w.layer_id == top_layer); - - // If the widget is disabled, treat it as if it isn't sensing anything. - // This simplifies the code in `hit_test_on_close` so it doesn't have to check - // the `enabled` flag everywhere: - for w in &mut close { - if !w.enabled { - w.sense.click = false; - w.sense.drag = false; - } + // Transform to global coordinates: + for hit in &mut close { + if let Some(to_global) = layer_to_global.get(&hit.layer_id).copied() { + *hit = hit.transform(to_global); } + } - let pos_in_layer = pos_in_layers.get(&top_layer).copied().unwrap_or(pos); - let hits = hit_test_on_close(&close, pos_in_layer); - - if let Some(drag) = hits.drag { - debug_assert!(drag.sense.drag); - } - if let Some(click) = hits.click { - debug_assert!(click.sense.click); + // When using layer transforms it is common to stack layers close to each other. + // For instance, you may have a resize-separator on a panel, with two + // transform-layers on either side. + // The resize-separator is technically in a layer _behind_ the transform-layers, + // but the user doesn't perceive it as such. + // So how do we handle this case? + // + // If we just allow interactions with ALL close widgets, + // then we might accidentally allow clicks through windows and other bad stuff. + // + // Let's try this: + // * Set up a hit-area (based on search_radius) + // * Iterate over all hits top-to-bottom + // * Stop if any hit covers the whole hit-area, otherwise keep going + // * Collect the layers ids in a set + // * Remove all widgets not in the above layer set + // + // This will most often result in only one layer, + // but if the pointer is at the edge of a layer, we might include widgets in + // a layer behind it. + + let mut included_layers: ahash::HashSet = Default::default(); + for hit in close.iter().rev() { + included_layers.insert(hit.layer_id); + let hit_covers_search_area = contains_circle(hit.interact_rect, pos, search_radius); + if hit_covers_search_area { + break; // nothing behind this layer could ever be interacted with } + } + + close.retain(|hit| included_layers.contains(&hit.layer_id)); - hits - } else { - // No close widgets. - Default::default() + // If a widget is disabled, treat it as if it isn't sensing anything. + // This simplifies the code in `hit_test_on_close` so it doesn't have to check + // the `enabled` flag everywhere: + for w in &mut close { + if !w.enabled { + w.sense.click = false; + w.sense.drag = false; + } } -} -fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { - #![allow(clippy::collapsible_else_if)] + let mut hits = hit_test_on_close(&close, pos); - // Only those widgets directly under the `pos`. - let hits: Vec = close + hits.contains_pointer = close .iter() .filter(|widget| widget.interact_rect.contains(pos)) .copied() .collect(); - let hit_click = hits.iter().copied().filter(|w| w.sense.click).last(); - let hit_drag = hits.iter().copied().filter(|w| w.sense.drag).last(); + hits.close = close; + + { + // Undo the to_global-transform we applied earlier, + // go back to local layer-coordinates: + + let restore_widget_rect = |w: &mut WidgetRect| { + *w = widgets.get(w.id).copied().unwrap_or(*w); + }; + + for wr in &mut hits.close { + restore_widget_rect(wr); + } + for wr in &mut hits.contains_pointer { + restore_widget_rect(wr); + } + if let Some(wr) = &mut hits.drag { + debug_assert!(wr.sense.drag); + restore_widget_rect(wr); + } + if let Some(wr) = &mut hits.click { + debug_assert!(wr.sense.click); + restore_widget_rect(wr); + } + } + + hits +} + +/// Returns true if the rectangle contains the whole circle. +fn contains_circle(interact_rect: emath::Rect, pos: Pos2, radius: f32) -> bool { + interact_rect.shrink(radius).contains(pos) +} + +fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { + #![allow(clippy::collapsible_else_if)] + + // First find the best direct hits: + let hit_click = find_closest_within(close.iter().copied().filter(|w| w.sense.click), pos, 0.0); + let hit_drag = find_closest_within(close.iter().copied().filter(|w| w.sense.drag), pos, 0.0); match (hit_click, hit_drag) { (None, None) => { @@ -136,16 +196,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { if let Some(closest) = closest { WidgetHits { - contains_pointer: hits, click: closest.sense.click.then_some(closest), drag: closest.sense.drag.then_some(closest), + ..Default::default() } } else { // Found nothing WidgetHits { - contains_pointer: hits, click: None, drag: None, + ..Default::default() } } } @@ -170,17 +230,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // This is a smaller thing on a big background - help the user hit it, // and ignore the big drag background. WidgetHits { - contains_pointer: hits, click: Some(closest_click), drag: Some(closest_click), + ..Default::default() } } else { - // The drag wiudth is separate from the click wiudth, - // so return only the drag widget + // The drag-widget is separate from the click-widget, + // so return only the drag-widget WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } else { @@ -194,17 +254,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // The drag widget is a big background thing (scroll area), // so returning a separate click widget should not be confusing WidgetHits { - contains_pointer: hits, click: Some(closest_click), drag: Some(hit_drag), + ..Default::default() } } else { // The two widgets are just two normal small widgets close to each other. // Highlighting both would be very confusing. WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -229,17 +289,17 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // `hit_drag` is a big background thing and `closest_drag` is something small on top of it. // Be helpful and return the small things: return WidgetHits { - contains_pointer: hits, click: None, drag: Some(closest_drag), + ..Default::default() }; } } WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -253,57 +313,57 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { // where when hovering directly over a drag-widget (like a big ScrollArea), // we look for close click-widgets (e.g. buttons). // This is because big background drag-widgets (ScrollArea, Window) are common, - // but bit clickable things aren't. + // but big clickable things aren't. // Even if they were, I think it would be confusing for a user if clicking // a drag-only widget would click something _behind_ it. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: None, + ..Default::default() } } (Some(hit_click), Some(hit_drag)) => { // We have a perfect hit on both click and drag. Which is the topmost? - let click_idx = hits.iter().position(|w| *w == hit_click).unwrap(); - let drag_idx = hits.iter().position(|w| *w == hit_drag).unwrap(); + let click_idx = close.iter().position(|w| *w == hit_click).unwrap(); + let drag_idx = close.iter().position(|w| *w == hit_drag).unwrap(); let click_is_on_top_of_drag = drag_idx < click_idx; if click_is_on_top_of_drag { if hit_click.sense.drag { // The top thing senses both clicks and drags. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: Some(hit_click), + ..Default::default() } } else { // They are interested in different things, // and click is on top. Report both hits, // e.g. the top Button and the ScrollArea behind it. WidgetHits { - contains_pointer: hits, click: Some(hit_click), drag: Some(hit_drag), + ..Default::default() } } } else { if hit_drag.sense.click { // The top thing senses both clicks and drags. WidgetHits { - contains_pointer: hits, click: Some(hit_drag), drag: Some(hit_drag), + ..Default::default() } } else { // The top things senses only drags, // so we ignore the click-widget, because it would be confusing // if clicking a drag-widget would actually click something else below it. WidgetHits { - contains_pointer: hits, click: None, drag: Some(hit_drag), + ..Default::default() } } } @@ -312,8 +372,16 @@ fn hit_test_on_close(close: &[WidgetRect], pos: Pos2) -> WidgetHits { } fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option { - let mut closest = None; - let mut closest_dist_sq = f32::INFINITY; + find_closest_within(widgets, pos, f32::INFINITY) +} + +fn find_closest_within( + widgets: impl Iterator, + pos: Pos2, + max_dist: f32, +) -> Option { + let mut closest: Option = None; + let mut closest_dist_sq = max_dist * max_dist; for widget in widgets { if widget.interact_rect.is_negative() { continue; @@ -321,6 +389,16 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< let dist_sq = widget.interact_rect.distance_sq_to_pos(pos); + if let Some(closest) = closest { + if dist_sq == closest_dist_sq { + // It's a tie! Pick the thin candidate over the thick one. + // This makes it easier to hit a thin resize-handle, for instance: + if should_prioritizie_hits_on_back(closest.interact_rect, widget.interact_rect) { + continue; + } + } + } + // In case of a tie, take the last one = the one on top. if dist_sq <= closest_dist_sq { closest_dist_sq = dist_sq; @@ -331,6 +409,27 @@ fn find_closest(widgets: impl Iterator, pos: Pos2) -> Option< closest } +/// Should we prioritizie hits on `back` over those on `front`? +/// +/// `back` should be behind the `front` widget. +/// +/// Returns true if `back` is a small hit-target and `front` is not. +fn should_prioritizie_hits_on_back(back: Rect, front: Rect) -> bool { + if front.contains_rect(back) { + return false; // back widget is fully occluded; no way to hit it + } + + // Reduce each rect to its width or height, whichever is smaller: + let back = back.width().min(back.height()); + let front = front.width().min(front.height()); + + // These are hard-coded heuristics that could surely be improved. + let back_is_much_thinner = back <= 0.5 * front; + let back_is_thin = back <= 16.0; + + back_is_much_thinner && back_is_thin +} + #[cfg(test)] mod tests { use emath::{pos2, vec2, Rect}; diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index afac1602836..04f8f7dbf6f 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -249,7 +249,7 @@ pub(crate) fn interact( .copied() .collect() } else { - // We may be hovering a an interactive widget or two. + // We may be hovering an interactive widget or two. // We must also consider the case where non-interactive widgets // are _on top_ of an interactive widget. // For instance: a label in a draggable window. @@ -264,9 +264,9 @@ pub(crate) fn interact( // but none below it (an interactive widget stops the hover search). // // To know when to stop we need to first know the order of the widgets, - // which luckily we have in the `WidgetRects`. + // which luckily we already have in `hits.close`. - let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer + let order = |id| hits.close.iter().position(|w| w.id == id); let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0); let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0); diff --git a/crates/egui/src/memory/mod.rs b/crates/egui/src/memory/mod.rs index 2d1449c1694..976ad2d95ef 100644 --- a/crates/egui/src/memory/mod.rs +++ b/crates/egui/src/memory/mod.rs @@ -1132,15 +1132,18 @@ type OrderMap = HashMap; pub struct Areas { areas: IdMap, + visible_areas_last_frame: ahash::HashSet, + visible_areas_current_frame: ahash::HashSet, + + // ---------------------------- + // Everything below this is general to all layers, not just areas. + // TODO(emilk): move this to a separate struct. /// Back-to-front, top is last. order: Vec, - /// Actual order of the layers, pre-calculated each frame. + /// Inverse of [`Self::order`], calculated at the end of the frame. order_map: OrderMap, - visible_last_frame: ahash::HashSet, - visible_current_frame: ahash::HashSet, - /// When an area wants to be on top, it is assigned here. /// This is used to reorder the layers at the end of the frame. /// If several layers want to be on top, they will keep their relative order. @@ -1148,9 +1151,9 @@ pub struct Areas { /// results in them being sent to the top and keeping their previous internal order. wants_to_be_on_top: ahash::HashSet, - /// List of sublayers for each layer. + /// The sublayers that each layer has. /// - /// When a layer has sublayers, they are moved directly above it in the ordering. + /// The parent sublayer is moved directly above the child sublayers in the ordering. sublayers: ahash::HashMap>, } @@ -1163,17 +1166,13 @@ impl Areas { self.areas.get(&id) } - /// Back-to-front, top is last. + /// All layers back-to-front, top is last. pub(crate) fn order(&self) -> &[LayerId] { &self.order } - /// For each layer, which [`Self::order`] is it in? - pub(crate) fn order_map(&self) -> &OrderMap { - &self.order_map - } - /// Compare the order of two layers, based on the order list from last frame. + /// /// May return [`std::cmp::Ordering::Equal`] if the layers are not in the order list. pub(crate) fn compare_order(&self, a: LayerId, b: LayerId) -> std::cmp::Ordering { if let (Some(a), Some(b)) = (self.order_map.get(&a), self.order_map.get(&b)) { @@ -1183,18 +1182,8 @@ impl Areas { } } - /// Calculates the order map. - fn calculate_order_map(&mut self) { - self.order_map = self - .order - .iter() - .enumerate() - .map(|(i, id)| (*id, i)) - .collect(); - } - pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::AreaState) { - self.visible_current_frame.insert(layer_id); + self.visible_areas_current_frame.insert(layer_id); self.areas.insert(layer_id.id, state); if !self.order.iter().any(|x| *x == layer_id) { self.order.push(layer_id); @@ -1227,18 +1216,19 @@ impl Areas { } pub fn visible_last_frame(&self, layer_id: &LayerId) -> bool { - self.visible_last_frame.contains(layer_id) + self.visible_areas_last_frame.contains(layer_id) } pub fn is_visible(&self, layer_id: &LayerId) -> bool { - self.visible_last_frame.contains(layer_id) || self.visible_current_frame.contains(layer_id) + self.visible_areas_last_frame.contains(layer_id) + || self.visible_areas_current_frame.contains(layer_id) } pub fn visible_layer_ids(&self) -> ahash::HashSet { - self.visible_last_frame + self.visible_areas_last_frame .iter() .copied() - .chain(self.visible_current_frame.iter().copied()) + .chain(self.visible_areas_current_frame.iter().copied()) .collect() } @@ -1251,7 +1241,7 @@ impl Areas { } pub fn move_to_top(&mut self, layer_id: LayerId) { - self.visible_current_frame.insert(layer_id); + self.visible_areas_current_frame.insert(layer_id); self.wants_to_be_on_top.insert(layer_id); if !self.order.iter().any(|x| *x == layer_id) { @@ -1266,8 +1256,21 @@ impl Areas { /// /// This currently only supports one level of nesting. If `parent` is a sublayer of another /// layer, the behavior is unspecified. + /// + /// The two layers must have the same [`LayerId::order`]. pub fn set_sublayer(&mut self, parent: LayerId, child: LayerId) { + debug_assert_eq!(parent.order, child.order, + "DEBUG ASSERT: Trying to set sublayers across layers of different order ({:?}, {:?}), which is currently undefined behavior in egui", parent.order, child.order); + self.sublayers.entry(parent).or_default().insert(child); + + // Make sure the layers are in the order list: + if !self.order.iter().any(|x| *x == parent) { + self.order.push(parent); + } + if !self.order.iter().any(|x| *x == child) { + self.order.push(child); + } } pub fn top_layer_id(&self, order: Order) -> Option { @@ -1278,26 +1281,42 @@ impl Areas { .copied() } + /// If this layer is the sublayer of another layer, return the parent. + pub fn parent_layer(&self, layer_id: LayerId) -> Option { + self.sublayers.iter().find_map(|(parent, children)| { + if children.contains(&layer_id) { + Some(*parent) + } else { + None + } + }) + } + + /// All the child layers of this layer. + pub fn child_layers(&self, layer_id: LayerId) -> impl Iterator + '_ { + self.sublayers.get(&layer_id).into_iter().flatten().copied() + } + pub(crate) fn is_sublayer(&self, layer: &LayerId) -> bool { - self.sublayers - .iter() - .any(|(_, children)| children.contains(layer)) + self.parent_layer(*layer).is_some() } pub(crate) fn end_pass(&mut self) { let Self { - visible_last_frame, - visible_current_frame, + visible_areas_last_frame, + visible_areas_current_frame, order, wants_to_be_on_top, sublayers, .. } = self; - std::mem::swap(visible_last_frame, visible_current_frame); - visible_current_frame.clear(); + std::mem::swap(visible_areas_last_frame, visible_areas_current_frame); + visible_areas_current_frame.clear(); + order.sort_by_key(|layer| (layer.order, wants_to_be_on_top.contains(layer))); wants_to_be_on_top.clear(); + // For all layers with sublayers, put the sublayers directly after the parent layer: let sublayers = std::mem::take(sublayers); for (parent, children) in sublayers { @@ -1315,7 +1334,13 @@ impl Areas { }; order.splice(parent_pos..=parent_pos, moved_layers); } - self.calculate_order_map(); + + self.order_map = self + .order + .iter() + .enumerate() + .map(|(i, id)| (*id, i)) + .collect(); } } diff --git a/crates/egui/src/widget_rect.rs b/crates/egui/src/widget_rect.rs index dd900af1f11..3725d62a7b0 100644 --- a/crates/egui/src/widget_rect.rs +++ b/crates/egui/src/widget_rect.rs @@ -20,10 +20,10 @@ pub struct WidgetRect { /// What layer the widget is on. pub layer_id: LayerId, - /// The full widget rectangle. + /// The full widget rectangle, in local layer coordinates. pub rect: Rect, - /// Where the widget is. + /// Where the widget is, in local layer coordinates. /// /// This is after clipping with the parent ui clip rect. pub interact_rect: Rect, @@ -42,6 +42,27 @@ pub struct WidgetRect { pub enabled: bool, } +impl WidgetRect { + pub fn transform(self, transform: emath::TSTransform) -> Self { + let Self { + id, + layer_id, + rect, + interact_rect, + sense, + enabled, + } = self; + Self { + id, + layer_id, + rect: transform * rect, + interact_rect: transform * interact_rect, + sense, + enabled, + } + } +} + /// Stores the [`WidgetRect`]s of all widgets generated during a single egui update/frame. /// /// All [`crate::Ui`]s have a [`WidgetRect`]. It is created in [`crate::Ui::new`] with [`Rect::NOTHING`] From 3bdb19e8649482690a607a7f7bd905b91435c1ad Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 16 Dec 2024 11:38:08 +0100 Subject: [PATCH 16/34] Workaround for egui having wrong scale in firefox (#5466) * [x] I have followed the instructions in the PR template --- crates/eframe/src/web/events.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 414e5be2383..42fcf9f4c0f 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -870,7 +870,15 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32 let mut dpr = web_sys::window().unwrap().device_pixel_ratio(); let entry: web_sys::ResizeObserverEntry = resize_observer_entries.at(0).dyn_into()?; - if JsValue::from_str("devicePixelContentBoxSize").js_in(entry.as_ref()) { + // TODO(lucasmerlin): This is disabled because of https://github.com/emilk/egui/issues/5246 + // Not only does it break on chrome when moving the window across screens, but it also + // completely breaks in firefox because for some reason firefox reports the devicePixelRatio + // as 2.0 when it should be 1.0. + // The proper fix would probably be to calculate the correct device pixel ratio based on + // the inline_size and the content_rect.width, and use that + // wherever we access window.devicePixelRatio + #[allow(clippy::overly_complex_bool_expr)] + if JsValue::from_str("devicePixelContentBoxSize").js_in(entry.as_ref()) && false { // NOTE: Only this path gives the correct answer for most browsers. // Unfortunately this doesn't work perfectly everywhere. let size: web_sys::ResizeObserverSize = From f0ec2f05c49f05217892ab05f4e78090aaa28164 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 14:16:54 +0100 Subject: [PATCH 17/34] Fix broken images on egui.rs (move from git lfs to normal git) (#5480) The images in the widget gallery on egui.rs are broken: ![image](https://github.com/user-attachments/assets/305e1041-e3e3-472d-9a52-1b90e8da053d) ~Not sure why yet, and I fail to reproduce locally.~ It's because they are on git lfs. --- .gitattributes | 5 ++++- .github/workflows/png_only_on_lfs.yml | 11 +++++++++-- crates/egui/assets/ferris.png | Bin 130 -> 46286 bytes crates/egui_demo_app/Cargo.toml | 9 ++++++--- crates/egui_demo_lib/data/icon.png | Bin 129 -> 2642 bytes .../egui_extras/src/loaders/image_loader.rs | 6 ++++++ 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.gitattributes b/.gitattributes index 09b004b60c0..b5348bf23b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,8 @@ * text=auto eol=lf Cargo.lock linguist-generated=false *.png filter=lfs diff=lfs merge=lfs -text -# The icon.png is needed when including eframe via git, so it may not be in lfs + +# Exclude some small files from LFS: crates/eframe/data/* !filter !diff !merge text=auto eol=lf +crates/egui_demo_lib/data/* !filter !diff !merge text=auto eol=lf +crates/egui/assets/* !filter !diff !merge text=auto eol=lf diff --git a/.github/workflows/png_only_on_lfs.yml b/.github/workflows/png_only_on_lfs.yml index e3d68ea3abf..624a7f4502d 100644 --- a/.github/workflows/png_only_on_lfs.yml +++ b/.github/workflows/png_only_on_lfs.yml @@ -13,11 +13,18 @@ jobs: - name: Check that png files are on git LFS run: | binary_extensions="png" - exclude="crates/eframe/data" + exclude_paths=( + "crates/eframe/data" + "crates/egui_demo_lib/data/" + "crates/egui/assets/" + ) # Find binary files that are not tracked by Git LFS for ext in $binary_extensions; do - if comm -23 <(git ls-files | grep -v "^$exclude" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then + # Create grep pattern to exclude multiple paths + exclude_pattern=$(printf "|^%s" "${exclude_paths[@]}" | sed 's/^|//') + + if comm -23 <(git ls-files | grep -Ev "$exclude_pattern" | sort) <(git lfs ls-files -n | sort) | grep "\.${ext}$"; then echo "Error: Found binary file with extension .$ext not tracked by git LFS. See CONTRIBUTING.md" exit 1 fi diff --git a/crates/egui/assets/ferris.png b/crates/egui/assets/ferris.png index 129c7f931c059afa966ebe1e5d910fc6dbab99cb..8741baa19d02a003db73c33bd779a324bc1a57fa 100644 GIT binary patch literal 46286 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 RENp6y4PQkzK4g3_k}7JS`?oK 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 MVrYS>(szsd4{n6D`v3p{ literal 129 zcmWN?K@!3s3;@78uiyig0D+eNCJjNDQRxWw;OliSd&zsW{?c{MbL__4+q^x>SpL_~ zv2cHyaSEHus@}35m0XBB_>>qC0U`)(xDh45>^-{-8Jx|bSVAtraV|#;X-%Ps4J!*! MifXj4Xcy6`AEKuw_5c6? diff --git a/crates/egui_extras/src/loaders/image_loader.rs b/crates/egui_extras/src/loaders/image_loader.rs index 088ef5e4587..4c1a846e26b 100644 --- a/crates/egui_extras/src/loaders/image_loader.rs +++ b/crates/egui_extras/src/loaders/image_loader.rs @@ -78,6 +78,12 @@ impl ImageLoader for ImageCrateLoader { } } + if bytes.starts_with(b"version https://git-lfs") { + return Err(LoadError::FormatNotSupported { + detected_format: Some("git-lfs".to_owned()), + }); + } + // (3) log::trace!("started loading {uri:?}"); let result = crate::image::load_image_bytes(&bytes).map(Arc::new); From 3995491212fd06a98f7ebe7528757df1bc86e5cb Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 14:41:02 +0100 Subject: [PATCH 18/34] Revert "Workaround for egui having wrong scale in firefox" (#5481) * Reverts emilk/egui#5466 Restores pixel-perfect rendering on chromium, Mac --- crates/eframe/src/web/events.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 42fcf9f4c0f..414e5be2383 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -870,15 +870,7 @@ fn get_display_size(resize_observer_entries: &js_sys::Array) -> Result<(u32, u32 let mut dpr = web_sys::window().unwrap().device_pixel_ratio(); let entry: web_sys::ResizeObserverEntry = resize_observer_entries.at(0).dyn_into()?; - // TODO(lucasmerlin): This is disabled because of https://github.com/emilk/egui/issues/5246 - // Not only does it break on chrome when moving the window across screens, but it also - // completely breaks in firefox because for some reason firefox reports the devicePixelRatio - // as 2.0 when it should be 1.0. - // The proper fix would probably be to calculate the correct device pixel ratio based on - // the inline_size and the content_rect.width, and use that - // wherever we access window.devicePixelRatio - #[allow(clippy::overly_complex_bool_expr)] - if JsValue::from_str("devicePixelContentBoxSize").js_in(entry.as_ref()) && false { + if JsValue::from_str("devicePixelContentBoxSize").js_in(entry.as_ref()) { // NOTE: Only this path gives the correct answer for most browsers. // Unfortunately this doesn't work perfectly everywhere. let size: web_sys::ResizeObserverSize = From b1d2551e7ee529d7d0427173d8a0ec6f49f5b976 Mon Sep 17 00:00:00 2001 From: Andreas Reich Date: Mon, 16 Dec 2024 15:03:01 +0100 Subject: [PATCH 19/34] Make frame delay on screenshots consistently one frame on web as well (#5482) Native is already delayed by a frame because it calls `handle_viewport_output` -> `egui_winit::process_viewport_commands` after drawing. On web however, we process input including viewport commands separately from drawing. This adds an arbitrary frame delay mechanism for web and then uses this with 1 frame delay always --- crates/eframe/src/web/app_runner.rs | 28 ++++++++++++++++++++++------ crates/egui/src/viewport.rs | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index d8abd3d4bd7..13ad762874a 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,5 +1,4 @@ use egui::{TexturesDelta, UserData, ViewportCommand}; -use std::mem; use crate::{epi, App}; @@ -17,8 +16,9 @@ pub struct AppRunner { last_save_time: f64, pub(crate) text_agent: TextAgent, - // If not empty, the painter should capture the next frame - screenshot_commands: Vec, + // If not empty, the painter should capture n frames from now. + // zero means capture the exact next frame. + screenshot_commands_with_frame_delay: Vec<(UserData, usize)>, // Output for the last run: textures_delta: TexturesDelta, @@ -114,7 +114,7 @@ impl AppRunner { needs_repaint, last_save_time: now_sec(), text_agent, - screenshot_commands: vec![], + screenshot_commands_with_frame_delay: vec![], textures_delta: Default::default(), clipped_primitives: None, }; @@ -236,7 +236,8 @@ impl AppRunner { for command in viewport_output.commands { match command { ViewportCommand::Screenshot(user_data) => { - self.screenshot_commands.push(user_data); + self.screenshot_commands_with_frame_delay + .push((user_data, 1)); } _ => { // TODO(emilk): handle some of the commands @@ -259,12 +260,27 @@ impl AppRunner { let clipped_primitives = std::mem::take(&mut self.clipped_primitives); if let Some(clipped_primitives) = clipped_primitives { + let mut screenshot_commands = vec![]; + self.screenshot_commands_with_frame_delay + .retain_mut(|(user_data, frame_delay)| { + if *frame_delay == 0 { + screenshot_commands.push(user_data.clone()); + false + } else { + *frame_delay -= 1; + true + } + }); + if !self.screenshot_commands_with_frame_delay.is_empty() { + self.egui_ctx().request_repaint(); + } + if let Err(err) = self.painter.paint_and_update_textures( self.app.clear_color(&self.egui_ctx.style().visuals), &clipped_primitives, self.egui_ctx.pixels_per_point(), &textures_delta, - mem::take(&mut self.screenshot_commands), + screenshot_commands, ) { log::error!("Failed to paint: {}", super::string_from_js_value(&err)); } diff --git a/crates/egui/src/viewport.rs b/crates/egui/src/viewport.rs index adafeca43a8..91cd12e2b60 100644 --- a/crates/egui/src/viewport.rs +++ b/crates/egui/src/viewport.rs @@ -1056,7 +1056,7 @@ pub enum ViewportCommand { /// Enable mouse pass-through: mouse clicks pass through the window, used for non-interactable overlays. MousePassthrough(bool), - /// Take a screenshot. + /// Take a screenshot of the next frame after this. /// /// The results are returned in [`crate::Event::Screenshot`]. Screenshot(crate::UserData), From 0823a36952a6791a0e28e6e8eac4f35639ab3091 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 15:07:14 +0100 Subject: [PATCH 20/34] Fix: ui.new_child should now respect 'disabled' (#5483) * Closes https://github.com/emilk/egui/issues/5475 --- crates/egui/src/ui.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/egui/src/ui.rs b/crates/egui/src/ui.rs index 786b93d53c4..6cc474e141b 100644 --- a/crates/egui/src/ui.rs +++ b/crates/egui/src/ui.rs @@ -301,7 +301,7 @@ impl Ui { min_rect: placer.min_rect(), max_rect: placer.max_rect(), }; - let child_ui = Ui { + let mut child_ui = Ui { id: stable_id, unique_id, next_auto_id_salt, @@ -316,6 +316,10 @@ impl Ui { min_rect_already_remembered: false, }; + if disabled { + child_ui.disable(); + } + // Register in the widget stack early, to ensure we are behind all widgets we contain: let start_rect = Rect::NOTHING; // This will be overwritten when `remember_min_rect` is called child_ui.ctx().create_widget( From 450c6242e4c35fc2a946fbf5c516153892e533c0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 15:14:50 +0100 Subject: [PATCH 21/34] Improve error message in ColorImage::region --- crates/epaint/src/image.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/epaint/src/image.rs b/crates/epaint/src/image.rs index 286c09ad765..9a204a12199 100644 --- a/crates/epaint/src/image.rs +++ b/crates/epaint/src/image.rs @@ -154,8 +154,10 @@ impl ColorImage { let max_x = (region.max.x * pixels_per_point) as usize; let min_y = (region.min.y * pixels_per_point) as usize; let max_y = (region.max.y * pixels_per_point) as usize; - assert!(min_x <= max_x); - assert!(min_y <= max_y); + assert!( + min_x <= max_x && min_y <= max_y, + "Screenshot region is invalid: {region:?}" + ); let width = max_x - min_x; let height = max_y - min_y; let mut output = Vec::with_capacity(width * height); From e8029178b6ca21a6fde6a738ba1d98def1da50d0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 16:28:15 +0100 Subject: [PATCH 22/34] Reduce aliasing when painting thin box outlines (#5484) * Part of https://github.com/emilk/egui/issues/5164 --- crates/epaint/src/tessellator.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 9821e4021e1..0594c8a2aa2 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1277,6 +1277,11 @@ impl Tessellator { ((point * self.pixels_per_point - 0.5).round() + 0.5) / self.pixels_per_point } + #[inline(always)] + pub fn round_pos_to_pixel(&self, pos: Pos2) -> Pos2 { + pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) + } + #[inline(always)] pub fn round_pos_to_pixel_center(&self, pos: Pos2) -> Pos2 { pos2( @@ -1702,6 +1707,20 @@ impl Tessellator { self.tessellate_line(line, stroke, out); // …and forth } } else { + let rect = if !stroke.is_empty() && stroke.width < self.feathering { + // Very thin rectangle strokes create extreme aliasing when they move around. + // We can fix that by rounding the rectangle corners to pixel centers. + // TODO(#5164): maybe do this for all shapes and stroke sizes + // TODO(emilk): since we use StrokeKind::Outside, we should probably round the + // corners after offsetting them with half the stroke width (see `translate_stroke_point`). + Rect { + min: self.round_pos_to_pixel_center(rect.min), + max: self.round_pos_to_pixel_center(rect.max), + } + } else { + rect + }; + let path = &mut self.scratchpad_path; path.clear(); path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding); From 045ed41efcd20ef6f24882a0bce32b3738c50087 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 16:54:18 +0100 Subject: [PATCH 23/34] Fix zero-width strokes still affecting the feathering color of boxes (#5485) --- crates/epaint/src/stroke.rs | 13 +++++++++---- crates/epaint/src/tessellator.rs | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 399a602c259..63412cdc013 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -161,10 +161,15 @@ where impl From for PathStroke { fn from(value: Stroke) -> Self { - Self { - width: value.width, - color: ColorMode::Solid(value.color), - kind: StrokeKind::default(), + if value.is_empty() { + // Important, since we use the stroke color when doing feathering of the fill! + Self::NONE + } else { + Self { + width: value.width, + color: ColorMode::Solid(value.color), + kind: StrokeKind::default(), + } } } } diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index 0594c8a2aa2..ebd22f55303 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -502,6 +502,8 @@ impl Path { /// Calling this may reverse the vertices in the path if they are wrong winding order. /// /// The preferred winding order is clockwise. + /// + /// The stroke colors is used for color-correct feathering. pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) { fill_closed_path(feathering, &mut self.0, color, stroke, out); } @@ -918,7 +920,7 @@ fn stroke_path( ) { let n = path.len() as u32; - if stroke.width <= 0.0 || stroke.color == ColorMode::TRANSPARENT || n < 2 { + if stroke.is_empty() || n < 2 { return; } From 0fb340fe89e9476b2fec15c2eff1405e51e5c04e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 17:10:39 +0100 Subject: [PATCH 24/34] Use released version of kittest --- Cargo.lock | 3 ++- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d43ee086ff..cecd1ec2d26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2340,7 +2340,8 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kittest" version = "0.1.0" -source = "git+https://github.com/rerun-io/kittest?branch=main#06e01f17fed36a997e1541f37b2d47e3771d7533" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f659954571a3c132356bd15c25f0dcf14d270a28ec5c58797adc2f432831bed5" dependencies = [ "accesskit", "accesskit_consumer", diff --git a/Cargo.toml b/Cargo.toml index 5c00189d068..31faaba6128 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,12 +82,12 @@ glutin = { version = "0.32.0", default-features = false } glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25", default-features = false } -kittest = { git = "https://github.com/rerun-io/kittest", version = "0.1", branch = "main" } +kittest = { version = "0.1" } log = { version = "0.4", features = ["std"] } nohash-hasher = "0.2" parking_lot = "0.12" pollster = "0.4" -profiling = {version = "1.0", default-features = false } +profiling = { version = "1.0", default-features = false } puffin = "0.19" puffin_http = "0.16" raw-window-handle = "0.6.0" From 320377e3ca63a298e9485cf79559fb150cffa635 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 17:45:35 +0100 Subject: [PATCH 25/34] Release 0.30 - egui_kittest and modals (#5487) --- CHANGELOG.md | 88 ++++++++++++++++++++++++ Cargo.lock | 30 ++++---- Cargo.toml | 26 +++---- RELEASES.md | 3 +- crates/ecolor/CHANGELOG.md | 5 ++ crates/eframe/CHANGELOG.md | 17 +++++ crates/egui-wgpu/CHANGELOG.md | 8 +++ crates/egui-winit/CHANGELOG.md | 5 ++ crates/egui_extras/CHANGELOG.md | 6 ++ crates/egui_glow/CHANGELOG.md | 4 ++ crates/egui_kittest/CHANGELOG.md | 4 ++ crates/epaint/CHANGELOG.md | 7 ++ crates/epaint_default_fonts/CHANGELOG.md | 4 ++ 13 files changed, 177 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17a2b7a9585..4642da637f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,94 @@ 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.30.0 - 2024-12-16 - Modals and better layer support + +### ✨ Highlights +* Add `Modal`, a popup that blocks input to the rest of the application ([#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin)) +* Improved support for transform layers ([#5465](https://github.com/emilk/egui/pull/5465), [#5468](https://github.com/emilk/egui/pull/5468), [#5429](https://github.com/emilk/egui/pull/5429)) + +#### `egui_kittest` +This release welcomes a new crate to the family: [egui_kittest](https://github.com/emilk/egui/tree/master/crates/egui_kittest). +`egui_kittest` is a testing framework for egui, allowing you to test both automation (simulated clicks and other events), +and also do screenshot testing (useful for regression tests). +`egui_kittest` is built using [`kittest`](https://github.com/rerun-io/kittest), which is a general GUI testing framework that aims to work with any Rust GUI (not just egui!). +`kittest` uses the accessibility library [`AccessKit`](https://github.com/AccessKit/accesskit/) for automatation and to query the widget tree. + +`kittest` and `egui_kittest` are written by [@lucasmerlin](https://github.com/lucasmerlin). + +Here's a quick example of how to use `egui_kittest` to test a checkbox: + +```rust +use egui::accesskit::Toggled; +use egui_kittest::{Harness, kittest::Queryable}; + +fn main() { + let mut checked = false; + let app = |ui: &mut egui::Ui| { + ui.checkbox(&mut checked, "Check me!"); + }; + + let mut harness = egui_kittest::Harness::new_ui(app); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::False)); + checkbox.click(); + + harness.run(); + + let checkbox = harness.get_by_label("Check me!"); + assert_eq!(checkbox.toggled(), Some(Toggled::True)); + + // You can even render the ui and do image snapshot tests + #[cfg(all(feature = "wgpu", feature = "snapshot"))] + harness.wgpu_snapshot("readme_example"); +} +``` + +### ⭐ Added +* Add `Modal` and `Memory::set_modal_layer` [#5358](https://github.com/emilk/egui/pull/5358) by [@lucasmerlin](https://github.com/lucasmerlin) +* Add `UiBuilder::layer_id` and remove `layer_id` from `Ui::new` [#5195](https://github.com/emilk/egui/pull/5195) by [@emilk](https://github.com/emilk) +* Allow easier setting of background color for `TextEdit` [#5203](https://github.com/emilk/egui/pull/5203) by [@bircni](https://github.com/bircni) +* Set `Response::intrinsic_size` for `TextEdit` [#5266](https://github.com/emilk/egui/pull/5266) by [@lucasmerlin](https://github.com/lucasmerlin) +* Expose center position in `MultiTouchInfo` [#5247](https://github.com/emilk/egui/pull/5247) by [@lucasmerlin](https://github.com/lucasmerlin) +* `Context::add_font` [#5228](https://github.com/emilk/egui/pull/5228) by [@frederik-uni](https://github.com/frederik-uni) +* Impl from `Box` for `WidgetText`, `RichText` [#5309](https://github.com/emilk/egui/pull/5309) by [@dimtpap](https://github.com/dimtpap) +* Add `Window::scroll_bar_visibility` [#5231](https://github.com/emilk/egui/pull/5231) by [@Zeenobit](https://github.com/Zeenobit) +* Add `ComboBox::close_behavior` [#5305](https://github.com/emilk/egui/pull/5305) by [@avalsch](https://github.com/avalsch) +* Add `painter.line()` [#5291](https://github.com/emilk/egui/pull/5291) by [@bircni](https://github.com/bircni) +* Allow attaching custom user data to a screenshot command [#5416](https://github.com/emilk/egui/pull/5416) by [@emilk](https://github.com/emilk) +* Add `Button::image_tint_follows_text_color` [#5430](https://github.com/emilk/egui/pull/5430) by [@emilk](https://github.com/emilk) +* Consume escape keystroke when bailing out from a drag operation [#5433](https://github.com/emilk/egui/pull/5433) by [@abey79](https://github.com/abey79) +* Add `Context::layer_transform_to_global` & `layer_transform_from_global` [#5465](https://github.com/emilk/egui/pull/5465) by [@emilk](https://github.com/emilk) + +### πŸ”§ Changed +* Update MSRV to Rust 1.80 [#5421](https://github.com/emilk/egui/pull/5421), [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk) +* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) +* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) +* Move `egui::util::cache` to `egui::cache`; add `FramePublisher` [#5426](https://github.com/emilk/egui/pull/5426) by [@emilk](https://github.com/emilk) +* Remove `Order::PanelResizeLine` [#5455](https://github.com/emilk/egui/pull/5455) by [@emilk](https://github.com/emilk) +* Drag-and-drop: keep cursor set by user, if any [#5467](https://github.com/emilk/egui/pull/5467) by [@abey79](https://github.com/abey79) +* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik) +* Improve hit-test of thin widgets, and widgets across layers [#5468](https://github.com/emilk/egui/pull/5468) by [@emilk](https://github.com/emilk) + +### πŸ› Fixed +* Update `ScrollArea` drag velocity when drag stopped [#5175](https://github.com/emilk/egui/pull/5175) by [@valadaptive](https://github.com/valadaptive) +* Fix bug causing wrong-fire of `ViewportCommand::Visible` [#5244](https://github.com/emilk/egui/pull/5244) by [@rustbasic](https://github.com/rustbasic) +* Fix: `Ui::new_child` does not consider the `sizing_pass` field of `UiBuilder` [#5262](https://github.com/emilk/egui/pull/5262) by [@zhatuokun](https://github.com/zhatuokun) +* Fix Ctrl+Shift+Z redo shortcut [#5258](https://github.com/emilk/egui/pull/5258) by [@YgorSouza](https://github.com/YgorSouza) +* Fix: `Window::default_pos` does not work [#5315](https://github.com/emilk/egui/pull/5315) by [@rustbasic](https://github.com/rustbasic) +* Fix: `Sides` did not apply the layout position correctly [#5303](https://github.com/emilk/egui/pull/5303) by [@zhatuokun](https://github.com/zhatuokun) +* Respect `Style::override_font_id` in `RichText` [#5310](https://github.com/emilk/egui/pull/5310) by [@MStarha](https://github.com/MStarha) +* Fix disabled widgets "eating" focus [#5370](https://github.com/emilk/egui/pull/5370) by [@lucasmerlin](https://github.com/lucasmerlin) +* Fix cursor clipping in `TextEdit` inside a `ScrollArea` [#3660](https://github.com/emilk/egui/pull/3660) by [@juancampa](https://github.com/juancampa) +* Make text cursor always appear on click [#5420](https://github.com/emilk/egui/pull/5420) by [@juancampa](https://github.com/juancampa) +* Fix `on_hover_text_at_pointer` for transformed layers [#5429](https://github.com/emilk/egui/pull/5429) by [@emilk](https://github.com/emilk) +* Fix: don't interact with `Area` outside its `constrain_rect` [#5459](https://github.com/emilk/egui/pull/5459) by [@MScottMcBee](https://github.com/MScottMcBee) +* Fix broken images on egui.rs (move from git lfs to normal git) [#5480](https://github.com/emilk/egui/pull/5480) by [@emilk](https://github.com/emilk) +* Fix: `ui.new_child` should now respect `disabled` [#5483](https://github.com/emilk/egui/pull/5483) by [@emilk](https://github.com/emilk) +* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk) + + ## 0.29.1 - 2024-10-01 - Bug fixes * Remove debug-assert triggered by `with_layer_id/dnd_drag_source` [#5191](https://github.com/emilk/egui/pull/5191) by [@emilk](https://github.com/emilk) * Fix id clash in `Ui::response` [#5192](https://github.com/emilk/egui/pull/5192) by [@emilk](https://github.com/emilk) diff --git a/Cargo.lock b/Cargo.lock index cecd1ec2d26..ae49b06c5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,7 +1200,7 @@ checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" [[package]] name = "ecolor" -version = "0.29.1" +version = "0.30.0" dependencies = [ "bytemuck", "cint", @@ -1212,7 +1212,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.29.1" +version = "0.30.0" dependencies = [ "accesskit", "ahash", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1288,7 +1288,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.29.1" +version = "0.30.0" dependencies = [ "accesskit_winit", "ahash", @@ -1308,7 +1308,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.29.1" +version = "0.30.0" dependencies = [ "bytemuck", "chrono", @@ -1334,7 +1334,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.29.1" +version = "0.30.0" dependencies = [ "chrono", "criterion", @@ -1350,7 +1350,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "chrono", @@ -1369,7 +1369,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ahash", "bytemuck", @@ -1389,7 +1389,7 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.29.1" +version = "0.30.0" dependencies = [ "dify", "document-features", @@ -1424,7 +1424,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" -version = "0.29.1" +version = "0.30.0" dependencies = [ "bytemuck", "document-features", @@ -1515,7 +1515,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.29.1" +version = "0.30.0" dependencies = [ "ab_glyph", "ahash", @@ -1536,7 +1536,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.29.1" +version = "0.30.0" [[package]] name = "equivalent" @@ -3067,7 +3067,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "popups" -version = "0.29.1" +version = "0.30.0" dependencies = [ "eframe", "env_logger", @@ -4989,7 +4989,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.29.1" +version = "0.30.0" [[package]] name = "yaml-rust" diff --git a/Cargo.toml b/Cargo.toml index 31faaba6128..10172b66252 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ members = [ edition = "2021" license = "MIT OR Apache-2.0" rust-version = "1.80" -version = "0.29.1" +version = "0.30.0" [profile.release] @@ -55,18 +55,18 @@ opt-level = 2 [workspace.dependencies] -emath = { version = "0.29.1", path = "crates/emath", default-features = false } -ecolor = { version = "0.29.1", path = "crates/ecolor", default-features = false } -epaint = { version = "0.29.1", path = "crates/epaint", default-features = false } -epaint_default_fonts = { version = "0.29.1", path = "crates/epaint_default_fonts" } -egui = { version = "0.29.1", path = "crates/egui", default-features = false } -egui-winit = { version = "0.29.1", path = "crates/egui-winit", default-features = false } -egui_extras = { version = "0.29.1", path = "crates/egui_extras", default-features = false } -egui-wgpu = { version = "0.29.1", path = "crates/egui-wgpu", default-features = false } -egui_demo_lib = { version = "0.29.1", path = "crates/egui_demo_lib", default-features = false } -egui_glow = { version = "0.29.1", path = "crates/egui_glow", default-features = false } -egui_kittest = { version = "0.29.1", path = "crates/egui_kittest", default-features = false } -eframe = { version = "0.29.1", path = "crates/eframe", default-features = false } +emath = { version = "0.30.0", path = "crates/emath", default-features = false } +ecolor = { version = "0.30.0", path = "crates/ecolor", default-features = false } +epaint = { version = "0.30.0", path = "crates/epaint", default-features = false } +epaint_default_fonts = { version = "0.30.0", path = "crates/epaint_default_fonts" } +egui = { version = "0.30.0", path = "crates/egui", default-features = false } +egui-winit = { version = "0.30.0", path = "crates/egui-winit", default-features = false } +egui_extras = { version = "0.30.0", path = "crates/egui_extras", default-features = false } +egui-wgpu = { version = "0.30.0", path = "crates/egui-wgpu", default-features = false } +egui_demo_lib = { version = "0.30.0", path = "crates/egui_demo_lib", default-features = false } +egui_glow = { version = "0.30.0", path = "crates/egui_glow", default-features = false } +egui_kittest = { version = "0.30.0", path = "crates/egui_kittest", default-features = false } +eframe = { version = "0.30.0", path = "crates/eframe", default-features = false } ahash = { version = "0.8.11", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead diff --git a/RELEASES.md b/RELEASES.md index 677f31fddf9..b724806fbd3 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -53,8 +53,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. * [ ] run `scripts/generate_example_screenshots.sh` if needed * [ ] write a short release note that fits in a tweet * [ ] record gif for `CHANGELOG.md` release note (and later twitter post) -* [ ] update changelogs using `scripts/generate_changelog.py --write` - - For major releases, always diff to the latest MAJOR release, e.g. `--commit-range 0.29.0..HEAD` +* [ ] update changelogs using `scripts/generate_changelog.py --version 0.x.0 --write` * [ ] bump version numbers in workspace `Cargo.toml` ## Actual release diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index ec42258baf8..37b5555b60a 100644 --- a/crates/ecolor/CHANGELOG.md +++ b/crates/ecolor/CHANGELOG.md @@ -6,6 +6,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.30.0 - 2024-12-16 +* Use boxed slice for lookup table to avoid stack overflow [#5212](https://github.com/emilk/egui/pull/5212) by [@YgorSouza](https://github.com/YgorSouza) +* Add `Color32::mul` [#5437](https://github.com/emilk/egui/pull/5437) by [@emilk](https://github.com/emilk) + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 65b5a731985..35908e33d79 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -7,6 +7,23 @@ 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.30.0 - 2024-12-16 - Android support +### ⭐ Added +* Support `ViewportCommand::Screenshot` on web [#5438](https://github.com/emilk/egui/pull/5438) by [@lucasmerlin](https://github.com/lucasmerlin) + +### πŸ”§ Changed +* Android support [#5318](https://github.com/emilk/egui/pull/5318) by [@parasyte](https://github.com/parasyte) +* Update MSRV to 1.80 [#5457](https://github.com/emilk/egui/pull/5457) by [@emilk](https://github.com/emilk) +* Use `profiling` crate to support more profiler backends [#5150](https://github.com/emilk/egui/pull/5150) by [@teddemunnik](https://github.com/teddemunnik) +* Update glow to 0.16 [#5395](https://github.com/emilk/egui/pull/5395) by [@sagudev](https://github.com/sagudev) +* Forward `x11` and `wayland` features to `glutin` [#5391](https://github.com/emilk/egui/pull/5391) by [@e00E](https://github.com/e00E) + +### πŸ› Fixed +* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) +* Prevent panic when copying text outside of a secure context [#5326](https://github.com/emilk/egui/pull/5326) by [@YgorSouza](https://github.com/YgorSouza) +* Fix accidental change of `FallbackEgl` to `PreferEgl` [#5408](https://github.com/emilk/egui/pull/5408) by [@e00E](https://github.com/e00E) + + ## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11 * Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk) diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index a25b2a21548..d92e867a77e 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,14 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Fix docs.rs build [#5204](https://github.com/emilk/egui/pull/5204) by [@lucasmerlin](https://github.com/lucasmerlin) +* Free textures after submitting queue instead of before with wgpu renderer [#5226](https://github.com/emilk/egui/pull/5226) by [@Rusty-Cube](https://github.com/Rusty-Cube) +* Add option to initialize with existing wgpu instance/adapter/device/queue [#5319](https://github.com/emilk/egui/pull/5319) by [@ArthurBrussee](https://github.com/ArthurBrussee) +* Updare to `wgpu` 23.0.0 and `wasm-bindgen` to 0.2.95 [#5330](https://github.com/emilk/egui/pull/5330) by [@torokati44](https://github.com/torokati44) +* Support wgpu-tracing with same mechanism as wgpu examples [#5450](https://github.com/emilk/egui/pull/5450) by [@EriKWDev](https://github.com/EriKWDev) + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index cf9def985f7..d164464208d 100644 --- a/crates/egui-winit/CHANGELOG.md +++ b/crates/egui-winit/CHANGELOG.md @@ -5,6 +5,11 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* iOS: Support putting UI next to the dynamic island [#5211](https://github.com/emilk/egui/pull/5211) by [@frederik-uni](https://github.com/frederik-uni) +* Remove implicit `accesskit_winit` feature [#5316](https://github.com/emilk/egui/pull/5316) by [@waywardmonkeys](https://github.com/waywardmonkeys) + + ## 0.29.1 - 2024-10-01 - Fix backspace/arrow keys on X11 * Linux: Disable IME to fix backspace/arrow keys [#5188](https://github.com/emilk/egui/pull/5188) by [@emilk](https://github.com/emilk) diff --git a/crates/egui_extras/CHANGELOG.md b/crates/egui_extras/CHANGELOG.md index d0f5aa868bf..7617b1863b0 100644 --- a/crates/egui_extras/CHANGELOG.md +++ b/crates/egui_extras/CHANGELOG.md @@ -5,6 +5,12 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Use `Table::id_salt` on `ScrollArea` [#5282](https://github.com/emilk/egui/pull/5282) by [@jwhear](https://github.com/jwhear) +* Use proper `image` crate URI and MIME support detection [#5324](https://github.com/emilk/egui/pull/5324) by [@xangelix](https://github.com/xangelix) +* Support loading images with weird urls and improve error message [#5431](https://github.com/emilk/egui/pull/5431) by [@lucasmerlin](https://github.com/lucasmerlin) + + ## 0.29.1 - 2024-10-01 - Fix table interaction * Bug fix: click anywhere on a `Table` row to select it [#5193](https://github.com/emilk/egui/pull/5193) by [@emilk](https://github.com/emilk) diff --git a/crates/egui_glow/CHANGELOG.md b/crates/egui_glow/CHANGELOG.md index 0ebbad6d95c..dc3d6dfda46 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.30.0 - 2024-12-16 - Initial relrease +* Support for egui 0.30.0 +* Automate clicks and text input +* Automatic screenshot testing with wgpu diff --git a/crates/epaint/CHANGELOG.md b/crates/epaint/CHANGELOG.md index b044cddee2d..742cf0acd78 100644 --- a/crates/epaint/CHANGELOG.md +++ b/crates/epaint/CHANGELOG.md @@ -5,6 +5,13 @@ This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. +## 0.30.0 - 2024-12-16 +* Expand max font atlas size from 8k to 16k [#5257](https://github.com/emilk/egui/pull/5257) by [@rustbasic](https://github.com/rustbasic) +* Put font data into `Arc` to reduce memory consumption [#5276](https://github.com/emilk/egui/pull/5276) by [@StarStarJ](https://github.com/StarStarJ) +* Reduce aliasing when painting thin box outlines [#5484](https://github.com/emilk/egui/pull/5484) by [@emilk](https://github.com/emilk) +* Fix zero-width strokes still affecting the feathering color of boxes [#5485](https://github.com/emilk/egui/pull/5485) by [@emilk](https://github.com/emilk) + + ## 0.29.1 - 2024-10-01 Nothing new diff --git a/crates/epaint_default_fonts/CHANGELOG.md b/crates/epaint_default_fonts/CHANGELOG.md index 3e8e6959b3c..42cd89ba51a 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.30.0 - 2024-12-16 +Nothing new + + ## 0.29.1 - 2024-10-01 Nothing new From 69dbb00087fab060d4ee870740a1fbcfbecbd664 Mon Sep 17 00:00:00 2001 From: lucasmerlin Date: Mon, 16 Dec 2024 18:02:21 +0100 Subject: [PATCH 26/34] Simplify kittest readme example (#5486) Updates the example using the new_ui function, and call fit_contents - [x] I have followed the instructions in the PR template --- crates/egui_kittest/README.md | 18 ++++++++---------- .../tests/snapshots/readme_example.png | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 2ffda785c05..c124fac3a62 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -4,21 +4,16 @@ Ui testing library for egui, based on [kittest](https://github.com/rerun-io/kitt ## Example usage ```rust -use egui::accesskit::{Role, Toggled}; -use egui::{CentralPanel, Context, TextEdit, Vec2}; -use egui_kittest::Harness; -use kittest::Queryable; -use std::cell::RefCell; +use egui::accesskit::Toggled; +use egui_kittest::{Harness, kittest::Queryable}; fn main() { let mut checked = false; - let app = |ctx: &Context| { - CentralPanel::default().show(ctx, |ui| { - ui.checkbox(&mut checked, "Check me!"); - }); + let app = |ui: &mut egui::Ui| { + ui.checkbox(&mut checked, "Check me!"); }; - let mut harness = Harness::builder().with_size(egui::Vec2::new(200.0, 100.0)).build(app); + let mut harness = Harness::new_ui(app); let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.toggled(), Some(Toggled::False)); @@ -28,6 +23,9 @@ fn main() { let checkbox = harness.get_by_label("Check me!"); assert_eq!(checkbox.toggled(), Some(Toggled::True)); + + // Shrink the window size to the smallest size possible + harness.fit_contents(); // You can even render the ui and do image snapshot tests #[cfg(all(feature = "wgpu", feature = "snapshot"))] diff --git a/crates/egui_kittest/tests/snapshots/readme_example.png b/crates/egui_kittest/tests/snapshots/readme_example.png index 66b21e7f4bf..ef0774162da 100644 --- a/crates/egui_kittest/tests/snapshots/readme_example.png +++ b/crates/egui_kittest/tests/snapshots/readme_example.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36c1b432140456ea5cbb687076b1c910aea8b31affd33a0ece22218f60af2d6e -size 2296 +oid sha256:31bd906040fcc356c19dc36036fbfd2a28dfcef54c7a073f584f4a9abddbdb4c +size 1699 From eb403655cec180101282f38e352124b8bf72bb54 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 17:56:56 +0100 Subject: [PATCH 27/34] Move egui tests to avoid cyclic dependency --- Cargo.lock | 1 - crates/egui/Cargo.toml | 4 ---- crates/{egui => egui_kittest}/tests/accesskit.rs | 7 ++++--- crates/{egui => egui_kittest}/tests/regression_tests.rs | 3 +-- 4 files changed, 5 insertions(+), 10 deletions(-) rename crates/{egui => egui_kittest}/tests/accesskit.rs (97%) rename crates/{egui => egui_kittest}/tests/regression_tests.rs (91%) diff --git a/Cargo.lock b/Cargo.lock index ae49b06c5f8..b5a5141cc3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1258,7 +1258,6 @@ dependencies = [ "ahash", "backtrace", "document-features", - "egui_kittest", "emath", "epaint", "log", diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index a9408da2f6d..ce7999dd1f4 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -94,7 +94,3 @@ document-features = { workspace = true, optional = true } log = { workspace = true, optional = true } ron = { workspace = true, optional = true } serde = { workspace = true, optional = true, features = ["derive", "rc"] } - - -[dev-dependencies] -egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } diff --git a/crates/egui/tests/accesskit.rs b/crates/egui_kittest/tests/accesskit.rs similarity index 97% rename from crates/egui/tests/accesskit.rs rename to crates/egui_kittest/tests/accesskit.rs index 83efeab3f37..02afddefc80 100644 --- a/crates/egui/tests/accesskit.rs +++ b/crates/egui_kittest/tests/accesskit.rs @@ -1,8 +1,9 @@ //! Tests the accesskit accessibility output of egui. -#![cfg(feature = "accesskit")] -use accesskit::{NodeId, Role, TreeUpdate}; -use egui::{CentralPanel, Context, RawInput, Window}; +use egui::{ + accesskit::{NodeId, Role, TreeUpdate}, + CentralPanel, Context, RawInput, Window, +}; /// Baseline test that asserts there are no spurious nodes in the /// accesskit output when the ui is empty. diff --git a/crates/egui/tests/regression_tests.rs b/crates/egui_kittest/tests/regression_tests.rs similarity index 91% rename from crates/egui/tests/regression_tests.rs rename to crates/egui_kittest/tests/regression_tests.rs index f54cf1ffcf6..9493d5443f4 100644 --- a/crates/egui/tests/regression_tests.rs +++ b/crates/egui_kittest/tests/regression_tests.rs @@ -1,6 +1,5 @@ use egui::Button; -use egui_kittest::kittest::Queryable; -use egui_kittest::Harness; +use egui_kittest::{kittest::Queryable, Harness}; #[test] pub fn focus_should_skip_over_disabled_buttons() { From 5b2b8cfb3432828e45a0408fa34f872984065852 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 18:01:33 +0100 Subject: [PATCH 28/34] Remove cylic dependency of egui_kittest on itself --- Cargo.lock | 1 - crates/egui_kittest/Cargo.toml | 10 ++++++---- crates/egui_kittest/tests/tests.rs | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b5a5141cc3b..1584469837a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1394,7 +1394,6 @@ dependencies = [ "document-features", "egui", "egui-wgpu", - "egui_kittest", "image", "kittest", "pollster 0.4.0", diff --git a/crates/egui_kittest/Cargo.toml b/crates/egui_kittest/Cargo.toml index f9f98198c82..d93f0348368 100644 --- a/crates/egui_kittest/Cargo.toml +++ b/crates/egui_kittest/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "egui_kittest" version.workspace = true -authors = ["Lucas Meurer ", "Emil Ernerfeldt "] +authors = [ + "Lucas Meurer ", + "Emil Ernerfeldt ", +] description = "Testing library for egui based on kittest and AccessKit" edition.workspace = true rust-version.workspace = true @@ -39,10 +42,9 @@ dify = { workspace = true, optional = true } document-features = { workspace = true, optional = true } [dev-dependencies] -egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } -wgpu = { workspace = true, features = ["metal"] } -image = { workspace = true, features = ["png"] } egui = { workspace = true, features = ["default_fonts"] } +image = { workspace = true, features = ["png"] } +wgpu = { workspace = true, features = ["metal"] } [lints] workspace = true diff --git a/crates/egui_kittest/tests/tests.rs b/crates/egui_kittest/tests/tests.rs index 4978cbeb775..6799b9a3567 100644 --- a/crates/egui_kittest/tests/tests.rs +++ b/crates/egui_kittest/tests/tests.rs @@ -10,5 +10,6 @@ fn test_shrink() { harness.fit_contents(); + #[cfg(all(feature = "snapshot", feature = "wgpu"))] harness.wgpu_snapshot("test_shrink"); } From 629f64551a2dbddef9861b3f5f961c82495f053e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 18:05:24 +0100 Subject: [PATCH 29/34] Remove cyclic dependency of egui_demo_lib on itself --- Cargo.lock | 1 - crates/egui_demo_lib/Cargo.toml | 3 --- crates/egui_demo_lib/src/demo/widget_gallery.rs | 1 + 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1584469837a..5073cd8b64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1339,7 +1339,6 @@ dependencies = [ "criterion", "document-features", "egui", - "egui_demo_lib", "egui_extras", "egui_kittest", "serde", diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index f8f1f47f686..b494e18a99b 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -55,9 +55,6 @@ serde = { workspace = true, optional = true } [dev-dependencies] -# when running tests we always want to use the `chrono` feature -egui_demo_lib = { workspace = true, features = ["chrono"] } - criterion.workspace = true egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] } wgpu = { workspace = true, features = ["metal"] } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index c579576b85b..b69d0f1c8d2 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -286,6 +286,7 @@ fn doc_link_label_with_crate<'a>( } } +#[cfg(feature = "chrono")] #[cfg(test)] mod tests { use super::*; From d864655018fcd73f2df629e5df4df04374b89a4b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 18:09:55 +0100 Subject: [PATCH 30/34] Reorder crate publish steps --- RELEASES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index b724806fbd3..79bf23bc614 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -78,12 +78,12 @@ I usually do this all on the `master` branch, but doing it in a release branch i (cd crates/epaint && cargo publish --quiet) && echo "βœ… epaint" (cd crates/egui && cargo publish --quiet) && echo "βœ… egui" (cd crates/egui-winit && cargo publish --quiet) && echo "βœ… egui-winit" -(cd crates/egui_extras && cargo publish --quiet) && echo "βœ… egui_extras" (cd crates/egui-wgpu && cargo publish --quiet) && echo "βœ… egui-wgpu" +(cd crates/egui_kittest && cargo publish --quiet) && echo "βœ… egui_kittest" +(cd crates/egui_extras && cargo publish --quiet) && echo "βœ… egui_extras" (cd crates/egui_demo_lib && cargo publish --quiet) && echo "βœ… egui_demo_lib" (cd crates/egui_glow && cargo publish --quiet) && echo "βœ… egui_glow" (cd crates/eframe && cargo publish --quiet) && echo "βœ… eframe" -(cd crates/egui_kittest && cargo publish --quiet) && echo "βœ… egui_kittest" ``` ## Announcements From adfc0bebfc6be14cee2068dee758412a5e0648dc Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 16 Dec 2024 19:25:21 +0100 Subject: [PATCH 31/34] Revert "forward x11 and wayland features to glutin" (#5391) (#5488) * Reverts https://github.com/emilk/egui/pull/5391 Because it causes head-ache: https://github.com/emilk/eframe_template/actions/runs/12357896151/job/34487194281 --- Cargo.toml | 4 ++-- crates/eframe/Cargo.toml | 10 ++++++---- crates/egui_glow/Cargo.toml | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10172b66252..4077f9d7ed2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,8 +78,8 @@ criterion = { version = "0.5.1", default-features = false } dify = { version = "0.7", default-features = false } document-features = "0.2.10" glow = "0.16" -glutin = { version = "0.32.0", default-features = false } -glutin-winit = { version = "0.5.0", default-features = false } +glutin = "0.32.0" +glutin-winit = "0.5.0" home = "0.5.9" image = { version = "0.25", default-features = false } kittest = { version = "0.1" } diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index c07dc9d0d3a..16ba3765521 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -72,7 +72,7 @@ persistence = [ ] ## Enables wayland support and fixes clipboard issue. -wayland = ["egui-winit/wayland", "egui-wgpu?/wayland", "egui_glow?/wayland", "glutin?/wayland", "glutin-winit?/wayland"] +wayland = ["egui-winit/wayland", "egui-wgpu?/wayland", "egui_glow?/wayland"] ## Enable screen reader support (requires `ctx.options_mut(|o| o.screen_reader = true);`) on web. ## @@ -98,7 +98,7 @@ web_screen_reader = [ wgpu = ["dep:wgpu", "dep:egui-wgpu", "dep:pollster"] ## Enables compiling for x11. -x11 = ["egui-winit/x11", "egui-wgpu?/x11", "egui_glow?/x11", "glutin?/x11", "glutin?/glx", "glutin-winit?/x11", "glutin-winit?/glx"] +x11 = ["egui-winit/x11", "egui-wgpu?/x11", "egui_glow?/x11"] ## If set, eframe will look for the env-var `EFRAME_SCREENSHOT_TO` and write a screenshot to that location, and then quit. ## This is used to generate images for examples. @@ -142,8 +142,10 @@ egui-wgpu = { workspace = true, optional = true, features = [ ] } # if wgpu is used, use it with winit pollster = { workspace = true, optional = true } # needed for wgpu -glutin = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] } -glutin-winit = { workspace = true, optional = true, default-features = false, features = ["egl", "wgl"] } +# we can expose these to user so that they can select which backends they want to enable to avoid compiling useless deps. +# this can be done at the same time we expose x11/wayland features of winit crate. +glutin = { workspace = true, optional = true } +glutin-winit = { workspace = true, optional = true } home = { workspace = true, optional = true } wgpu = { workspace = true, optional = true, features = [ # Let's enable some backends so that users can use `eframe` out-of-the-box diff --git a/crates/egui_glow/Cargo.toml b/crates/egui_glow/Cargo.toml index 7a402d4067a..ef2d451dd2b 100644 --- a/crates/egui_glow/Cargo.toml +++ b/crates/egui_glow/Cargo.toml @@ -74,8 +74,8 @@ wasm-bindgen.workspace = true [dev-dependencies] -glutin = { workspace = true, default-features = true } # examples/pure_glow -glutin-winit = { workspace = true, default-features = true } +glutin.workspace = true # examples/pure_glow +glutin-winit.workspace = true [[example]] name = "pure_glow" From cfc341fabdbb65b0273b96e455c9d71fa79e7539 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 17 Dec 2024 09:36:03 +0100 Subject: [PATCH 32/34] Revert "Revert "forward x11 and wayland features to glutin" (#5391)" (#5490) * https://github.com/emilk/egui/pull/5391 * https://github.com/emilk/egui/pull/5488 * https://github.com/emilk/egui/pull/5490 --- Cargo.toml | 4 ++-- crates/eframe/CHANGELOG.md | 2 ++ crates/eframe/Cargo.toml | 34 +++++++++++++++++++++++++++------- crates/egui_glow/Cargo.toml | 4 ++-- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4077f9d7ed2..10172b66252 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,8 +78,8 @@ criterion = { version = "0.5.1", default-features = false } dify = { version = "0.7", default-features = false } document-features = "0.2.10" glow = "0.16" -glutin = "0.32.0" -glutin-winit = "0.5.0" +glutin = { version = "0.32.0", default-features = false } +glutin-winit = { version = "0.5.0", default-features = false } home = "0.5.9" image = { version = "0.25", default-features = false } kittest = { version = "0.1" } diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 35908e33d79..cbb6cf411ca 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -8,6 +8,8 @@ Changes since the last release can be found at Date: Tue, 17 Dec 2024 09:37:15 +0100 Subject: [PATCH 33/34] Use correct minimum version of `profiling` crate (#5494) We need profiling::function_scope which was introduced in 1.0.16, so this is the minimum required version * Closes * [x] I have followed the instructions in the PR template --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 10172b66252..b551f9720de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ log = { version = "0.4", features = ["std"] } nohash-hasher = "0.2" parking_lot = "0.12" pollster = "0.4" -profiling = { version = "1.0", default-features = false } +profiling = { version = "1.0.16", default-features = false } puffin = "0.19" puffin_http = "0.16" raw-window-handle = "0.6.0" From 27a5803dd338cf6e1b5d99da218cc2427108961b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?On=C3=A8?= <43485962+c-git@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:29:41 -0500 Subject: [PATCH 34/34] docs: remove "a" (#5499) Was a duplicate article in the sentence. Already has "the" * Closes * [ ] I have followed the instructions in the PR template --- crates/egui/src/util/id_type_map.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/egui/src/util/id_type_map.rs b/crates/egui/src/util/id_type_map.rs index 6e382f9917f..a7b8f18c9c3 100644 --- a/crates/egui/src/util/id_type_map.rs +++ b/crates/egui/src/util/id_type_map.rs @@ -308,7 +308,7 @@ fn from_ron_str(ron: &str) -> Option { use crate::Id; // TODO(emilk): make IdTypeMap generic over the key (`Id`), and make a library of IdTypeMap. -/// Stores values identified by an [`Id`] AND a the [`std::any::TypeId`] of the value. +/// Stores values identified by an [`Id`] AND the [`std::any::TypeId`] of the value. /// /// In other words, it maps `(Id, TypeId)` to any value you want. ///