From 63e48dc855be13f52626347922506987a55a4f1d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Wed, 22 Nov 2023 20:34:51 +0100 Subject: [PATCH] Introduce global `zoom_factor` (#3608) * Closes https://github.com/emilk/egui/issues/3602 You can now zoom any egui app by pressing Cmd+Plus, Cmd+Minus or Cmd+0, just like in a browser. This will change the current `zoom_factor` (default 1.0) which is persisted in the egui memory, and is the same for all viewports. You can turn off the keyboard shortcuts with `ctx.options_mut(|o| o.zoom_with_keyboard = false);` `zoom_factor` can also be explicitly read/written with `ctx.zoom_factor()` and `ctx.set_zoom_factor()`. This redefines `pixels_per_point` as `zoom_factor * native_pixels_per_point`, where `native_pixels_per_point` is whatever is the native scale factor for the monitor that the current viewport is in. This adds some complexity to the interaction with winit, since we need to know the current `zoom_factor` in a lot of places, because all egui IO is done in ui points. I'm pretty sure this PR fixes a bunch of subtle bugs though that used to be in this code. `egui::gui_zoom::zoom_with_keyboard_shortcuts` is now gone, and is no longer needed, as this is now the default behavior. `Context::set_pixels_per_point` is still there, but it is recommended you use `Context::set_zoom_factor` instead. --- Cargo.lock | 2 + clippy.toml | 3 + crates/eframe/src/native/epi_integration.rs | 38 +- crates/eframe/src/native/glow_integration.rs | 56 ++- crates/eframe/src/native/wgpu_integration.rs | 120 +++-- crates/eframe/src/native/winit_integration.rs | 28 +- crates/eframe/src/web/app_runner.rs | 14 +- crates/eframe/src/web/backend.rs | 11 +- crates/egui-winit/src/lib.rs | 449 ++++++++++-------- crates/egui-winit/src/window_settings.rs | 22 +- crates/egui/src/context.rs | 99 +++- crates/egui/src/data/input.rs | 65 +-- crates/egui/src/gui_zoom.rs | 85 ++-- crates/egui/src/input_state.rs | 3 +- crates/egui/src/memory.rs | 22 +- crates/egui_demo_app/Cargo.toml | 12 +- crates/egui_demo_app/src/backend_panel.rs | 177 +++---- crates/egui_demo_app/src/main.rs | 41 ++ crates/egui_demo_app/src/wrap_app.rs | 5 - .../src/demo/demo_app_windows.rs | 7 +- crates/egui_glow/src/winit.rs | 1 + examples/puffin_profiler/Cargo.toml | 4 + examples/puffin_profiler/src/main.rs | 2 +- examples/test_viewports/src/main.rs | 1 - scripts/check.sh | 64 +-- scripts/clippy_wasm.sh | 2 +- 26 files changed, 752 insertions(+), 581 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d8465cabdef..a585c0c55d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1317,6 +1317,8 @@ dependencies = [ "image", "log", "poll-promise", + "puffin", + "puffin_http", "rfd", "serde", "wasm-bindgen", diff --git a/clippy.toml b/clippy.toml index 48a487e4bbc..1192b56ae2c 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/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 5cd245f8001..64714f2c7b7 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, @@ -245,9 +248,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); @@ -336,7 +336,7 @@ impl EpiIntegration { epi::set_value( storage, STORAGE_WINDOW_KEY, - &WindowSettings::from_display(window), + &WindowSettings::from_window(self.egui_ctx.zoom_factor(), window), ); } } diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 37146b70b0e..d3dd4cb4f49 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -24,8 +24,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::{ @@ -86,6 +86,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, @@ -138,6 +140,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, @@ -146,11 +149,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)?; @@ -190,7 +198,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, @@ -209,12 +220,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, @@ -640,7 +651,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 @@ -761,6 +772,7 @@ impl GlowWinitRunning { impl GlutinWindowContext { #[allow(unsafe_code)] unsafe fn new( + egui_ctx: &egui::Context, viewport_builder: ViewportBuilder, native_options: &NativeOptions, event_loop: &EventLoopWindowTarget, @@ -812,7 +824,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::ApiPrefence::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"); @@ -908,6 +924,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, @@ -967,7 +984,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); @@ -1095,7 +1112,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(); @@ -1115,6 +1136,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, @@ -1126,6 +1148,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, @@ -1158,14 +1181,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() { @@ -1213,6 +1237,7 @@ fn initialize_or_update_viewport( } 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, @@ -1248,6 +1273,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, @@ -1363,7 +1389,7 @@ fn render_immediate_viewport( 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 02010a4602e..43ab83c29f2 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,22 +184,20 @@ 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; - - event_loop_proxy - .lock() - .send_event(UserEvent::RequestRepaint { - when, - frame_nr, - viewport_id: info.viewport_id, - }) - .ok(); - }); + 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(); + }); } let mut egui_winit = egui_winit::State::new( @@ -208,12 +213,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")] @@ -250,6 +255,7 @@ impl WgpuWinitApp { ); let shared = Rc::new(RefCell::new(SharedState { + egui_ctx, viewport_from_window, viewports, painter, @@ -263,20 +269,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"); } @@ -374,9 +374,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( @@ -549,6 +554,7 @@ impl WgpuWinitRunning { let mut shared = shared.borrow_mut(); let SharedState { + egui_ctx, viewports, painter, viewport_from_window, @@ -578,16 +584,16 @@ impl WgpuWinitRunning { viewport_output, } = full_output; - egui_winit.handle_platform_output(window, &integration.egui_ctx, platform_output); + 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, @@ -607,7 +613,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)); @@ -755,6 +766,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, @@ -763,7 +775,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); @@ -806,6 +820,7 @@ impl Viewport { } fn create_window( + egui_ctx: &egui::Context, event_loop: &EventLoopWindowTarget, storage: Option<&dyn Storage>, native_options: &mut NativeOptions, @@ -813,11 +828,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); @@ -826,7 +846,6 @@ fn create_window( fn render_immediate_viewport( event_loop: &EventLoopWindowTarget, - egui_ctx: &egui::Context, beginning: Instant, shared: &RefCell, immediate_viewport: ImmediateViewport<'_>, @@ -841,6 +860,7 @@ fn render_immediate_viewport( let input = { let SharedState { + egui_ctx, viewports, painter, viewport_from_window, @@ -848,6 +868,7 @@ fn render_immediate_viewport( } = &mut *shared.borrow_mut(); let viewport = initialize_or_update_viewport( + egui_ctx, viewports, ids, ViewportClass::Immediate, @@ -856,7 +877,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(); @@ -873,6 +894,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 (!). @@ -924,13 +947,14 @@ fn render_immediate_viewport( false, ); - winit_state.handle_platform_output(window, 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, @@ -950,6 +974,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, @@ -961,6 +986,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, @@ -971,14 +997,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 @@ -1023,6 +1050,7 @@ fn initialize_or_update_viewport( } 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 0f4ec5e762b..53de13cc44d 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 00c3240e377..e7456649a53 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -58,6 +58,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()); @@ -112,7 +118,13 @@ impl AppRunner { }; 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) } diff --git a/crates/eframe/src/web/backend.rs b/crates/eframe/src/web/backend.rs index 8c5edec14bb..29a06339b32 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) { diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 7c9b4d2e783..b0461b8bd5d 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -24,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(); @@ -111,7 +116,7 @@ impl State { 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), @@ -125,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); } @@ -155,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] @@ -193,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, @@ -206,8 +202,6 @@ impl State { 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()); // TODO remove this in winit 0.29 @@ -220,7 +214,7 @@ impl State { // 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) @@ -228,7 +222,13 @@ impl State { // Tell egui which viewport is now active: self.egui_input.viewport_id = self.viewport_id; - self.egui_input.native_pixels_per_point = Some(native_pixels_per_point(window)); + + self.egui_input + .viewports + .entry(self.viewport_id) + .or_default() + .native_pixels_per_point = Some(window.scale_factor() as f32); + self.egui_input.take() } @@ -245,9 +245,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, @@ -709,8 +714,7 @@ impl State { accesskit_update, } = platform_output; - self.current_pixels_per_point = - egui_ctx.input_for(self.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); @@ -829,15 +833,15 @@ 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.fullscreen = Some(window.fullscreen().is_some()); + viewport_info.inner_rect = inner_rect; viewport_info.maximized = Some(window.is_maximized()); + viewport_info.minimized = window.is_minimized().or(viewport_info.minimized); + 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()); } fn open_url_in_browser(_url: &str) { @@ -1039,178 +1043,230 @@ 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! - - // 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); - window.set_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, - }) { + log::debug!("Processing ViewportCommand::{command:?}"); + + 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::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::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::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_position(LogicalPosition::new(pos.x, 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::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::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::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::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}"); + } + ViewportCommand::RequestUserAttention(a) => { + window.request_user_attention(match a { + egui::UserAttentionType::Reset => None, + egui::UserAttentionType::Critical => { + Some(winit::window::UserAttentionType::Critical) } - } - 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}"); + 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::CursorVisible(v) => window.set_cursor_visible(v), - ViewportCommand::MousePassthrough(passthrough) => { - if let Err(err) = window.set_cursor_hittest(!passthrough) { - 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::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, @@ -1271,27 +1327,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 { @@ -1430,10 +1490,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 3659d95869d..c59a0f451ce 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/src/context.rs b/crates/egui/src/context.rs index 8decb71a541..152d4999e42 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? @@ -234,6 +237,8 @@ impl ContextImpl { .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(); @@ -252,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); @@ -275,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); @@ -469,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))) } } @@ -1338,44 +1351,85 @@ 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); + 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. + /// + /// 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 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), @@ -1496,6 +1550,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 53a3d976749..598fa9caf91 100644 --- a/crates/egui/src/data/input.rs +++ b/crates/egui/src/data/input.rs @@ -11,7 +11,10 @@ use crate::{emath::*, ViewportId, ViewportIdMap}; /// 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 { @@ -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_id: 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, @@ -122,8 +109,6 @@ impl RawInput { 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, @@ -141,8 +126,6 @@ impl RawInput { viewport_id: viewport_ids, viewports, screen_rect, - pixels_per_point, - native_pixels_per_point, max_texture_side, time, predicted_dt, @@ -156,8 +139,6 @@ impl RawInput { 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 @@ -181,10 +162,12 @@ pub enum ViewportEvent { 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 { @@ -196,8 +179,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, @@ -239,7 +227,7 @@ impl ViewportInfo { parent, title, events, - pixels_per_point, + native_pixels_per_point, monitor_size, inner_rect, outer_rect, @@ -262,8 +250,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:"); @@ -1115,8 +1103,6 @@ impl RawInput { viewport_id, viewports, screen_rect, - pixels_per_point, - native_pixels_per_point, max_texture_side, time, predicted_dt, @@ -1137,16 +1123,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/gui_zoom.rs b/crates/egui/src/gui_zoom.rs index a8fdf4b1fa6..fb4f9bd11ed 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 f59c3501e81..693f6c56ee2 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, diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 92664273395..b9a9b46e076 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,21 @@ pub struct Options { #[cfg_attr(feature = "serde", serde(skip))] pub(crate) style: std::sync::Arc