Skip to content

Commit

Permalink
Introduce global zoom_factor (#3608)
Browse files Browse the repository at this point in the history
* Closes #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.
  • Loading branch information
emilk authored Nov 22, 2023
1 parent ea53246 commit 63e48dc
Show file tree
Hide file tree
Showing 26 changed files with 752 additions and 581 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
]

# -----------------------------------------------------------------------------
Expand Down
38 changes: 19 additions & 19 deletions crates/eframe/src/native/epi_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use egui_winit::{EventResponse, WindowSettings};
use crate::{epi, Theme};

pub fn viewport_builder<E>(
egui_zoom_factor: f32,
event_loop: &EventLoopWindowTarget<E>,
native_options: &mut epi::NativeOptions,
window_settings: Option<WindowSettings>,
Expand All @@ -26,8 +27,9 @@ pub fn viewport_builder<E>(
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()
Expand All @@ -37,8 +39,8 @@ pub fn viewport_builder<E>(
}

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);
}

Expand All @@ -49,9 +51,11 @@ pub fn viewport_builder<E>(
if native_options.centered {
crate::profile_scope!("center");
if let Some(monitor) = event_loop.available_monitors().next() {
let monitor_size = monitor.size().to_logical::<f32>(monitor.scale_factor());
let monitor_size = monitor
.size()
.to_logical::<f32>(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]);
Expand All @@ -76,7 +80,10 @@ pub fn apply_window_settings(
}
}

fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
fn largest_monitor_point_size<E>(
egui_zoom_factor: f32,
event_loop: &EventLoopWindowTarget<E>,
) -> egui::Vec2 {
crate::profile_function!();

let mut max_size = egui::Vec2::ZERO;
Expand All @@ -87,7 +94,9 @@ fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui:
};

for monitor in available_monitors {
let size = monitor.size().to_logical::<f32>(monitor.scale_factor());
let size = monitor
.size()
.to_logical::<f32>(egui_zoom_factor as f64 * monitor.scale_factor());
let size = egui::vec2(size.width, size.height);
max_size = max_size.max(size);
}
Expand Down Expand Up @@ -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<Theme>,
app_name: &str,
native_options: &crate::NativeOptions,
storage: Option<Box<dyn epi::Storage>>,
is_desktop: bool,
#[cfg(feature = "glow")] gl: Option<std::rc::Rc<glow::Context>>,
#[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
) -> 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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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),
);
}
}
Expand Down
56 changes: 41 additions & 15 deletions crates/eframe/src/native/glow_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -138,6 +140,7 @@ impl GlowWinitApp {

#[allow(unsafe_code)]
fn create_glutin_windowed_context(
egui_ctx: &egui::Context,
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<&dyn Storage>,
native_options: &mut NativeOptions,
Expand All @@ -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)?;
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<UserEvent>,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1095,7 +1112,11 @@ impl GlutinWindowContext {
self.gl_config.display().get_proc_address(addr)
}

fn handle_viewport_output(&mut self, viewport_output: ViewportIdMap<ViewportOutput>) {
fn handle_viewport_output(
&mut self,
egui_ctx: &egui::Context,
viewport_output: ViewportIdMap<ViewportOutput>,
) {
crate::profile_function!();

let active_viewports_ids: ViewportIdSet = viewport_output.keys().copied().collect();
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -1158,14 +1181,15 @@ impl Viewport {
}
}

fn initialize_or_update_viewport(
viewports: &mut ViewportIdMap<Viewport>,
fn initialize_or_update_viewport<'vp>(
egu_ctx: &'_ egui::Context,
viewports: &'vp mut ViewportIdMap<Viewport>,
ids: ViewportIdPair,
class: ViewportClass,
mut builder: ViewportBuilder,
viewport_ui_cb: Option<Arc<dyn Fn(&egui::Context) + Send + Sync>>,
focused_viewport: Option<ViewportId>,
) -> &mut Viewport {
) -> &'vp mut Viewport {
crate::profile_function!();

if builder.icon.is_none() {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")]
Expand Down
Loading

0 comments on commit 63e48dc

Please sign in to comment.