diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d428fe71..c51aa466d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,57 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. +## 0.24.0 - 2023-11-23 - Multi-viewport + +### ✨ Highlights +You can now spawn multiple native windows on supported backends (e.g. `eframe`), using [the new `viewport` API](https://docs.rs/egui/latest/egui/viewport/index.html) ([#3172](https://github.com/emilk/egui/pull/3172)). + +You can easily zoom any egui app using Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser ([#3608](https://github.com/emilk/egui/pull/3608)). + +Scrollbars are now hidden by default until you hover the `ScrollArea` ([#3539](https://github.com/emilk/egui/pull/3539)). + +### ⭐ Added +* Multiple viewports/windows [#3172](https://github.com/emilk/egui/pull/3172) (thanks [@konkitoman](https://github.com/konkitoman)!) +* Introduce global `zoom_factor` [#3608](https://github.com/emilk/egui/pull/3608) +* Floating scroll bars [#3539](https://github.com/emilk/egui/pull/3539) +* Add redo support to `Undoer` [#3478](https://github.com/emilk/egui/pull/3478) (thanks [@LoganDark](https://github.com/LoganDark)!) +* Add `egui::Vec2b` [#3543](https://github.com/emilk/egui/pull/3543) +* Add max `Window` size & other size helpers [#3537](https://github.com/emilk/egui/pull/3537) (thanks [@arduano](https://github.com/arduano)!) +* Allow changing shape of slider handle [#3429](https://github.com/emilk/egui/pull/3429) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* `RawInput::viewports` contains a list of all viewports. Access the current one with `ctx.input(|i| i.viewport())` + +### 🔧 Changed +* Replace `Id::null()` with `Id::NULL` [#3544](https://github.com/emilk/egui/pull/3544) +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Update puffin to 0.18 [#3600](https://github.com/emilk/egui/pull/3600) + +### 🐛 Fixed +* Fix upside down slider in the vertical orientation [#3424](https://github.com/emilk/egui/pull/3424) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Make slider step account for range start [#3488](https://github.com/emilk/egui/pull/3488) (thanks [@YgorSouza](https://github.com/YgorSouza)!) +* Fix rounding of `ImageButton` [#3531](https://github.com/emilk/egui/pull/3531) (thanks [@chriscate](https://github.com/chriscate)!) +* Fix naming: `constraint_to` -> `constrain_to` [#3438](https://github.com/emilk/egui/pull/3438) (thanks [@rinde](https://github.com/rinde)!) +* Fix Shift+Tab behavior when no widget is focused [#3498](https://github.com/emilk/egui/pull/3498) (thanks [@DataTriny](https://github.com/DataTriny)!) +* Fix scroll not sticking when scrollbar is hidden [#3434](https://github.com/emilk/egui/pull/3434) (thanks [@LoganDark](https://github.com/LoganDark)!) +* Add `#[inline]` to all builder-pattern functions [#3557](https://github.com/emilk/egui/pull/3557) +* Properly reverse bool animation if value changes before it's finished [#3577](https://github.com/emilk/egui/pull/3577) (thanks [@YgorSouza](https://github.com/YgorSouza)!) + + +### ⚠️ BREAKING +* `egui::gui_zoom::zoom_with_keyboard_shortcuts` is gone, replaced with `Options::zoom_with_keyboard`, which is `true` by default +* `Spacing::scroll_bar_X` has been moved to `Spacing::scroll_bar.X` +* `Context::set_pixels_per_point` now calls `Context::set_zoom_level`, and it may make sense for you to call that directly instead +* If you are using `eframe`, check out the breaking changes in [the `eframe` changelog](crates/eframe/CHANGELOG.md) + +#### For integrations +There are several changes relevant to integrations. + +* Added `crate::RawInput::viewports` with information about all active viewports +* The repaint callback set by `Context::set_request_repaint_callback` now points to which viewport should be repainted +* `Context::run` now returns a list of `ViewportOutput` in `FullOutput` which should result in their own independent windows +* There is a new `Context::set_immediate_viewport_renderer` for setting up the immediate viewport integration +* If you support viewports, you need to call `Context::set_embed_viewports(false)`, or all new viewports will be embedded (the default behavior) + + ## 0.23.0 - 2023-09-27 - New image API This release contains a simple and powerful image API: diff --git a/Cargo.lock b/Cargo.lock index ba6efe4db..923906fdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1266,7 +1266,7 @@ checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" [[package]] name = "ecolor" -version = "0.23.0" +version = "0.24.0" dependencies = [ "bytemuck", "cint", @@ -1277,7 +1277,7 @@ dependencies = [ [[package]] name = "eframe" -version = "0.23.0" +version = "0.24.0" dependencies = [ "bytemuck", "cocoa", @@ -1314,7 +1314,7 @@ dependencies = [ [[package]] name = "egui" -version = "0.23.0" +version = "0.24.0" dependencies = [ "accesskit", "ahash", @@ -1330,7 +1330,7 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.23.0" +version = "0.24.0" dependencies = [ "bytemuck", "document-features", @@ -1346,7 +1346,7 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.23.0" +version = "0.24.0" dependencies = [ "accesskit_winit", "arboard", @@ -1364,7 +1364,7 @@ dependencies = [ [[package]] name = "egui_demo_app" -version = "0.23.0" +version = "0.24.0" dependencies = [ "bytemuck", "chrono", @@ -1377,6 +1377,8 @@ dependencies = [ "image", "log", "poll-promise", + "puffin", + "puffin_http", "rfd", "serde", "wasm-bindgen", @@ -1386,7 +1388,7 @@ dependencies = [ [[package]] name = "egui_demo_lib" -version = "0.23.0" +version = "0.24.0" dependencies = [ "chrono", "criterion", @@ -1401,7 +1403,7 @@ dependencies = [ [[package]] name = "egui_extras" -version = "0.23.0" +version = "0.24.0" dependencies = [ "chrono", "document-features", @@ -1421,7 +1423,7 @@ dependencies = [ [[package]] name = "egui_glow" -version = "0.23.0" +version = "0.24.0" dependencies = [ "bytemuck", "document-features", @@ -1440,7 +1442,7 @@ dependencies = [ [[package]] name = "egui_plot" -version = "0.23.0" +version = "0.24.0" dependencies = [ "document-features", "egui", @@ -1469,7 +1471,7 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "emath" -version = "0.23.0" +version = "0.24.0" dependencies = [ "bytemuck", "document-features", @@ -1545,7 +1547,7 @@ dependencies = [ [[package]] name = "epaint" -version = "0.23.0" +version = "0.24.0" dependencies = [ "ab_glyph", "ahash", diff --git a/Cargo.toml b/Cargo.toml index 038a0c2db..45f2f3c30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,13 @@ members = [ "examples/*", ] +[workspace.package] +edition = "2021" +license = "MIT OR Apache-2.0" +rust-version = "1.72" +version = "0.24.0" + + [profile.release] # lto = true # VERY slightly smaller wasm # opt-level = 's' # 10-20% smaller wasm compared to `opt-level = 3` diff --git a/clippy.toml b/clippy.toml index 48a487e4b..1192b56ae 100644 --- a/clippy.toml +++ b/clippy.toml @@ -60,6 +60,9 @@ disallowed-types = [ # "std::sync::Once", # enabled for now as the `log_once` macro uses it internally "ring::digest::SHA1_FOR_LEGACY_USE_ONLY", # SHA1 is cryptographically broken + + "winit::dpi::LogicalSize", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account + "winit::dpi::LogicalPosition", # We do our own pixels<->point conversion, taking `egui_ctx.zoom_factor` into account ] # ----------------------------------------------------------------------------- diff --git a/crates/ecolor/CHANGELOG.md b/crates/ecolor/CHANGELOG.md index 228275e04..0fb9f9d43 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 by running the `scripts/generate_changelog.py` script. +## 0.24.0 - 2023-11-23 +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Add `#[inline]` to all color-related function [38b4234](https://github.com/emilk/egui/commit/38b4234c3282a7c044c18b77234ee8c204efe171) + + ## 0.22.0 - 2023-05-23 * Nothing new diff --git a/crates/ecolor/Cargo.toml b/crates/ecolor/Cargo.toml index 245fbe433..6870d2093 100644 --- a/crates/ecolor/Cargo.toml +++ b/crates/ecolor/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "ecolor" -version = "0.23.0" +version.workspace = true authors = [ "Emil Ernerfeldt ", "Andreas Reich ", ] description = "Color structs and color conversion utilities" -edition = "2021" -rust-version = "1.72" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui" categories = ["mathematics", "encoding"] diff --git a/crates/eframe/CHANGELOG.md b/crates/eframe/CHANGELOG.md index 2a0b5136e..92d6ec532 100644 --- a/crates/eframe/CHANGELOG.md +++ b/crates/eframe/CHANGELOG.md @@ -6,6 +6,56 @@ NOTE: [`egui-winit`](../egui-winit/CHANGELOG.md), [`egui_glium`](../egui_glium/C This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. + +## 0.24.0 - 2023-11-23 +* Multiple viewports/windows [#3172](https://github.com/emilk/egui/pull/3172) (thanks [@konkitoman](https://github.com/konkitoman)!) +* Replace `eframe::Frame` commands and `WindowInfo` with egui [#3564](https://github.com/emilk/egui/pull/3564) +* Use `egui::ViewportBuilder` in `eframe::NativeOptions` [#3572](https://github.com/emilk/egui/pull/3572) +* Remove warm-starting [#3574](https://github.com/emilk/egui/pull/3574) +* Fix copy and cut on Safari [#3513](https://github.com/emilk/egui/pull/3513) (thanks [@lunixbochs](https://github.com/lunixbochs)!) +* Update puffin to 0.18 [#3600](https://github.com/emilk/egui/pull/3600) +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) + +### Breaking changes: +Most settings in `NativeOptions` have been moved to `NativeOptions::viewport`, which uses the new `egui::ViewportBuilder`: + +```diff + let native_options = eframe::nativeOptions { +- initial_window_size: Some(egui::vec2(320.0, 240.0)), +- drag_and_drop_support: true, ++ viewport: egui::ViewportBuilder::default() ++ .with_inner_size([320.0, 240.0]) ++ .with_drag_and_drop(true), + ..Default::default() + }; +``` + +`NativeOptions::fullsize_content` has been replaced with four settings: `ViewportBuilder::with_fullsize_content_view`, `with_title_shown`, `with_titlebar_shown`, `with_titlebar_buttons_shown` + +`frame.info().window_info` is gone, replaced with `ctx.input(|i| i.viewport())`. + +`frame.info().native_pixels_per_point` is replaced with `ctx.input(|i| i.raw.native_pixels_per_point)`. + +Most commands in `eframe::Frame` has been replaced with `egui::ViewportCommand`, so So `frame.close()` becomes `ctx.send_viewport_cmd(ViewportCommand::Close)`, etc. + +`App::on_close_event` has been replaced with `ctx.input(|i| i.viewport().close_requested())` and `ctx.send_viewport_cmd(ViewportCommand::CancelClose)`. + +`eframe::IconData` is now `egui::IconData`. + +`eframe::IconData::try_from_png_bytes` is now `eframe::icon_data::from_png_bytes`. + +`App::post_rendering` is gone. Screenshots are taken with `ctx.send_viewport_cmd(ViewportCommand::Screenshots)` and are returned in `egui::Event` which you can check with: +``` rust +ui.input(|i| { + for event in &i.raw.events { + if let egui::Event::Screenshot { viewport_id, image } = event { + // handle it here + } + } +}); +``` + + ## 0.23.0 - 2023-09-27 * Update MSRV to Rust 1.70.0 [#3310](https://github.com/emilk/egui/pull/3310) * Update to puffin 0.16 [#3144](https://github.com/emilk/egui/pull/3144) diff --git a/crates/eframe/Cargo.toml b/crates/eframe/Cargo.toml index 0ae35f02e..0178b0964 100644 --- a/crates/eframe/Cargo.toml +++ b/crates/eframe/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "eframe" -version = "0.23.0" +version.workspace = true authors = ["Emil Ernerfeldt "] description = "egui framework - write GUI apps that compiles to web and/or natively" -edition = "2021" -rust-version = "1.72" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/eframe" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/eframe" categories = ["gui", "game-development"] @@ -98,7 +98,7 @@ x11 = ["egui-winit/x11"] __screenshot = [] [dependencies] -egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ +egui = { version = "0.24.0", path = "../egui", default-features = false, features = [ "bytemuck", "log", ] } @@ -111,7 +111,7 @@ thiserror.workspace = true ## Enable this when generating docs. document-features = { version = "0.2", optional = true } -egui_glow = { version = "0.23.0", path = "../egui_glow", optional = true, default-features = false } +egui_glow = { version = "0.24.0", path = "../egui_glow", optional = true, default-features = false } glow = { version = "0.12", optional = true } ron = { version = "0.8", optional = true, features = ["integer128"] } serde = { version = "1", optional = true, features = ["derive"] } @@ -119,7 +119,7 @@ serde = { version = "1", optional = true, features = ["derive"] } # ------------------------------------------- # native: [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -egui-winit = { version = "0.23.0", path = "../egui-winit", default-features = false, features = [ +egui-winit = { version = "0.24.0", path = "../egui-winit", default-features = false, features = [ "clipboard", "links", ] } @@ -131,7 +131,7 @@ winit = { version = "0.29", default-features = false, features = ["rwh_05"] } # optional native: directories-next = { version = "2", optional = true } -egui-wgpu = { version = "0.23.0", path = "../egui-wgpu", optional = true, features = [ +egui-wgpu = { version = "0.24.0", path = "../egui-wgpu", optional = true, features = [ "winit", ] } # if wgpu is used, use it with winit pollster = { version = "0.3", optional = true } # needed for wgpu @@ -205,7 +205,7 @@ web-sys = { version = "0.3.58", features = [ ] } # optional web: -egui-wgpu = { version = "0.23.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit +egui-wgpu = { version = "0.24.0", path = "../egui-wgpu", optional = true } # if wgpu is used, use it without (!) winit raw-window-handle = { workspace = true, optional = true } tts = { version = "0.25", optional = true, default-features = false } wgpu = { workspace = true, optional = true } diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index 848b144c1..982eb5481 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -149,25 +149,10 @@ pub trait App { /// On native the path is picked using [`crate::storage_dir`]. fn save(&mut self, _storage: &mut dyn Storage) {} - /// Called when the user attempts to close the desktop window and/or quit the application. - /// - /// By returning `false` the closing will be aborted. To continue the closing return `true`. - /// - /// A scenario where this method will be run is after pressing the close button on a native - /// window, which allows you to ask the user whether they want to do something before exiting. - /// See the example at for practical usage. - /// - /// It will _not_ be called on the web or when the window is forcefully closed. - #[cfg(not(target_arch = "wasm32"))] - #[doc(alias = "exit")] - #[doc(alias = "quit")] - fn on_close_event(&mut self) -> bool { - true - } - /// Called once on shutdown, after [`Self::save`]. /// - /// If you need to abort an exit use [`Self::on_close_event`]. + /// If you need to abort an exit check `ctx.input(|i| i.viewport().close_requested())` + /// and respond with [`egui::ViewportCommand::CancelClose`]. /// /// To get a [`glow`] context you need to compile with the `glow` feature flag, /// and run eframe with the glow backend. @@ -248,6 +233,9 @@ pub struct NativeOptions { /// Controls the native window of the root viewport. /// /// This is where you set things like window title and size. + /// + /// If you don't set an icon, a default egui icon will be used. + /// To avoid this, set the icon to [`egui::IconData::default`]. pub viewport: egui::ViewportBuilder, /// Turn on vertical syncing, limiting the FPS to the display refresh rate. @@ -379,13 +367,7 @@ impl Clone for NativeOptions { impl Default for NativeOptions { fn default() -> Self { Self { - viewport: egui::ViewportBuilder { - icon: Some(std::sync::Arc::new( - crate::icon_data::from_png_bytes(&include_bytes!("../data/icon.png")[..]) - .unwrap(), - )), - ..Default::default() - }, + viewport: Default::default(), vsync: true, multisampling: 0, diff --git a/crates/eframe/src/native/app_icon.rs b/crates/eframe/src/native/app_icon.rs index cff0349ac..f16942086 100644 --- a/crates/eframe/src/native/app_icon.rs +++ b/crates/eframe/src/native/app_icon.rs @@ -13,7 +13,13 @@ pub struct AppTitleIconSetter { } impl AppTitleIconSetter { - pub fn new(title: String, icon_data: Option>) -> Self { + pub fn new(title: String, mut icon_data: Option>) -> Self { + if let Some(icon) = &icon_data { + if **icon == IconData::default() { + icon_data = None; + } + } + Self { title, icon_data, diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index fff31f184..eb42a5a61 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -12,6 +12,7 @@ use egui_winit::{EventResponse, WindowSettings}; use crate::{epi, Theme}; pub fn viewport_builder( + egui_zoom_factor: f32, event_loop: &EventLoopWindowTarget, native_options: &mut epi::NativeOptions, window_settings: Option, @@ -26,8 +27,9 @@ pub fn viewport_builder( let inner_size_points = if let Some(mut window_settings) = window_settings { // Restore pos/size from previous session - window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop)); - window_settings.clamp_position_to_monitors(event_loop); + window_settings + .clamp_size_to_sane_values(largest_monitor_point_size(egui_zoom_factor, event_loop)); + window_settings.clamp_position_to_monitors(egui_zoom_factor, event_loop); viewport_builder = window_settings.initialize_viewport_builder(viewport_builder); window_settings.inner_size_points() @@ -37,8 +39,8 @@ pub fn viewport_builder( } if let Some(initial_window_size) = viewport_builder.inner_size { - let initial_window_size = - initial_window_size.at_most(largest_monitor_point_size(event_loop)); + let initial_window_size = initial_window_size + .at_most(largest_monitor_point_size(egui_zoom_factor, event_loop)); viewport_builder = viewport_builder.with_inner_size(initial_window_size); } @@ -49,9 +51,11 @@ pub fn viewport_builder( if native_options.centered { crate::profile_scope!("center"); if let Some(monitor) = event_loop.available_monitors().next() { - let monitor_size = monitor.size().to_logical::(monitor.scale_factor()); + let monitor_size = monitor + .size() + .to_logical::(egui_zoom_factor as f64 * monitor.scale_factor()); let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 }); - if monitor_size.width > 0.0 && monitor_size.height > 0.0 { + if 0.0 < monitor_size.width && 0.0 < monitor_size.height { let x = (monitor_size.width - inner_size.x) / 2.0; let y = (monitor_size.height - inner_size.y) / 2.0; viewport_builder = viewport_builder.with_position([x, y]); @@ -76,7 +80,10 @@ pub fn apply_window_settings( } } -fn largest_monitor_point_size(event_loop: &EventLoopWindowTarget) -> egui::Vec2 { +fn largest_monitor_point_size( + egui_zoom_factor: f32, + event_loop: &EventLoopWindowTarget, +) -> egui::Vec2 { crate::profile_function!(); let mut max_size = egui::Vec2::ZERO; @@ -87,7 +94,9 @@ fn largest_monitor_point_size(event_loop: &EventLoopWindowTarget) -> egui: }; for monitor in available_monitors { - let size = monitor.size().to_logical::(monitor.scale_factor()); + let size = monitor + .size() + .to_logical::(egui_zoom_factor as f64 * monitor.scale_factor()); let size = egui::vec2(size.width, size.height); max_size = max_size.max(size); } @@ -137,21 +146,15 @@ pub struct EpiIntegration { impl EpiIntegration { #[allow(clippy::too_many_arguments)] pub fn new( + egui_ctx: egui::Context, window: &winit::window::Window, system_theme: Option, app_name: &str, native_options: &crate::NativeOptions, storage: Option>, - is_desktop: bool, #[cfg(feature = "glow")] gl: Option>, #[cfg(feature = "wgpu")] wgpu_render_state: Option, ) -> Self { - let egui_ctx = egui::Context::default(); - egui_ctx.set_embed_viewports(!is_desktop); - - let memory = load_egui_memory(storage.as_deref()).unwrap_or_default(); - egui_ctx.memory_mut(|mem| *mem = memory); - let frame = epi::Frame { info: epi::IntegrationInfo { system_theme, @@ -166,13 +169,19 @@ impl EpiIntegration { raw_window_handle: window.raw_window_handle(), }; + let icon = native_options + .viewport + .icon + .clone() + .unwrap_or_else(|| std::sync::Arc::new(load_default_egui_icon())); + let app_icon_setter = super::app_icon::AppTitleIconSetter::new( native_options .viewport .title .clone() .unwrap_or_else(|| app_name.to_owned()), - native_options.viewport.icon.clone(), + Some(icon), ); Self { @@ -220,22 +229,14 @@ impl EpiIntegration { pub fn on_window_event( &mut self, - app: &mut dyn epi::App, event: &winit::event::WindowEvent, egui_winit: &mut egui_winit::State, - viewport_id: ViewportId, ) -> EventResponse { crate::profile_function!(egui_winit::short_window_event_description(event)); use winit::event::{ElementState, MouseButton, WindowEvent}; match event { - WindowEvent::CloseRequested => { - if viewport_id == ViewportId::ROOT { - self.close = app.on_close_event(); - log::debug!("App::on_close_event returned {}", self.close); - } - } WindowEvent::Destroyed => { log::debug!("Received WindowEvent::Destroyed"); self.close = true; @@ -245,9 +246,6 @@ impl EpiIntegration { state: ElementState::Pressed, .. } => self.can_drag_window = true, - WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - egui_winit.egui_input_mut().native_pixels_per_point = Some(*scale_factor as _); - } WindowEvent::ThemeChanged(winit_theme) if self.follow_system_theme => { let theme = theme_from_winit_theme(*winit_theme); self.frame.info.system_theme = Some(theme); @@ -275,23 +273,32 @@ impl EpiIntegration { ) -> egui::FullOutput { raw_input.time = Some(self.beginning.elapsed().as_secs_f64()); + let close_requested = raw_input.viewport().close_requested(); + 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"); viewport_ui_cb(egui_ctx); } else { - // Root viewport - if egui_ctx.input(|i| i.viewport().close_requested()) { - self.close = app.on_close_event(); - log::debug!("App::on_close_event returned {}", self.close); - } - crate::profile_scope!("App::update"); app.update(egui_ctx, &mut self.frame); } }); + let is_root_viewport = viewport_ui_cb.is_none(); + if is_root_viewport && close_requested { + let canceled = full_output.viewport_output[&ViewportId::ROOT] + .commands + .contains(&egui::ViewportCommand::CancelClose); + if canceled { + log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose"); + } else { + log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)"); + self.close = true; + } + } + self.pending_full_output.append(full_output); std::mem::take(&mut self.pending_full_output) } @@ -309,16 +316,6 @@ impl EpiIntegration { } } - pub fn handle_platform_output( - &mut self, - window: &winit::window::Window, - viewport_id: ViewportId, - platform_output: egui::PlatformOutput, - egui_winit: &mut egui_winit::State, - ) { - egui_winit.handle_platform_output(window, viewport_id, &self.egui_ctx, platform_output); - } - // ------------------------------------------------------------------------ // Persistence stuff: @@ -346,7 +343,7 @@ impl EpiIntegration { epi::set_value( storage, STORAGE_WINDOW_KEY, - &WindowSettings::from_display(window), + &WindowSettings::from_window(self.egui_ctx.zoom_factor(), window), ); } } @@ -366,6 +363,11 @@ impl EpiIntegration { } } +fn load_default_egui_icon() -> egui::IconData { + crate::profile_function!(); + crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap() +} + #[cfg(feature = "persistence")] const STORAGE_EGUI_MEMORY_KEY: &str = "egui"; diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 91f8b6c7d..0041a2d75 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -25,8 +25,8 @@ use egui_winit::{ }; use crate::{ - native::epi_integration::EpiIntegration, App, AppCreator, CreationContext, NativeOptions, - Result, Storage, + native::{epi_integration::EpiIntegration, winit_integration::create_egui_context}, + App, AppCreator, CreationContext, NativeOptions, Result, Storage, }; use super::{ @@ -87,6 +87,8 @@ struct GlowWinitRunning { /// The setup is divided between the `new` fn and `on_resume` fn. we can just assume that `on_resume` is a continuation of /// `new` fn on all platforms. only on android, do we get multiple resumed events because app can be suspended. struct GlutinWindowContext { + egui_ctx: egui::Context, + swap_interval: glutin::surface::SwapInterval, gl_config: glutin::config::Config, @@ -139,6 +141,7 @@ impl GlowWinitApp { #[allow(unsafe_code)] fn create_glutin_windowed_context( + egui_ctx: &egui::Context, event_loop: &EventLoopWindowTarget, storage: Option<&dyn Storage>, native_options: &mut NativeOptions, @@ -147,11 +150,16 @@ impl GlowWinitApp { let window_settings = epi_integration::load_window_settings(storage); - let winit_window_builder = - epi_integration::viewport_builder(event_loop, native_options, window_settings); + let winit_window_builder = epi_integration::viewport_builder( + egui_ctx.zoom_factor(), + event_loop, + native_options, + window_settings, + ); - let mut glutin_window_context = - unsafe { GlutinWindowContext::new(winit_window_builder, native_options, event_loop)? }; + let mut glutin_window_context = unsafe { + GlutinWindowContext::new(egui_ctx, winit_window_builder, native_options, event_loop)? + }; // Creates the window - must come before we create our glow context glutin_window_context.on_resume(event_loop)?; @@ -191,7 +199,10 @@ impl GlowWinitApp { .unwrap_or(&self.app_name), ); + let egui_ctx = create_egui_context(storage.as_deref()); + let (mut glutin, painter) = Self::create_glutin_windowed_context( + &egui_ctx, event_loop, storage.as_deref(), &mut self.native_options, @@ -210,12 +221,12 @@ impl GlowWinitApp { winit_integration::system_theme(&glutin.window(ViewportId::ROOT), &self.native_options); let integration = EpiIntegration::new( + egui_ctx, &glutin.window(ViewportId::ROOT), system_theme, &self.app_name, &self.native_options, storage, - winit_integration::IS_DESKTOP, Some(gl.clone()), #[cfg(feature = "wgpu")] None, @@ -498,7 +509,7 @@ impl GlowWinitRunning { let window = viewport.window.as_ref().unwrap(); let egui_winit = viewport.egui_winit.as_mut().unwrap(); - let mut raw_input = egui_winit.take_egui_input(window, viewport.ids); + let mut raw_input = egui_winit.take_egui_input(window); let viewport_ui_cb = viewport.viewport_ui_cb.clone(); self.integration.pre_update(); @@ -549,12 +560,13 @@ impl GlowWinitRunning { } = &mut *glutin; let viewport = viewports.get_mut(&viewport_id).unwrap(); + viewport.info.events.clear(); // they should have been processed let window = viewport.window.as_ref().unwrap(); let gl_surface = viewport.gl_surface.as_ref().unwrap(); let egui_winit = viewport.egui_winit.as_mut().unwrap(); integration.post_update(); - integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); + egui_winit.handle_platform_output(window, &integration.egui_ctx, platform_output); let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); @@ -632,7 +644,7 @@ impl GlowWinitRunning { std::thread::sleep(std::time::Duration::from_millis(10)); } - glutin.handle_viewport_output(viewport_output); + glutin.handle_viewport_output(&integration.egui_ctx, viewport_output); if integration.should_close() { EventResult::Exit @@ -728,12 +740,9 @@ impl GlowWinitRunning { }; if let Some(viewport_id) = viewport_id { if let Some(viewport) = glutin.viewports.get_mut(&viewport_id) { - event_response = self.integration.on_window_event( - self.app.as_mut(), - event, - viewport.egui_winit.as_mut().unwrap(), - viewport.ids.this, - ); + event_response = self + .integration + .on_window_event(event, viewport.egui_winit.as_mut().unwrap()); } } @@ -752,6 +761,7 @@ impl GlowWinitRunning { impl GlutinWindowContext { #[allow(unsafe_code)] unsafe fn new( + egui_ctx: &egui::Context, viewport_builder: ViewportBuilder, native_options: &NativeOptions, event_loop: &EventLoopWindowTarget, @@ -803,7 +813,11 @@ impl GlutinWindowContext { let display_builder = glutin_winit::DisplayBuilder::new() // we might want to expose this option to users in the future. maybe using an env var or using native_options. .with_preference(glutin_winit::ApiPreference::FallbackEgl) // https://github.com/emilk/egui/issues/2520#issuecomment-1367841150 - .with_window_builder(Some(create_winit_window_builder(viewport_builder.clone()))); + .with_window_builder(Some(create_winit_window_builder( + egui_ctx, + event_loop, + viewport_builder.clone(), + ))); let (window, gl_config) = { crate::profile_scope!("DisplayBuilder::build"); @@ -899,6 +913,7 @@ impl GlutinWindowContext { // https://github.com/emilk/egui/pull/2541#issuecomment-1370767582 let mut slf = GlutinWindowContext { + egui_ctx: egui_ctx.clone(), swap_interval, gl_config, current_gl_context: None, @@ -958,7 +973,7 @@ impl GlutinWindowContext { log::trace!("Window doesn't exist yet. Creating one now with finalize_window"); let window = glutin_winit::finalize_window( event_loop, - create_winit_window_builder(viewport.builder.clone()), + create_winit_window_builder(&self.egui_ctx, event_loop, viewport.builder.clone()), &self.gl_config, )?; apply_viewport_builder_to_new_window(&window, &viewport.builder); @@ -1009,6 +1024,7 @@ impl GlutinWindowContext { viewport.egui_winit.get_or_insert_with(|| { egui_winit::State::new( + viewport_id, event_loop, Some(window.scale_factor() as f32), self.max_texture_side, @@ -1085,7 +1101,11 @@ impl GlutinWindowContext { self.gl_config.display().get_proc_address(addr) } - fn handle_viewport_output(&mut self, viewport_output: ViewportIdMap) { + fn handle_viewport_output( + &mut self, + egui_ctx: &egui::Context, + viewport_output: ViewportIdMap, + ) { crate::profile_function!(); let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); @@ -1105,6 +1125,7 @@ impl GlutinWindowContext { let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); let viewport = initialize_or_update_viewport( + egui_ctx, &mut self.viewports, ids, class, @@ -1116,6 +1137,7 @@ impl GlutinWindowContext { if let Some(window) = &viewport.window { let is_viewport_focused = self.focused_viewport == Some(viewport_id); egui_winit::process_viewport_commands( + egui_ctx, &mut viewport.info, commands, window, @@ -1148,14 +1170,15 @@ impl Viewport { } } -fn initialize_or_update_viewport( - viewports: &mut ViewportIdMap, +fn initialize_or_update_viewport<'vp>( + egu_ctx: &'_ egui::Context, + viewports: &'vp mut ViewportIdMap, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, focused_viewport: Option, -) -> &mut Viewport { +) -> &'vp mut Viewport { crate::profile_function!(); if builder.icon.is_none() { @@ -1190,19 +1213,20 @@ fn initialize_or_update_viewport( viewport.class = class; viewport.viewport_ui_cb = viewport_ui_cb; - let (delta_commands, recreate) = viewport.builder.patch(&builder); + let (delta_commands, recreate) = viewport.builder.patch(builder); if recreate { log::debug!( "Recreating window for viewport {:?} ({:?})", ids.this, - builder.title + viewport.builder.title ); viewport.window = None; viewport.egui_winit = None; } else if let Some(window) = &viewport.window { let is_viewport_focused = focused_viewport == Some(ids.this); process_viewport_commands( + egu_ctx, &mut viewport.info, delta_commands, window, @@ -1238,6 +1262,7 @@ fn render_immediate_viewport( let mut glutin = glutin.borrow_mut(); let viewport = initialize_or_update_viewport( + egui_ctx, &mut glutin.viewports, ids, ViewportClass::Immediate, @@ -1267,7 +1292,7 @@ fn render_immediate_viewport( return; }; - let mut raw_input = winit_state.take_egui_input(window, ids); + let mut raw_input = winit_state.take_egui_input(window); raw_input.viewports = glutin .viewports .iter() @@ -1304,6 +1329,7 @@ fn render_immediate_viewport( let Some(viewport) = viewports.get_mut(&ids.this) else { return; }; + viewport.info.events.clear(); // they should have been processed let Some(winit_state) = &mut viewport.egui_winit else { return; @@ -1351,9 +1377,9 @@ fn render_immediate_viewport( } } - winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); + winit_state.handle_platform_output(window, egui_ctx, platform_output); - glutin.handle_viewport_output(viewport_output); + glutin.handle_viewport_output(egui_ctx, viewport_output); } #[cfg(feature = "__screenshot")] diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index a51ecec6d..3e8cf0e51 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -57,6 +57,7 @@ struct WgpuWinitRunning { /// /// Wrapped in an `Rc>` so it can be re-entrantly shared via a weak-pointer. pub struct SharedState { + egui_ctx: egui::Context, viewports: Viewports, painter: egui_wgpu::winit::Painter, viewport_from_window: HashMap, @@ -123,7 +124,12 @@ impl WgpuWinitApp { for viewport in viewports.values_mut() { if viewport.window.is_none() { - viewport.init_window(viewport_from_window, painter, event_loop); + viewport.init_window( + &running.integration.egui_ctx, + viewport_from_window, + painter, + event_loop, + ); } } } @@ -140,6 +146,7 @@ impl WgpuWinitApp { fn init_run_state( &mut self, + egui_ctx: egui::Context, event_loop: &EventLoopWindowTarget, storage: Option>, window: Window, @@ -163,12 +170,12 @@ impl WgpuWinitApp { let system_theme = winit_integration::system_theme(&window, &self.native_options); let integration = EpiIntegration::new( + egui_ctx.clone(), &window, system_theme, &self.app_name, &self.native_options, storage, - winit_integration::IS_DESKTOP, #[cfg(feature = "glow")] None, wgpu_render_state.clone(), @@ -177,25 +184,25 @@ impl WgpuWinitApp { { let event_loop_proxy = self.repaint_proxy.clone(); - integration - .egui_ctx - .set_request_repaint_callback(move |info| { - log::trace!("request_repaint_callback: {info:?}"); - let when = Instant::now() + info.delay; - let frame_nr = info.current_frame_nr; + egui_ctx.set_request_repaint_callback(move |info| { + log::trace!("request_repaint_callback: {info:?}"); + let when = Instant::now() + info.delay; + let frame_nr = info.current_frame_nr; - event_loop_proxy - .lock() - .send_event(UserEvent::RequestRepaint { - when, - frame_nr, - viewport_id: info.viewport_id, - }) - .ok(); - }); + event_loop_proxy + .lock() + .send_event(UserEvent::RequestRepaint { + when, + frame_nr, + viewport_id: info.viewport_id, + }) + .ok(); + }); } + #[allow(unused_mut)] // used for accesskit let mut egui_winit = egui_winit::State::new( + ViewportId::ROOT, event_loop, Some(window.scale_factor() as f32), painter.max_texture_side(), @@ -207,12 +214,12 @@ impl WgpuWinitApp { integration.init_accesskit(&mut egui_winit, &window, event_loop_proxy); } let theme = system_theme.unwrap_or(self.native_options.default_theme); - integration.egui_ctx.set_visuals(theme.egui_visuals()); + egui_ctx.set_visuals(theme.egui_visuals()); let app_creator = std::mem::take(&mut self.app_creator) .expect("Single-use AppCreator has unexpectedly already been taken"); let cc = CreationContext { - egui_ctx: integration.egui_ctx.clone(), + egui_ctx: egui_ctx.clone(), integration_info: integration.frame.info().clone(), storage: integration.frame.storage(), #[cfg(feature = "glow")] @@ -249,6 +256,7 @@ impl WgpuWinitApp { ); let shared = Rc::new(RefCell::new(SharedState { + egui_ctx, viewport_from_window, viewports, painter, @@ -262,20 +270,14 @@ impl WgpuWinitApp { let event_loop: *const EventLoopWindowTarget = event_loop; - egui::Context::set_immediate_viewport_renderer(move |egui_ctx, immediate_viewport| { + egui::Context::set_immediate_viewport_renderer(move |_egui_ctx, immediate_viewport| { if let Some(shared) = shared.upgrade() { // SAFETY: the event loop lives longer than // the Rc:s we just upgraded above. #[allow(unsafe_code)] let event_loop = unsafe { event_loop.as_ref().unwrap() }; - render_immediate_viewport( - event_loop, - egui_ctx, - beginning, - &shared, - immediate_viewport, - ); + render_immediate_viewport(event_loop, beginning, &shared, immediate_viewport); } else { log::warn!("render_sync_callback called after window closed"); } @@ -373,9 +375,14 @@ impl WinitApp for WgpuWinitApp { .as_ref() .unwrap_or(&self.app_name), ); - let (window, builder) = - create_window(event_loop, storage.as_deref(), &mut self.native_options)?; - self.init_run_state(event_loop, storage, window, builder)? + let egui_ctx = winit_integration::create_egui_context(storage.as_deref()); + let (window, builder) = create_window( + &egui_ctx, + event_loop, + storage.as_deref(), + &mut self.native_options, + )?; + self.init_run_state(egui_ctx, event_loop, storage, window, builder)? }; EventResult::RepaintNow( @@ -504,7 +511,6 @@ impl WgpuWinitRunning { viewport.update_viewport_info(); let Viewport { - ids, viewport_ui_cb, window, egui_winit, @@ -525,10 +531,7 @@ impl WgpuWinitRunning { } } - let mut raw_input = egui_winit.as_mut().unwrap().take_egui_input( - window, - ViewportIdPair::from_self_and_parent(viewport_id, ids.parent), - ); + let mut raw_input = egui_winit.as_mut().unwrap().take_egui_input(window); integration.pre_update(); @@ -552,6 +555,7 @@ impl WgpuWinitRunning { let mut shared = shared.borrow_mut(); let SharedState { + egui_ctx, viewports, painter, viewport_from_window, @@ -562,6 +566,8 @@ impl WgpuWinitRunning { return EventResult::Wait; }; + viewport.info.events.clear(); // they should have been processed + let Viewport { window: Some(window), egui_winit: Some(egui_winit), @@ -581,16 +587,16 @@ impl WgpuWinitRunning { viewport_output, } = full_output; - integration.handle_platform_output(window, viewport_id, platform_output, egui_winit); + egui_winit.handle_platform_output(window, egui_ctx, platform_output); { - let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point); + let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); let screenshot_requested = std::mem::take(&mut viewport.screenshot_requested); let screenshot = painter.paint_and_update_textures( viewport_id, pixels_per_point, - app.clear_color(&integration.egui_ctx.style().visuals), + app.clear_color(&egui_ctx.style().visuals), &clipped_primitives, &textures_delta, screenshot_requested, @@ -610,7 +616,12 @@ impl WgpuWinitRunning { let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect(); - handle_viewport_output(viewport_output, viewports, *focused_viewport); + handle_viewport_output( + &integration.egui_ctx, + viewport_output, + viewports, + *focused_viewport, + ); // Prune dead viewports: viewports.retain(|id, _| active_viewports_ids.contains(id)); @@ -649,8 +660,8 @@ impl WgpuWinitRunning { let Self { integration, - app, shared, + .. } = self; let mut shared = shared.borrow_mut(); @@ -725,9 +736,10 @@ impl WgpuWinitRunning { let event_response = viewport_id .and_then(|viewport_id| { shared.viewports.get_mut(&viewport_id).and_then(|viewport| { - viewport.egui_winit.as_mut().map(|egui_winit| { - integration.on_window_event(app.as_mut(), event, egui_winit, viewport_id) - }) + viewport + .egui_winit + .as_mut() + .map(|egui_winit| integration.on_window_event(event, egui_winit)) }) }) .unwrap_or_default(); @@ -749,6 +761,7 @@ impl WgpuWinitRunning { impl Viewport { fn init_window( &mut self, + egui_ctx: &egui::Context, windows_id: &mut HashMap, painter: &mut egui_wgpu::winit::Painter, event_loop: &EventLoopWindowTarget, @@ -757,7 +770,9 @@ impl Viewport { let viewport_id = self.ids.this; - match create_winit_window_builder(self.builder.clone()).build(event_loop) { + match create_winit_window_builder(egui_ctx, event_loop, self.builder.clone()) + .build(event_loop) + { Ok(window) => { apply_viewport_builder_to_new_window(&window, &self.builder); @@ -769,6 +784,7 @@ impl Viewport { } self.egui_winit = Some(egui_winit::State::new( + viewport_id, event_loop, Some(window.scale_factor() as f32), painter.max_texture_side(), @@ -799,6 +815,7 @@ impl Viewport { } fn create_window( + egui_ctx: &egui::Context, event_loop: &EventLoopWindowTarget, storage: Option<&dyn Storage>, native_options: &mut NativeOptions, @@ -806,11 +823,16 @@ fn create_window( crate::profile_function!(); let window_settings = epi_integration::load_window_settings(storage); - let viewport_builder = - epi_integration::viewport_builder(event_loop, native_options, window_settings); + let viewport_builder = epi_integration::viewport_builder( + egui_ctx.zoom_factor(), + event_loop, + native_options, + window_settings, + ); let window = { crate::profile_scope!("WindowBuilder::build"); - create_winit_window_builder(viewport_builder.clone()).build(event_loop)? + create_winit_window_builder(egui_ctx, event_loop, viewport_builder.clone()) + .build(event_loop)? }; apply_viewport_builder_to_new_window(&window, &viewport_builder); epi_integration::apply_window_settings(&window, window_settings); @@ -819,7 +841,6 @@ fn create_window( fn render_immediate_viewport( event_loop: &EventLoopWindowTarget, - egui_ctx: &egui::Context, beginning: Instant, shared: &RefCell, immediate_viewport: ImmediateViewport<'_>, @@ -834,6 +855,7 @@ fn render_immediate_viewport( let input = { let SharedState { + egui_ctx, viewports, painter, viewport_from_window, @@ -841,6 +863,7 @@ fn render_immediate_viewport( } = &mut *shared.borrow_mut(); let viewport = initialize_or_update_viewport( + egui_ctx, viewports, ids, ViewportClass::Immediate, @@ -849,7 +872,7 @@ fn render_immediate_viewport( None, ); if viewport.window.is_none() { - viewport.init_window(viewport_from_window, painter, event_loop); + viewport.init_window(egui_ctx, viewport_from_window, painter, event_loop); } viewport.update_viewport_info(); @@ -857,7 +880,7 @@ fn render_immediate_viewport( return; }; - let mut input = winit_state.take_egui_input(window, ids); + let mut input = winit_state.take_egui_input(window); input.viewports = viewports .iter() .map(|(id, viewport)| (*id, viewport.info.clone())) @@ -866,6 +889,8 @@ fn render_immediate_viewport( input }; + let egui_ctx = shared.borrow().egui_ctx.clone(); + // ------------------------------------------ // Run the user code, which could re-entrantly call this function again (!). @@ -893,6 +918,7 @@ fn render_immediate_viewport( let Some(viewport) = viewports.get_mut(&ids.this) else { return; }; + viewport.info.events.clear(); // they should have been processed let Some(winit_state) = &mut viewport.egui_winit else { return; }; @@ -917,13 +943,14 @@ fn render_immediate_viewport( false, ); - winit_state.handle_platform_output(window, ids.this, egui_ctx, platform_output); + winit_state.handle_platform_output(window, &egui_ctx, platform_output); - handle_viewport_output(viewport_output, viewports, *focused_viewport); + handle_viewport_output(&egui_ctx, viewport_output, viewports, *focused_viewport); } /// Add new viewports, and update existing ones: fn handle_viewport_output( + egui_ctx: &egui::Context, viewport_output: ViewportIdMap, viewports: &mut ViewportIdMap, focused_viewport: Option, @@ -943,6 +970,7 @@ fn handle_viewport_output( let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent); let viewport = initialize_or_update_viewport( + egui_ctx, viewports, ids, class, @@ -954,6 +982,7 @@ fn handle_viewport_output( if let Some(window) = viewport.window.as_ref() { let is_viewport_focused = focused_viewport == Some(viewport_id); egui_winit::process_viewport_commands( + egui_ctx, &mut viewport.info, commands, window, @@ -964,14 +993,15 @@ fn handle_viewport_output( } } -fn initialize_or_update_viewport( - viewports: &mut Viewports, +fn initialize_or_update_viewport<'vp>( + egui_ctx: &egui::Context, + viewports: &'vp mut Viewports, ids: ViewportIdPair, class: ViewportClass, mut builder: ViewportBuilder, viewport_ui_cb: Option>, focused_viewport: Option, -) -> &mut Viewport { +) -> &'vp mut Viewport { if builder.icon.is_none() { // Inherit icon from parent builder.icon = viewports @@ -1003,19 +1033,20 @@ fn initialize_or_update_viewport( viewport.ids.parent = ids.parent; viewport.viewport_ui_cb = viewport_ui_cb; - let (delta_commands, recreate) = viewport.builder.patch(&builder); + let (delta_commands, recreate) = viewport.builder.patch(builder); if recreate { log::debug!( "Recreating window for viewport {:?} ({:?})", ids.this, - builder.title + viewport.builder.title ); viewport.window = None; viewport.egui_winit = None; } else if let Some(window) = &viewport.window { let is_viewport_focused = focused_viewport == Some(ids.this); process_viewport_commands( + egui_ctx, &mut viewport.info, delta_commands, window, diff --git a/crates/eframe/src/native/winit_integration.rs b/crates/eframe/src/native/winit_integration.rs index 6a858a1dd..6a2a97b32 100644 --- a/crates/eframe/src/native/winit_integration.rs +++ b/crates/eframe/src/native/winit_integration.rs @@ -11,13 +11,27 @@ use egui_winit::accesskit_winit; use super::epi_integration::EpiIntegration; -pub const IS_DESKTOP: bool = cfg!(any( - target_os = "freebsd", - target_os = "linux", - target_os = "macos", - target_os = "openbsd", - target_os = "windows", -)); +/// 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!(); + + pub const IS_DESKTOP: bool = cfg!(any( + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "openbsd", + target_os = "windows", + )); + + let egui_ctx = egui::Context::default(); + + egui_ctx.set_embed_viewports(!IS_DESKTOP); + + let memory = crate::native::epi_integration::load_egui_memory(storage).unwrap_or_default(); + egui_ctx.memory_mut(|mem| *mem = memory); + + egui_ctx +} /// The custom even `eframe` uses with the [`winit`] event loop. #[derive(Debug)] diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 00c3240e3..5a0ead013 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -1,5 +1,4 @@ use egui::TexturesDelta; -use wasm_bindgen::JsValue; use crate::{epi, App}; @@ -17,7 +16,10 @@ pub struct AppRunner { screen_reader: super::screen_reader::ScreenReader, pub(crate) text_cursor_pos: Option, pub(crate) mutable_text_under_cursor: bool, + + // Output for the last run: textures_delta: TexturesDelta, + clipped_primitives: Option>, } impl Drop for AppRunner { @@ -58,6 +60,12 @@ impl AppRunner { )); super::storage::load_memory(&egui_ctx); + egui_ctx.options_mut(|o| { + // On web, the browser controls the zoom factor: + o.zoom_with_keyboard = false; + o.zoom_factor = 1.0; + }); + let theme = system_theme.unwrap_or(web_options.default_theme); egui_ctx.set_visuals(theme.egui_visuals()); @@ -109,10 +117,17 @@ impl AppRunner { text_cursor_pos: None, mutable_text_under_cursor: false, textures_delta: Default::default(), + clipped_primitives: None, }; runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side()); - runner.input.raw.native_pixels_per_point = Some(super::native_pixels_per_point()); + runner + .input + .raw + .viewports + .entry(egui::ViewportId::ROOT) + .or_default() + .native_pixels_per_point = Some(super::native_pixels_per_point()); Ok(runner) } @@ -158,8 +173,26 @@ impl AppRunner { self.painter.destroy(); } - /// Call [`Self::paint`] later to paint - pub fn logic(&mut self) -> Vec { + /// Runs the user code and paints the UI. + /// + /// If there is already an outstanding frame of output, + /// that is painted instead. + pub fn run_and_paint(&mut self) { + if self.clipped_primitives.is_none() { + // Run user code, and paint the results: + self.logic(); + self.paint(); + } else { + // We have already run the logic, e.g. in an on-click event, + // so let's only present the results: + self.paint(); + } + } + + /// Runs the logic, but doesn't paint the result. + /// + /// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`]. + pub fn logic(&mut self) { let frame_start = now_sec(); super::resize_canvas_to_screen_size(self.canvas_id(), self.web_options.max_size_points); @@ -191,25 +224,26 @@ impl AppRunner { self.handle_platform_output(platform_output); self.textures_delta.append(textures_delta); - let clipped_primitives = self.egui_ctx.tessellate(shapes, pixels_per_point); + self.clipped_primitives = Some(self.egui_ctx.tessellate(shapes, pixels_per_point)); self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32); - - clipped_primitives } /// Paint the results of the last call to [`Self::logic`]. - pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> { + pub fn paint(&mut self) { let textures_delta = std::mem::take(&mut self.textures_delta); + let clipped_primitives = std::mem::take(&mut self.clipped_primitives); - 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, - )?; - - Ok(()) + if let Some(clipped_primitives) = clipped_primitives { + 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, + ) { + log::error!("Failed to paint: {}", super::string_from_js_value(&err)); + } + } } fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) { diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 8c5edec14..2dc3af4e9 100644 --- a/crates/eframe/src/web/backend.rs +++ b/crates/eframe/src/web/backend.rs @@ -23,12 +23,17 @@ pub(crate) struct WebInput { impl WebInput { pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput { - egui::RawInput { + let mut raw_input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)), - pixels_per_point: Some(super::native_pixels_per_point()), // We ALWAYS use the native pixels-per-point time: Some(super::now_sec()), ..self.raw.take() - } + }; + raw_input + .viewports + .entry(egui::ViewportId::ROOT) + .or_default() + .native_pixels_per_point = Some(super::native_pixels_per_point()); + raw_input } pub fn on_web_page_focus_change(&mut self, focused: bool) { @@ -68,6 +73,10 @@ impl NeedRepaint { *repaint_time = repaint_time.min(super::now_sec() + num_seconds); } + pub fn needs_repaint(&self) -> bool { + self.when_to_repaint() <= super::now_sec() + } + pub fn repaint_asap(&self) { *self.0.lock() = f64::NEG_INFINITY; } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index c8eb0e73d..bd8bc88a6 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -8,22 +8,19 @@ use super::*; fn paint_and_schedule(runner_ref: &WebRunner) -> Result<(), JsValue> { // Only paint and schedule if there has been no panic if let Some(mut runner_lock) = runner_ref.try_lock() { - paint_if_needed(&mut runner_lock)?; + paint_if_needed(&mut runner_lock); drop(runner_lock); request_animation_frame(runner_ref.clone())?; } - Ok(()) } -fn paint_if_needed(runner: &mut AppRunner) -> Result<(), JsValue> { - if runner.needs_repaint.when_to_repaint() <= now_sec() { +fn paint_if_needed(runner: &mut AppRunner) { + if runner.needs_repaint.needs_repaint() { runner.needs_repaint.clear(); - let clipped_primitives = runner.logic(); - runner.paint(&clipped_primitives)?; - runner.auto_save_if_needed(); + runner.run_and_paint(); } - Ok(()) + runner.auto_save_if_needed(); } pub(crate) fn request_animation_frame(runner_ref: WebRunner) -> Result<(), JsValue> { @@ -177,10 +174,14 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa "cut", |event: web_sys::ClipboardEvent, runner| { runner.input.raw.events.push(egui::Event::Cut); + // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: - runner.logic(); // we ignore the returned triangles, but schedule a repaint right after + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + event.stop_propagation(); event.prevent_default(); }, @@ -192,10 +193,14 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa "copy", |event: web_sys::ClipboardEvent, runner| { runner.input.raw.events.push(egui::Event::Copy); + // In Safari we are only allowed to write to the clipboard during the // event callback, which is why we run the app logic here and now: - runner.logic(); // we ignore the returned triangles, but schedule a repaint right after + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); + event.stop_propagation(); event.prevent_default(); }, @@ -281,6 +286,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu pressed: true, modifiers, }); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); } event.stop_propagation(); @@ -310,6 +321,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu pressed: false, modifiers, }); + + // In Safari we are only allowed to write to the clipboard during the + // event callback, which is why we run the app logic here and now: + runner.logic(); + + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); text_agent::update_text_agent(runner); diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index 252dddd30..85e50ea23 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -49,6 +49,10 @@ use crate::Theme; // ---------------------------------------------------------------------------- +pub(crate) fn string_from_js_value(value: &JsValue) -> String { + value.as_string().unwrap_or_else(|| format!("{value:#?}")) +} + /// Current time in seconds (since undefined point in time). /// /// Monotonically increasing. @@ -196,7 +200,7 @@ fn set_clipboard_text(s: &str) { let future = wasm_bindgen_futures::JsFuture::from(promise); let future = async move { if let Err(err) = future.await { - log::error!("Copy/cut action failed: {err:?}"); + log::error!("Copy/cut action failed: {}", string_from_js_value(&err)); } }; wasm_bindgen_futures::spawn_local(future); diff --git a/crates/eframe/src/web/web_runner.rs b/crates/eframe/src/web/web_runner.rs index 31ed14646..67d05b246 100644 --- a/crates/eframe/src/web/web_runner.rs +++ b/crates/eframe/src/web/web_runner.rs @@ -95,7 +95,10 @@ impl WebRunner { log::debug!("Unsubscribing from {} events", events_to_unsubscribe.len()); for x in events_to_unsubscribe { if let Err(err) = x.unsubscribe() { - log::warn!("Failed to unsubscribe from event: {err:?}"); + log::warn!( + "Failed to unsubscribe from event: {}", + super::string_from_js_value(&err) + ); } } } diff --git a/crates/egui-wgpu/CHANGELOG.md b/crates/egui-wgpu/CHANGELOG.md index 6d8ba691e..6633f9a3c 100644 --- a/crates/egui-wgpu/CHANGELOG.md +++ b/crates/egui-wgpu/CHANGELOG.md @@ -6,6 +6,13 @@ This file is updated upon each release. Changes since the last release can be found by running the `scripts/generate_changelog.py` script. + +## 0.24.0 - 2023-11-23 +* Updated to wgpu 0.18 [#3505](https://github.com/emilk/egui/pull/3505) (thanks [@Wumpf](https://github.com/Wumpf)!) +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Properly clamp and round viewport values, preventing rare warnings [#3604](https://github.com/emilk/egui/pull/3604) (thanks [@Wumpf](https://github.com/Wumpf)!) + + ## 0.23.0 - 2023-09-27 * Update to `wgpu` 0.17.0 [#3170](https://github.com/emilk/egui/pull/3170) (thanks [@Aaron1011](https://github.com/Aaron1011)!) * Improved wgpu callbacks [#3253](https://github.com/emilk/egui/pull/3253) (thanks [@Wumpf](https://github.com/Wumpf)!) diff --git a/crates/egui-wgpu/Cargo.toml b/crates/egui-wgpu/Cargo.toml index 2e0305c9d..5e9d5812d 100644 --- a/crates/egui-wgpu/Cargo.toml +++ b/crates/egui-wgpu/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "egui-wgpu" -version = "0.23.0" +version.workspace = true description = "Bindings for using egui natively using the wgpu library" authors = [ "Nils Hasenbanck ", "embotech ", "Emil Ernerfeldt ", ] -edition = "2021" -rust-version = "1.72" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/egui-wgpu" categories = ["gui", "game-development"] @@ -36,8 +36,8 @@ winit = ["dep:winit"] [dependencies] -egui = { version = "0.23.0", path = "../egui", default-features = false } -epaint = { version = "0.23.0", path = "../epaint", default-features = false, features = [ +egui = { version = "0.24.0", path = "../egui", default-features = false } +epaint = { version = "0.24.0", path = "../epaint", default-features = false, features = [ "bytemuck", ] } diff --git a/crates/egui-wgpu/src/renderer.rs b/crates/egui-wgpu/src/renderer.rs index f6817ee97..bf65d6afd 100644 --- a/crates/egui-wgpu/src/renderer.rs +++ b/crates/egui-wgpu/src/renderer.rs @@ -475,10 +475,10 @@ impl Renderer { let viewport_px = info.viewport_in_pixels(); render_pass.set_viewport( - viewport_px.left_px, - viewport_px.top_px, - viewport_px.width_px, - viewport_px.height_px, + viewport_px.left_px as f32, + viewport_px.top_px as f32, + viewport_px.width_px as f32, + viewport_px.height_px as f32, 0.0, 1.0, ); diff --git a/crates/egui-winit/CHANGELOG.md b/crates/egui-winit/CHANGELOG.md index ecb1de768..1d953162d 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 by running the `scripts/generate_changelog.py` script. +## 0.24.0 - 2023-11-23 +* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) +* Some breaking changes required for multi-viewport support + + ## 0.23.0 - 2023-09-27 * Only show on-screen-keyboard and IME when editing text [#3362](https://github.com/emilk/egui/pull/3362) (thanks [@Barugon](https://github.com/Barugon)!) * Replace `instant` with `web_time` [#3296](https://github.com/emilk/egui/pull/3296) diff --git a/crates/egui-winit/Cargo.toml b/crates/egui-winit/Cargo.toml index ad3d4a388..1cd34a083 100644 --- a/crates/egui-winit/Cargo.toml +++ b/crates/egui-winit/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "egui-winit" -version = "0.23.0" +version.workspace = true authors = ["Emil Ernerfeldt "] description = "Bindings for using egui with winit" -edition = "2021" -rust-version = "1.72" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui/tree/master/crates/egui-winit" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "README.md" repository = "https://github.com/emilk/egui/tree/master/crates/egui-winit" categories = ["gui", "game-development"] @@ -55,7 +55,7 @@ wayland = ["winit/wayland", "bytemuck"] x11 = ["winit/x11", "bytemuck"] [dependencies] -egui = { version = "0.23.0", path = "../egui", default-features = false, features = [ +egui = { version = "0.24.0", path = "../egui", default-features = false, features = [ "log", ] } log = { version = "0.4", features = ["std"] } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 3669cc262..7e1ae5e24 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -14,9 +14,7 @@ pub use accesskit_winit; pub use egui; #[cfg(feature = "accesskit")] use egui::accesskit; -use egui::{ - Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportIdPair, ViewportInfo, -}; +use egui::{Pos2, Rect, Vec2, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo}; pub use winit; pub mod clipboard; @@ -26,9 +24,14 @@ pub use window_settings::WindowSettings; use raw_window_handle::HasRawDisplayHandle; -pub fn native_pixels_per_point(window: &Window) -> f32 { - window.scale_factor() as f32 -} +#[allow(unused_imports)] +pub(crate) use profiling_scopes::*; + +use winit::{ + dpi::{PhysicalPosition, PhysicalSize}, + event_loop::EventLoopWindowTarget, + window::{CursorGrabMode, Window, WindowButtons, WindowLevel}, +}; pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 { let size = window.inner_size(); @@ -59,6 +62,7 @@ pub struct EventResponse { /// /// Instantiate one of these per viewport/window. pub struct State { + viewport_id: ViewportId, start_time: web_time::Instant, egui_input: egui::RawInput, pointer_pos_in_points: Option, @@ -93,6 +97,7 @@ pub struct State { impl State { /// Construct a new instance pub fn new( + viewport_id: ViewportId, display_target: &dyn HasRawDisplayHandle, native_pixels_per_point: Option, max_texture_side: Option, @@ -105,12 +110,13 @@ impl State { }; let mut slf = Self { + viewport_id, start_time: web_time::Instant::now(), egui_input, pointer_pos_in_points: None, any_pointer_button_down: false, current_cursor_icon: None, - current_pixels_per_point: 1.0, + current_pixels_per_point: native_pixels_per_point.unwrap_or(1.0), clipboard: clipboard::Clipboard::new(display_target), @@ -124,9 +130,13 @@ impl State { allow_ime: false, }; - if let Some(native_pixels_per_point) = native_pixels_per_point { - slf.set_pixels_per_point(native_pixels_per_point); - } + + slf.egui_input + .viewports + .entry(ViewportId::ROOT) + .or_default() + .native_pixels_per_point = native_pixels_per_point; + if let Some(max_texture_side) = max_texture_side { slf.set_max_texture_side(max_texture_side); } @@ -154,19 +164,6 @@ impl State { self.egui_input.max_texture_side = Some(max_texture_side); } - /// Call this when a new native Window is created for rendering to initialize the `pixels_per_point` - /// for that window. - /// - /// In particular, on Android it is necessary to call this after each `Resumed` lifecycle - /// event, each time a new native window is created. - /// - /// Once this has been initialized for a new window then this state will be maintained by handling - /// [`winit::event::WindowEvent::ScaleFactorChanged`] events. - pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) { - self.egui_input.pixels_per_point = Some(pixels_per_point); - self.current_pixels_per_point = pixels_per_point; - } - /// The number of physical pixels per logical point, /// as configured on the current egui context (see [`egui::Context::pixels_per_point`]). #[inline] @@ -192,7 +189,7 @@ impl State { /// /// Call before [`Self::update_viewport_info`] pub fn update_viewport_info(&self, info: &mut ViewportInfo, window: &Window) { - update_viewport_info(info, window, self.pixels_per_point()); + update_viewport_info(info, window, self.current_pixels_per_point); } /// Prepare for a new frame by extracting the accumulated input, @@ -202,26 +199,30 @@ impl State { /// You need to set [`egui::RawInput::viewports`] yourself though. /// Use [`Self::update_viewport_info`] to update the info for each /// viewport. - pub fn take_egui_input(&mut self, window: &Window, ids: ViewportIdPair) -> egui::RawInput { + pub fn take_egui_input(&mut self, window: &Window) -> egui::RawInput { crate::profile_function!(); - let pixels_per_point = self.pixels_per_point(); - self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); // On Windows, a minimized window will have 0 width and height. // See: https://github.com/rust-windowing/winit/issues/208 // This solves an issue where egui window positions would be changed when minimizing on Windows. let screen_size_in_pixels = screen_size_in_pixels(window); - let screen_size_in_points = screen_size_in_pixels / pixels_per_point; + let screen_size_in_points = screen_size_in_pixels / self.current_pixels_per_point; self.egui_input.screen_rect = (screen_size_in_points.x > 0.0 && screen_size_in_points.y > 0.0) .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points)); // Tell egui which viewport is now active: - self.egui_input.viewport_ids = ids; - self.egui_input.native_pixels_per_point = Some(native_pixels_per_point(window)); + self.egui_input.viewport_id = self.viewport_id; + + self.egui_input + .viewports + .entry(self.viewport_id) + .or_default() + .native_pixels_per_point = Some(window.scale_factor() as f32); + self.egui_input.take() } @@ -238,9 +239,14 @@ impl State { use winit::event::WindowEvent; match event { WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - let pixels_per_point = *scale_factor as f32; - self.egui_input.pixels_per_point = Some(pixels_per_point); - self.current_pixels_per_point = pixels_per_point; + let native_pixels_per_point = *scale_factor as f32; + + self.egui_input + .viewports + .entry(self.viewport_id) + .or_default() + .native_pixels_per_point = Some(native_pixels_per_point); + self.current_pixels_per_point = egui_ctx.zoom_factor() * native_pixels_per_point; EventResponse { repaint: true, consumed: false, @@ -695,7 +701,6 @@ impl State { pub fn handle_platform_output( &mut self, window: &Window, - viewport_id: ViewportId, egui_ctx: &egui::Context, platform_output: egui::PlatformOutput, ) { @@ -712,7 +717,7 @@ impl State { accesskit_update, } = platform_output; - self.current_pixels_per_point = egui_ctx.input_for(viewport_id, |i| i.pixels_per_point); // someone can have changed it to scale the UI + self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI self.set_cursor_icon(window, cursor_icon); @@ -838,15 +843,21 @@ fn update_viewport_info(viewport_info: &mut ViewportInfo, window: &Window, pixel } }; - viewport_info.title = Some(window.title()); - viewport_info.pixels_per_point = pixels_per_point; - viewport_info.monitor_size = monitor_size; - viewport_info.inner_rect = inner_rect; - viewport_info.outer_rect = outer_rect; - viewport_info.fullscreen = Some(window.fullscreen().is_some()); viewport_info.focused = Some(window.has_focus()); - viewport_info.minimized = window.is_minimized().or(viewport_info.minimized); - viewport_info.maximized = Some(window.is_maximized()); + viewport_info.fullscreen = Some(window.fullscreen().is_some()); + viewport_info.inner_rect = inner_rect; + viewport_info.monitor_size = monitor_size; + viewport_info.native_pixels_per_point = Some(window.scale_factor() as f32); + viewport_info.outer_rect = outer_rect; + viewport_info.title = Some(window.title()); + + if false { + // It's tempting to do this, but it leads to a deadlock on Mac when running + // `cargo run -p custom_window_frame`. + // See https://github.com/emilk/egui/issues/3494 + viewport_info.maximized = Some(window.is_maximized()); + viewport_info.minimized = window.is_minimized().or(viewport_info.minimized); + } } fn open_url_in_browser(_url: &str) { @@ -1035,185 +1046,233 @@ fn translate_cursor(cursor_icon: egui::CursorIcon) -> Option, window: &Window, is_viewport_focused: bool, screenshot_requested: &mut bool, +) { + for command in commands { + process_viewport_command( + egui_ctx, + window, + command, + info, + is_viewport_focused, + screenshot_requested, + ); + } +} + +fn process_viewport_command( + egui_ctx: &egui::Context, + window: &Window, + command: ViewportCommand, + info: &mut ViewportInfo, + is_viewport_focused: bool, + screenshot_requested: &mut bool, ) { crate::profile_function!(); use winit::window::ResizeDirection; - for command in commands { - match command { - ViewportCommand::Close => { - info.events.push(egui::ViewportEvent::Close); - } - ViewportCommand::StartDrag => { - // If `is_viewport_focused` is not checked on x11 the input will be permanently taken until the app is killed! + log::debug!("Processing ViewportCommand::{command:?}"); - // TODO: check that the left mouse-button was pressed down recently, - // or we will have bugs on Windows. - // See https://github.com/emilk/egui/pull/1108 - if is_viewport_focused { - if let Err(err) = window.drag_window() { - log::warn!("{command:?}: {err}"); - } - } - } - ViewportCommand::InnerSize(size) => { - let width = size.x.max(1.0); - let height = size.y.max(1.0); - let _ = window.request_inner_size(LogicalSize::new(width, height)); - } - ViewportCommand::BeginResize(direction) => { - if let Err(err) = window.drag_resize_window(match direction { - egui::viewport::ResizeDirection::North => ResizeDirection::North, - egui::viewport::ResizeDirection::South => ResizeDirection::South, - egui::viewport::ResizeDirection::West => ResizeDirection::West, - egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast, - egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast, - egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest, - egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest, - }) { + let egui_zoom_factor = egui_ctx.zoom_factor(); + let pixels_per_point = egui_zoom_factor * window.scale_factor() as f32; + + match command { + ViewportCommand::Close => { + info.events.push(egui::ViewportEvent::Close); + } + ViewportCommand::CancelClose => { + // Need to be handled elsewhere + } + ViewportCommand::StartDrag => { + // If `is_viewport_focused` is not checked on x11 the input will be permanently taken until the app is killed! + + // TODO: check that the left mouse-button was pressed down recently, + // or we will have bugs on Windows. + // See https://github.com/emilk/egui/pull/1108 + if is_viewport_focused { + if let Err(err) = window.drag_window() { log::warn!("{command:?}: {err}"); } } - ViewportCommand::Title(title) => { - window.set_title(&title); + } + ViewportCommand::InnerSize(size) => { + let width_px = pixels_per_point * size.x.max(1.0); + let height_px = pixels_per_point * size.y.max(1.0); + window.set_inner_size(PhysicalSize::new(width_px, height_px)); + } + ViewportCommand::BeginResize(direction) => { + if let Err(err) = window.drag_resize_window(match direction { + egui::viewport::ResizeDirection::North => ResizeDirection::North, + egui::viewport::ResizeDirection::South => ResizeDirection::South, + egui::viewport::ResizeDirection::West => ResizeDirection::West, + egui::viewport::ResizeDirection::NorthEast => ResizeDirection::NorthEast, + egui::viewport::ResizeDirection::SouthEast => ResizeDirection::SouthEast, + egui::viewport::ResizeDirection::NorthWest => ResizeDirection::NorthWest, + egui::viewport::ResizeDirection::SouthWest => ResizeDirection::SouthWest, + }) { + log::warn!("{command:?}: {err}"); } - ViewportCommand::Transparent(v) => window.set_transparent(v), - ViewportCommand::Visible(v) => window.set_visible(v), - ViewportCommand::OuterPosition(pos) => { - window.set_outer_position(LogicalPosition::new(pos.x, pos.y)); + } + ViewportCommand::Title(title) => { + window.set_title(&title); + } + ViewportCommand::Transparent(v) => window.set_transparent(v), + ViewportCommand::Visible(v) => window.set_visible(v), + ViewportCommand::OuterPosition(pos) => { + window.set_outer_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )); + } + ViewportCommand::MinInnerSize(s) => { + window.set_min_inner_size((s.is_finite() && s != Vec2::ZERO).then_some( + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), + )); + } + ViewportCommand::MaxInnerSize(s) => { + window.set_max_inner_size((s.is_finite() && s != Vec2::INFINITY).then_some( + PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y), + )); + } + ViewportCommand::ResizeIncrements(s) => { + window.set_resize_increments( + s.map(|s| PhysicalSize::new(pixels_per_point * s.x, pixels_per_point * s.y)), + ); + } + ViewportCommand::Resizable(v) => window.set_resizable(v), + ViewportCommand::EnableButtons { + close, + minimized, + maximize, + } => window.set_enabled_buttons( + if close { + WindowButtons::CLOSE + } else { + WindowButtons::empty() + } | if minimized { + WindowButtons::MINIMIZE + } else { + WindowButtons::empty() + } | if maximize { + WindowButtons::MAXIMIZE + } else { + WindowButtons::empty() + }, + ), + ViewportCommand::Minimized(v) => { + window.set_minimized(v); + info.minimized = Some(v); + } + ViewportCommand::Maximized(v) => { + window.set_maximized(v); + info.maximized = Some(v); + } + ViewportCommand::Fullscreen(v) => { + window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None))); + } + ViewportCommand::Decorations(v) => window.set_decorations(v), + ViewportCommand::WindowLevel(l) => window.set_window_level(match l { + egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, + egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, + egui::viewport::WindowLevel::Normal => WindowLevel::Normal, + }), + ViewportCommand::Icon(icon) => { + window.set_window_icon(icon.map(|icon| { + winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) + .expect("Invalid ICON data!") + })); + } + ViewportCommand::IMEPosition(pos) => { + window.set_ime_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )); + } + ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), + ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { + egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, + egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, + egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, + }), + ViewportCommand::Focus => { + if !window.has_focus() { + window.focus_window(); } - ViewportCommand::MinInnerSize(s) => { - window.set_min_inner_size( - (s.is_finite() && s != Vec2::ZERO).then_some(LogicalSize::new(s.x, s.y)), - ); - } - ViewportCommand::MaxInnerSize(s) => { - window.set_max_inner_size( - (s.is_finite() && s != Vec2::INFINITY).then_some(LogicalSize::new(s.x, s.y)), - ); - } - ViewportCommand::ResizeIncrements(s) => { - window.set_resize_increments(s.map(|s| LogicalSize::new(s.x, s.y))); - } - ViewportCommand::Resizable(v) => window.set_resizable(v), - ViewportCommand::EnableButtons { - close, - minimized, - maximize, - } => window.set_enabled_buttons( - if close { - WindowButtons::CLOSE - } else { - WindowButtons::empty() - } | if minimized { - WindowButtons::MINIMIZE - } else { - WindowButtons::empty() - } | if maximize { - WindowButtons::MAXIMIZE - } else { - WindowButtons::empty() - }, - ), - ViewportCommand::Minimized(v) => { - window.set_minimized(v); - info.minimized = Some(v); - } - ViewportCommand::Maximized(v) => { - window.set_maximized(v); - info.maximized = Some(v); - } - ViewportCommand::Fullscreen(v) => { - window.set_fullscreen(v.then_some(winit::window::Fullscreen::Borderless(None))); - } - ViewportCommand::Decorations(v) => window.set_decorations(v), - ViewportCommand::WindowLevel(l) => window.set_window_level(match l { - egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, - egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, - egui::viewport::WindowLevel::Normal => WindowLevel::Normal, - }), - ViewportCommand::Icon(icon) => { - window.set_window_icon(icon.map(|icon| { - winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) - .expect("Invalid ICON data!") - })); - } - ViewportCommand::IMEPosition(pos) => { - window.set_ime_cursor_area( - LogicalPosition::new(pos.x, pos.y), - winit::dpi::LogicalSize { - // TODO: What size to use? New size arg in winit 0.29 - width: 10, - height: 10, - }, - ); - } - ViewportCommand::IMEAllowed(v) => window.set_ime_allowed(v), - ViewportCommand::IMEPurpose(p) => window.set_ime_purpose(match p { - egui::viewport::IMEPurpose::Password => winit::window::ImePurpose::Password, - egui::viewport::IMEPurpose::Terminal => winit::window::ImePurpose::Terminal, - egui::viewport::IMEPurpose::Normal => winit::window::ImePurpose::Normal, - }), - ViewportCommand::Focus => { - if !window.has_focus() { - window.focus_window(); + } + ViewportCommand::RequestUserAttention(a) => { + window.request_user_attention(match a { + egui::UserAttentionType::Reset => None, + egui::UserAttentionType::Critical => { + Some(winit::window::UserAttentionType::Critical) } - } - ViewportCommand::RequestUserAttention(a) => { - window.request_user_attention(match a { - egui::UserAttentionType::Reset => None, - egui::UserAttentionType::Critical => { - Some(winit::window::UserAttentionType::Critical) - } - egui::UserAttentionType::Informational => { - Some(winit::window::UserAttentionType::Informational) - } - }); - } - ViewportCommand::SetTheme(t) => window.set_theme(match t { - egui::SystemTheme::Light => Some(winit::window::Theme::Light), - egui::SystemTheme::Dark => Some(winit::window::Theme::Dark), - egui::SystemTheme::SystemDefault => None, - }), - ViewportCommand::ContentProtected(v) => window.set_content_protected(v), - ViewportCommand::CursorPosition(pos) => { - if let Err(err) = window.set_cursor_position(LogicalPosition::new(pos.x, pos.y)) { - log::warn!("{command:?}: {err}"); + egui::UserAttentionType::Informational => { + Some(winit::window::UserAttentionType::Informational) } + }); + } + ViewportCommand::SetTheme(t) => window.set_theme(match t { + egui::SystemTheme::Light => Some(winit::window::Theme::Light), + egui::SystemTheme::Dark => Some(winit::window::Theme::Dark), + egui::SystemTheme::SystemDefault => None, + }), + ViewportCommand::ContentProtected(v) => window.set_content_protected(v), + ViewportCommand::CursorPosition(pos) => { + if let Err(err) = window.set_cursor_position(PhysicalPosition::new( + pixels_per_point * pos.x, + pixels_per_point * pos.y, + )) { + log::warn!("{command:?}: {err}"); } - ViewportCommand::CursorGrab(o) => { - if let Err(err) = window.set_cursor_grab(match o { - egui::viewport::CursorGrab::None => CursorGrabMode::None, - egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined, - egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked, - }) { - log::warn!("{command:?}: {err}"); - } + } + ViewportCommand::CursorGrab(o) => { + if let Err(err) = window.set_cursor_grab(match o { + egui::viewport::CursorGrab::None => CursorGrabMode::None, + egui::viewport::CursorGrab::Confined => CursorGrabMode::Confined, + egui::viewport::CursorGrab::Locked => CursorGrabMode::Locked, + }) { + log::warn!("{command:?}: {err}"); } - ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v), - ViewportCommand::MousePassthrough(passthrough) => { - if let Err(err) = window.set_cursor_hittest(!passthrough) { - log::warn!("{command:?}: {err}"); - } - } - ViewportCommand::Screenshot => { - *screenshot_requested = true; + } + ViewportCommand::CursorVisible(v) => window.set_cursor_visible(v), + ViewportCommand::MousePassthrough(passthrough) => { + if let Err(err) = window.set_cursor_hittest(!passthrough) { + log::warn!("{command:?}: {err}"); } } + ViewportCommand::Screenshot => { + *screenshot_requested = true; + } } } -pub fn create_winit_window_builder( +pub fn create_winit_window_builder( + egui_ctx: &egui::Context, + event_loop: &EventLoopWindowTarget, viewport_builder: ViewportBuilder, ) -> winit::window::WindowBuilder { crate::profile_function!(); + // 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. + let native_pixels_per_point = event_loop + .primary_monitor() + .or_else(|| event_loop.available_monitors().next()) + .map_or_else( + || { + log::debug!("Failed to find a monitor - assuming native_pixels_per_point of 1.0"); + 1.0 + }, + |m| m.scale_factor() as f32, + ); + let zoom_factor = egui_ctx.zoom_factor(); + let pixels_per_point = zoom_factor * native_pixels_per_point; + let ViewportBuilder { title, position, @@ -1233,13 +1292,18 @@ pub fn create_winit_window_builder( maximize_button, window_level, - // only handled on some platforms: - title_hidden: _title_hidden, - titlebar_transparent: _titlebar_transparent, + // macOS: fullsize_content_view: _fullsize_content_view, - app_id: _app_id, + title_shown: _title_shown, + titlebar_buttons_shown: _titlebar_buttons_shown, + titlebar_shown: _titlebar_shown, + + // Windows: drag_and_drop: _drag_and_drop, + // wayland: + app_id: _app_id, + mouse_passthrough: _, // handled in `apply_viewport_builder_to_new_window` } = viewport_builder; @@ -1250,7 +1314,7 @@ pub fn create_winit_window_builder( .with_resizable(resizable.unwrap_or(true)) .with_visible(visible.unwrap_or(true)) .with_maximized(maximized.unwrap_or(false)) - .with_window_level(match window_level { + .with_window_level(match window_level.unwrap_or_default() { egui::viewport::WindowLevel::AlwaysOnBottom => WindowLevel::AlwaysOnBottom, egui::viewport::WindowLevel::AlwaysOnTop => WindowLevel::AlwaysOnTop, egui::viewport::WindowLevel::Normal => WindowLevel::Normal, @@ -1274,27 +1338,31 @@ pub fn create_winit_window_builder( .with_active(active.unwrap_or(true)); if let Some(inner_size) = inner_size { - window_builder = window_builder - .with_inner_size(winit::dpi::LogicalSize::new(inner_size.x, inner_size.y)); + window_builder = window_builder.with_inner_size(PhysicalSize::new( + pixels_per_point * inner_size.x, + pixels_per_point * inner_size.y, + )); } if let Some(min_inner_size) = min_inner_size { - window_builder = window_builder.with_min_inner_size(winit::dpi::LogicalSize::new( - min_inner_size.x, - min_inner_size.y, + window_builder = window_builder.with_min_inner_size(PhysicalSize::new( + pixels_per_point * min_inner_size.x, + pixels_per_point * min_inner_size.y, )); } if let Some(max_inner_size) = max_inner_size { - window_builder = window_builder.with_max_inner_size(winit::dpi::LogicalSize::new( - max_inner_size.x, - max_inner_size.y, + window_builder = window_builder.with_max_inner_size(PhysicalSize::new( + pixels_per_point * max_inner_size.x, + pixels_per_point * max_inner_size.y, )); } if let Some(position) = position { - window_builder = - window_builder.with_position(winit::dpi::LogicalPosition::new(position.x, position.y)); + window_builder = window_builder.with_position(PhysicalPosition::new( + pixels_per_point * position.x, + pixels_per_point * position.y, + )); } if let Some(icon) = icon { @@ -1320,8 +1388,9 @@ pub fn create_winit_window_builder( { use winit::platform::macos::WindowBuilderExtMacOS as _; window_builder = window_builder - .with_title_hidden(_title_hidden.unwrap_or(false)) - .with_titlebar_transparent(_titlebar_transparent.unwrap_or(false)) + .with_title_hidden(!_title_shown.unwrap_or(true)) + .with_titlebar_buttons_hidden(!_titlebar_buttons_shown.unwrap_or(true)) + .with_titlebar_transparent(!_titlebar_shown.unwrap_or(true)) .with_fullsize_content_view(_fullsize_content_view.unwrap_or(false)); } @@ -1432,10 +1501,3 @@ mod profiling_scopes { } pub(crate) use profile_scope; } - -#[allow(unused_imports)] -pub(crate) use profiling_scopes::*; -use winit::{ - dpi::{LogicalPosition, LogicalSize}, - window::{CursorGrabMode, Window, WindowButtons, WindowLevel}, -}; diff --git a/crates/egui-winit/src/window_settings.rs b/crates/egui-winit/src/window_settings.rs index 3659d9586..c59a0f451 100644 --- a/crates/egui-winit/src/window_settings.rs +++ b/crates/egui-winit/src/window_settings.rs @@ -18,8 +18,10 @@ pub struct WindowSettings { } impl WindowSettings { - pub fn from_display(window: &winit::window::Window) -> Self { - let inner_size_points = window.inner_size().to_logical::(window.scale_factor()); + pub fn from_window(egui_zoom_factor: f32, window: &winit::window::Window) -> Self { + let inner_size_points = window + .inner_size() + .to_logical::(egui_zoom_factor as f64 * window.scale_factor()); let inner_position_pixels = window .inner_position() @@ -100,6 +102,7 @@ impl WindowSettings { pub fn clamp_position_to_monitors( &mut self, + egui_zoom_factor: f32, event_loop: &winit::event_loop::EventLoopWindowTarget, ) { // If the app last ran on two monitors and only one is now connected, then @@ -116,15 +119,16 @@ impl WindowSettings { }; if let Some(pos_px) = &mut self.inner_position_pixels { - clamp_pos_to_monitors(event_loop, inner_size_points, pos_px); + clamp_pos_to_monitors(egui_zoom_factor, event_loop, inner_size_points, pos_px); } if let Some(pos_px) = &mut self.outer_position_pixels { - clamp_pos_to_monitors(event_loop, inner_size_points, pos_px); + clamp_pos_to_monitors(egui_zoom_factor, event_loop, inner_size_points, pos_px); } } } fn clamp_pos_to_monitors( + egui_zoom_factor: f32, event_loop: &winit::event_loop::EventLoopWindowTarget, window_size_pts: egui::Vec2, position_px: &mut egui::Pos2, @@ -142,7 +146,7 @@ fn clamp_pos_to_monitors( }; for monitor in monitors { - let window_size_px = window_size_pts * (monitor.scale_factor() as f32); + let window_size_px = window_size_pts * (egui_zoom_factor * monitor.scale_factor() as f32); let monitor_x_range = (monitor.position().x - window_size_px.x as i32) ..(monitor.position().x + monitor.size().width as i32); let monitor_y_range = (monitor.position().y - window_size_px.y as i32) @@ -155,10 +159,14 @@ fn clamp_pos_to_monitors( } } - let mut window_size_px = window_size_pts * (active_monitor.scale_factor() as f32); + let mut window_size_px = + window_size_pts * (egui_zoom_factor * active_monitor.scale_factor() as f32); // Add size of title bar. This is 32 px by default in Win 10/11. if cfg!(target_os = "windows") { - window_size_px += egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32); + window_size_px += egui::Vec2::new( + 0.0, + 32.0 * egui_zoom_factor * active_monitor.scale_factor() as f32, + ); } let monitor_position = egui::Pos2::new( active_monitor.position().x as f32, diff --git a/crates/egui/Cargo.toml b/crates/egui/Cargo.toml index cb87b5b99..61e27dd72 100644 --- a/crates/egui/Cargo.toml +++ b/crates/egui/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "egui" -version = "0.23.0" +version.workspace = true authors = ["Emil Ernerfeldt "] description = "An easy-to-use immediate mode GUI that runs on both web and native" -edition = "2021" -rust-version = "1.72" +edition.workspace = true +rust-version.workspace = true homepage = "https://github.com/emilk/egui" -license = "MIT OR Apache-2.0" +license.workspace = true readme = "../../README.md" repository = "https://github.com/emilk/egui" categories = ["gui", "game-development"] @@ -76,7 +76,7 @@ unity = ["epaint/unity"] [dependencies] -epaint = { version = "0.23.0", path = "../epaint", default-features = false } +epaint = { version = "0.24.0", path = "../epaint", default-features = false } ahash = { version = "0.8.6", default-features = false, features = [ "no-rng", # we don't need DOS-protection, so we let users opt-in to it instead diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index a7702af42..52239865e 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -200,6 +200,9 @@ struct ContextImpl { animation_manager: AnimationManager, tex_manager: WrappedTextureManager, + /// Set during the frame, becomes active at the start of the next frame. + new_zoom_factor: Option, + os: OperatingSystem, /// How deeply nested are we? @@ -227,8 +230,15 @@ struct ContextImpl { impl ContextImpl { fn begin_frame_mut(&mut self, mut new_raw_input: RawInput) { - let ids = new_raw_input.viewport_ids; - let viewport_id = ids.this; + let viewport_id = new_raw_input.viewport_id; + let parent_id = new_raw_input + .viewports + .get(&viewport_id) + .and_then(|v| v.parent) + .unwrap_or_default(); + let ids = ViewportIdPair::from_self_and_parent(viewport_id, parent_id); + + let is_outermost_viewport = self.viewport_stack.is_empty(); // not necessarily root, just outermost immediate viewport self.viewport_stack.push(ids); let viewport = self.viewports.entry(viewport_id).or_default(); @@ -247,19 +257,26 @@ impl ContextImpl { } } - if let Some(new_pixels_per_point) = self.memory.override_pixels_per_point { - if viewport.input.pixels_per_point != new_pixels_per_point { - new_raw_input.pixels_per_point = Some(new_pixels_per_point); + if is_outermost_viewport { + if let Some(new_zoom_factor) = self.new_zoom_factor.take() { + let ratio = self.memory.options.zoom_factor / new_zoom_factor; + self.memory.options.zoom_factor = new_zoom_factor; let input = &viewport.input; // This is a bit hacky, but is required to avoid jitter: - let ratio = input.pixels_per_point / new_pixels_per_point; let mut rect = input.screen_rect; rect.min = (ratio * rect.min.to_vec2()).to_pos2(); rect.max = (ratio * rect.max.to_vec2()).to_pos2(); new_raw_input.screen_rect = Some(rect); + // We should really scale everything else in the input too, + // but the `screen_rect` is the most important part. } } + let pixels_per_point = self.memory.options.zoom_factor + * new_raw_input + .viewport() + .native_pixels_per_point + .unwrap_or(1.0); viewport.layer_rects_prev_frame = std::mem::take(&mut viewport.layer_rects_this_frame); @@ -270,8 +287,11 @@ impl ContextImpl { self.memory .begin_frame(&viewport.input, &new_raw_input, &all_viewport_ids); - viewport.input = std::mem::take(&mut viewport.input) - .begin_frame(new_raw_input, viewport.repaint.requested_last_frame); + viewport.input = std::mem::take(&mut viewport.input).begin_frame( + new_raw_input, + viewport.repaint.requested_last_frame, + pixels_per_point, + ); viewport.frame_state.begin_frame(&viewport.input); @@ -464,13 +484,11 @@ impl std::cmp::PartialEq for Context { impl Default for Context { fn default() -> Self { - let s = Self(Arc::new(RwLock::new(ContextImpl::default()))); - - s.write(|ctx| { - ctx.embed_viewports = true; - }); - - s + let ctx = ContextImpl { + embed_viewports: true, + ..Default::default() + }; + Self(Arc::new(RwLock::new(ctx))) } } @@ -1333,44 +1351,90 @@ impl Context { } /// The number of physical pixels for each logical point. + /// + /// This is calculated as [`Self::zoom_factor`] * [`Self::native_pixels_per_point`] #[inline(always)] pub fn pixels_per_point(&self) -> f32 { - self.input(|i| i.pixels_per_point()) + self.input(|i| i.pixels_per_point) } /// Set the number of physical pixels for each logical point. /// Will become active at the start of the next frame. /// - /// Note that this may be overwritten by input from the integration via [`RawInput::pixels_per_point`]. - /// For instance, when using `eframe` on web, the browsers native zoom level will always be used. + /// This will actually translate to a call to [`Self::set_zoom_factor`]. pub fn set_pixels_per_point(&self, pixels_per_point: f32) { if pixels_per_point != self.pixels_per_point() { - self.write(|ctx| { - ctx.memory.override_pixels_per_point = Some(pixels_per_point); - for id in ctx.all_viewport_ids() { - ctx.request_repaint(id); - } - }); + self.set_zoom_factor(pixels_per_point / self.native_pixels_per_point().unwrap_or(1.0)); } } + /// The number of physical pixels for each logical point on this monitor. + /// + /// This is given as input to egui via [`ViewportInfo::native_pixels_per_point`] + /// and cannot be changed. + #[inline(always)] + pub fn native_pixels_per_point(&self) -> Option { + self.input(|i| i.viewport().native_pixels_per_point) + } + + /// Global zoom factor of the UI. + /// + /// This is used to calculate the `pixels_per_point` + /// for the UI as `pixels_per_point = zoom_fator * native_pixels_per_point`. + /// + /// The default is 1.0. + /// Make larger to make everything larger. + #[inline(always)] + pub fn zoom_factor(&self) -> f32 { + self.options(|o| o.zoom_factor) + } + + /// Sets zoom factor of the UI. + /// Will become active at the start of the next frame. + /// + /// Note that calling this will not update [`Self::zoom_factor`] until the end of the frame. + /// + /// This is used to calculate the `pixels_per_point` + /// for the UI as `pixels_per_point = zoom_fator * native_pixels_per_point`. + /// + /// The default is 1.0. + /// Make larger to make everything larger. + /// + /// It is better to call this than modifying + /// [`Options::zoom_factor`]. + #[inline(always)] + pub fn set_zoom_factor(&self, zoom_factor: f32) { + self.write(|ctx| { + if ctx.memory.options.zoom_factor != zoom_factor { + ctx.new_zoom_factor = Some(zoom_factor); + for id in ctx.all_viewport_ids() { + ctx.request_repaint(id); + } + } + }); + } + /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_to_pixel(&self, point: f32) -> f32 { let pixels_per_point = self.pixels_per_point(); (point * pixels_per_point).round() / pixels_per_point } /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 { pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y)) } /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } /// Useful for pixel-perfect rendering + #[inline] pub(crate) fn round_rect_to_pixels(&self, rect: Rect) -> Rect { Rect { min: self.round_pos_to_pixels(rect.min), @@ -1491,6 +1555,11 @@ impl Context { #[must_use] pub fn end_frame(&self) -> FullOutput { crate::profile_function!(); + + if self.options(|o| o.zoom_with_keyboard) { + crate::gui_zoom::zoom_with_keyboard(self); + } + self.write(|ctx| ctx.end_frame()) } } diff --git a/crates/egui/src/data/input.rs b/crates/egui/src/data/input.rs index 61a50c114..50073338b 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -2,7 +2,7 @@ use epaint::ColorImage; -use crate::{emath::*, ViewportIdMap, ViewportIdPair}; +use crate::{emath::*, ViewportId, ViewportIdMap}; /// What the integrations provides to egui at the start of each frame. /// @@ -11,12 +11,15 @@ use crate::{emath::*, ViewportIdMap, ViewportIdPair}; /// You can check if `egui` is using the inputs using /// [`crate::Context::wants_pointer_input`] and [`crate::Context::wants_keyboard_input`]. /// -/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left corner. +/// All coordinates are in points (logical pixels) with origin (0, 0) in the top left .corner. +/// +/// Ii "points" can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `native_pixels_per_point`; #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct RawInput { - /// The id of the active viewport, and out parent. - pub viewport_ids: ViewportIdPair, + /// The id of the active viewport. + pub viewport_id: ViewportId, /// Information about all egui viewports. pub viewports: ViewportIdMap, @@ -31,20 +34,6 @@ pub struct RawInput { /// `None` will be treated as "same as last frame", with the default being a very big area. pub screen_rect: Option, - /// Also known as device pixel ratio, > 1 for high resolution screens. - /// - /// If text looks blurry you probably forgot to set this. - /// Set this the first frame, whenever it changes, or just on every frame. - pub pixels_per_point: Option, - - /// The OS native pixels-per-point. - /// - /// This should always be set, if known. - /// - /// On web this takes browser scaling into account, - /// and orresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. - pub native_pixels_per_point: Option, - /// Maximum size of one side of the font texture. /// /// Ask your graphics drivers about this. This corresponds to `GL_MAX_TEXTURE_SIZE`. @@ -89,11 +78,9 @@ pub struct RawInput { impl Default for RawInput { fn default() -> Self { Self { - viewport_ids: Default::default(), - viewports: Default::default(), + viewport_id: ViewportId::ROOT, + viewports: std::iter::once((ViewportId::ROOT, Default::default())).collect(), screen_rect: None, - pixels_per_point: None, - native_pixels_per_point: None, max_texture_side: None, time: None, predicted_dt: 1.0 / 60.0, @@ -107,17 +94,21 @@ impl Default for RawInput { } impl RawInput { + /// Info about the active viewport + #[inline] + pub fn viewport(&self) -> &ViewportInfo { + self.viewports.get(&self.viewport_id).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") + } + /// Helper: move volatile (deltas and events), clone the rest. /// /// * [`Self::hovered_files`] is cloned. /// * [`Self::dropped_files`] is moved. pub fn take(&mut self) -> RawInput { RawInput { - viewport_ids: self.viewport_ids, + viewport_id: self.viewport_id, viewports: self.viewports.clone(), screen_rect: self.screen_rect.take(), - pixels_per_point: self.pixels_per_point.take(), // take the diff - native_pixels_per_point: self.native_pixels_per_point, // copy max_texture_side: self.max_texture_side.take(), time: self.time.take(), predicted_dt: self.predicted_dt, @@ -132,11 +123,9 @@ impl RawInput { /// Add on new input. pub fn append(&mut self, newer: Self) { let Self { - viewport_ids, + viewport_id: viewport_ids, viewports, screen_rect, - pixels_per_point, - native_pixels_per_point, max_texture_side, time, predicted_dt, @@ -147,11 +136,9 @@ impl RawInput { focused, } = newer; - self.viewport_ids = viewport_ids; + self.viewport_id = viewport_ids; self.viewports = viewports; self.screen_rect = screen_rect.or(self.screen_rect); - self.pixels_per_point = pixels_per_point.or(self.pixels_per_point); - self.native_pixels_per_point = native_pixels_per_point.or(self.native_pixels_per_point); self.max_texture_side = max_texture_side.or(self.max_texture_side); self.time = time; // use latest time self.predicted_dt = predicted_dt; // use latest dt @@ -169,16 +156,23 @@ impl RawInput { pub enum ViewportEvent { /// The user clicked the close-button on the window, or similar. /// - /// It is up to the user to react to this by _not_ showing the viewport in the next frame in the parent viewport. + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. /// /// This even will wake up both the child and parent viewport. Close, } -/// Information about the current viewport, -/// given as input each frame. +/// Information about the current viewport, given as input each frame. /// /// `None` means "unknown". +/// +/// All units are in ui "points", which can be calculated from native physical pixels +/// using `pixels_per_point` = [`crate::Context::zoom_factor`] * `[Self::native_pixels_per_point`]; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ViewportInfo { @@ -190,8 +184,13 @@ pub struct ViewportInfo { pub events: Vec, - /// Number of physical pixels per ui point. - pub pixels_per_point: f32, + /// The OS native pixels-per-point. + /// + /// This should always be set, if known. + /// + /// On web this takes browser scaling into account, + /// and orresponds to [`window.devicePixelRatio`](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio) in JavaScript. + pub native_pixels_per_point: Option, /// Current monitor size in egui points. pub monitor_size: Option, @@ -222,6 +221,14 @@ pub struct ViewportInfo { } impl ViewportInfo { + /// This viewport has been told to close. + /// + /// If this is the root viewport, the application will exit + /// after this frame unless you send a + /// [`crate::ViewportCommand::CancelClose`] command. + /// + /// If this is not the root viewport, + /// it is up to the user to hide this viewport the next frame. pub fn close_requested(&self) -> bool { self.events .iter() @@ -233,7 +240,7 @@ impl ViewportInfo { parent, title, events, - pixels_per_point, + native_pixels_per_point, monitor_size, inner_rect, outer_rect, @@ -256,8 +263,8 @@ impl ViewportInfo { ui.label(format!("{events:?}")); ui.end_row(); - ui.label("Pixels per point:"); - ui.label(pixels_per_point.to_string()); + ui.label("Native pixels-per-point:"); + ui.label(opt_as_str(native_pixels_per_point)); ui.end_row(); ui.label("Monitor size:"); @@ -1106,11 +1113,9 @@ fn format_kb_shortcut() { impl RawInput { pub fn ui(&self, ui: &mut crate::Ui) { let Self { - viewport_ids, + viewport_id, viewports, screen_rect, - pixels_per_point, - native_pixels_per_point, max_texture_side, time, predicted_dt, @@ -1121,10 +1126,7 @@ impl RawInput { focused, } = self; - ui.label(format!( - "Active viwport: {:?}, parent: {:?}", - viewport_ids.this, viewport_ids.parent, - )); + ui.label(format!("Active viwport: {viewport_id:?}")); for (id, viewport) in viewports { ui.group(|ui| { ui.label(format!("Viewport {id:?}")); @@ -1134,16 +1136,7 @@ impl RawInput { }); } ui.label(format!("screen_rect: {screen_rect:?} points")); - ui.label(format!("pixels_per_point: {pixels_per_point:?}")) - .on_hover_text( - "Also called HDPI factor.\nNumber of physical pixels per each logical pixel.", - ); - ui.label(format!( - "native_pixels_per_point: {native_pixels_per_point:?}" - )) - .on_hover_text( - "Also called HDPI factor.\nNumber of physical pixels per each logical pixel.", - ); + ui.label(format!("max_texture_side: {max_texture_side:?}")); if let Some(time) = time { ui.label(format!("time: {time:.3} s")); diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index 20b782f7a..6c5103b36 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -27,6 +27,9 @@ pub struct FullOutput { pub pixels_per_point: f32, /// All the active viewports, including the root. + /// + /// It is up to the integration to spawn a native window for each viewport, + /// and to close any window that no longer has a viewport in this map. pub viewport_output: ViewportIdMap, } diff --git a/crates/egui/src/gui_zoom.rs b/crates/egui/src/gui_zoom.rs index a8fdf4b1f..fb4f9bd11 100644 --- a/crates/egui/src/gui_zoom.rs +++ b/crates/egui/src/gui_zoom.rs @@ -12,20 +12,14 @@ pub mod kb_shortcuts { pub const ZOOM_RESET: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Num0); } -/// Let the user scale the GUI (change `Context::pixels_per_point`) by pressing +/// Let the user scale the GUI (change [`Context::zoom_factor`]) by pressing /// Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser. /// -/// ``` -/// # let ctx = &egui::Context::default(); -/// // On web, the browser controls the gui zoom. -/// #[cfg(not(target_arch = "wasm32"))] -/// egui::gui_zoom::zoom_with_keyboard_shortcuts(ctx); -/// ``` -pub fn zoom_with_keyboard_shortcuts(ctx: &Context) { +/// By default, [`crate::Context`] calls this function at the end of each frame, +/// controllable by [`crate::Options::zoom_with_keyboard`]. +pub(crate) fn zoom_with_keyboard(ctx: &Context) { if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_RESET)) { - if let Some(native_pixels_per_point) = ctx.input(|i| i.raw.native_pixels_per_point) { - ctx.set_pixels_per_point(native_pixels_per_point); - } + ctx.set_zoom_factor(1.0); } else { if ctx.input_mut(|i| i.consume_shortcut(&kb_shortcuts::ZOOM_IN)) { zoom_in(ctx); @@ -36,47 +30,34 @@ pub fn zoom_with_keyboard_shortcuts(ctx: &Context) { } } -const MIN_PIXELS_PER_POINT: f32 = 0.2; -const MAX_PIXELS_PER_POINT: f32 = 4.0; +const MIN_ZOOM_FACTOR: f32 = 0.2; +const MAX_ZOOM_FACTOR: f32 = 5.0; -/// Make everything larger. +/// Make everything larger by increasing [`Context::zoom_factor`]. pub fn zoom_in(ctx: &Context) { - let mut pixels_per_point = ctx.pixels_per_point(); - pixels_per_point += 0.1; - pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT); - pixels_per_point = (pixels_per_point * 10.).round() / 10.; - ctx.set_pixels_per_point(pixels_per_point); + let mut zoom_factor = ctx.zoom_factor(); + zoom_factor += 0.1; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + ctx.set_zoom_factor(zoom_factor); } -/// Make everything smaller. +/// Make everything smaller by decreasing [`Context::zoom_factor`]. pub fn zoom_out(ctx: &Context) { - let mut pixels_per_point = ctx.pixels_per_point(); - pixels_per_point -= 0.1; - pixels_per_point = pixels_per_point.clamp(MIN_PIXELS_PER_POINT, MAX_PIXELS_PER_POINT); - pixels_per_point = (pixels_per_point * 10.).round() / 10.; - ctx.set_pixels_per_point(pixels_per_point); + let mut zoom_factor = ctx.zoom_factor(); + zoom_factor -= 0.1; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + ctx.set_zoom_factor(zoom_factor); } /// Show buttons for zooming the ui. /// /// This is meant to be called from within a menu (See [`Ui::menu_button`]). -/// -/// When using [`eframe`](https://github.com/emilk/egui/tree/master/crates/eframe), you want to call this as: -/// ```ignore -/// // On web, the browser controls the gui zoom. -/// if !frame.is_web() { -/// ui.menu_button("View", |ui| { -/// egui::gui_zoom::zoom_menu_buttons( -/// ui, -/// frame.info().native_pixels_per_point, -/// ); -/// }); -/// } -/// ``` -pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option) { +pub fn zoom_menu_buttons(ui: &mut Ui) { if ui .add_enabled( - ui.ctx().pixels_per_point() < MAX_PIXELS_PER_POINT, + ui.ctx().zoom_factor() < MAX_ZOOM_FACTOR, Button::new("Zoom In").shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_IN)), ) .clicked() @@ -87,7 +68,7 @@ pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option) { if ui .add_enabled( - ui.ctx().pixels_per_point() > MIN_PIXELS_PER_POINT, + ui.ctx().zoom_factor() > MIN_ZOOM_FACTOR, Button::new("Zoom Out") .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_OUT)), ) @@ -97,17 +78,15 @@ pub fn zoom_menu_buttons(ui: &mut Ui, native_pixels_per_point: Option) { ui.close_menu(); } - if let Some(native_pixels_per_point) = native_pixels_per_point { - if ui - .add_enabled( - ui.ctx().pixels_per_point() != native_pixels_per_point, - Button::new("Reset Zoom") - .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)), - ) - .clicked() - { - ui.ctx().set_pixels_per_point(native_pixels_per_point); - ui.close_menu(); - } + if ui + .add_enabled( + ui.ctx().zoom_factor() != 1.0, + Button::new("Reset Zoom") + .shortcut_text(ui.ctx().format_shortcut(&kb_shortcuts::ZOOM_RESET)), + ) + .clicked() + { + ui.ctx().set_zoom_factor(1.0); + ui.close_menu(); } } diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 79aff9a9c..693f6c56e 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -148,6 +148,7 @@ impl InputState { mut self, mut new: RawInput, requested_repaint_last_frame: bool, + pixels_per_point: f32, ) -> InputState { crate::profile_function!(); @@ -217,7 +218,7 @@ impl InputState { scroll_delta, zoom_factor_delta, screen_rect, - pixels_per_point: new.pixels_per_point.unwrap_or(self.pixels_per_point), + pixels_per_point, max_texture_side: new.max_texture_side.unwrap_or(self.max_texture_side), time, unstable_dt, @@ -232,8 +233,9 @@ impl InputState { } /// Info about the active viewport + #[inline] pub fn viewport(&self) -> &ViewportInfo { - self.raw.viewports.get(&self.raw.viewport_ids.this).expect("Failed to find current viewport in egui RawInput. This is the fault of the egui backend") + self.raw.viewport() } #[inline(always)] diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index a008ea4cd..377860057 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -336,7 +336,8 @@ #![allow(clippy::float_cmp)] #![allow(clippy::manual_range_contains)] -#![deny(unsafe_code)] +#![cfg_attr(feature = "puffin", deny(unsafe_code))] +#![cfg_attr(not(feature = "puffin"), forbid(unsafe_code))] mod animation_manager; pub mod containers; diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 973147bdc..970579806 100644 --- a/crates/egui/src/memory.rs +++ b/crates/egui/src/memory.rs @@ -71,10 +71,6 @@ pub struct Memory { pub caches: crate::util::cache::CacheStorage, // ------------------------------------------ - /// new scale that will be applied at the start of the next frame - #[cfg_attr(feature = "persistence", serde(skip))] - pub(crate) override_pixels_per_point: Option, - /// new fonts that will be applied at the start of the next frame #[cfg_attr(feature = "persistence", serde(skip))] pub(crate) new_font_definitions: Option, @@ -111,7 +107,6 @@ impl Default for Memory { options: Default::default(), data: Default::default(), caches: Default::default(), - override_pixels_per_point: Default::default(), new_font_definitions: Default::default(), interactions: Default::default(), viewport_id: Default::default(), @@ -176,6 +171,25 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) style: std::sync::Arc