diff --git a/Cargo.lock b/Cargo.lock index d8465cabd..a585c0c55 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 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/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 5cd245f80..64714f2c7 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 37146b70b..d3dd4cb4f 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 02010a460..43ab83c29 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 0f4ec5e76..53de13cc4 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..e7456649a 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 8c5edec14..29a06339b 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 7c9b4d2e7..b0461b8bd 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 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/src/context.rs b/crates/egui/src/context.rs index 8decb71a5..152d4999e 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 53a3d9767..598fa9caf 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 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 f59c3501e..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, diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index 926642733..b9a9b46e0 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