Skip to content

Commit 1676c48

Browse files
committed
egui-winit: Receive window scale factor and size via events
We found in our application (with custom `egui` renderer) that the `screen_rect` in `RawInput` was mismatching with the `winit` events that we processed earlier (in particular on resize on MacOS, which raises a lot of small size changes). One of the contributing factors was `egui-winit` reading the `Window` size and scale factor over and over again separate from processed events, causing a mismatch on some platforms because this value would already change ahead of consuming a `Resized` event. Secondly, some existing event processing may rely on this value being consistent with the event stream. There is currently no guarantee -at least I don't see it in the documentation- that `winit` ensures that `inner_size()` will always return a consistent value until the user has consumed a `Resized` event. This gets worse when certain functions (like `update_viewport_info()`) are possibly called on a separate thread because both `egui::Context` and `winit::Window` are thread-safe. Those also have an extra draw-back of roundtripping to the event loop on the main thread to query these values, as a `Window` is not actually thread-safe on all platforms.
1 parent 2115ca9 commit 1676c48

File tree

3 files changed

+80
-68
lines changed

3 files changed

+80
-68
lines changed

crates/eframe/src/native/glow_integration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,7 @@ impl GlutinWindowContext {
10491049

10501050
// Tell egui right away about native_pixels_per_point etc,
10511051
// so that the app knows about it during app creation:
1052+
// TODO: update_viewport_info could take care of this right?
10521053
let pixels_per_point = egui_winit::pixels_per_point(egui_ctx, window);
10531054

10541055
egui_ctx.input_mut(|i| {

crates/eframe/src/native/wgpu_integration.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ impl<'app> WgpuWinitApp<'app> {
205205
{
206206
// Tell egui right away about native_pixels_per_point etc,
207207
// so that the app knows about it during app creation:
208+
// TODO: update_viewport_info could take care of this right?
208209
let pixels_per_point = egui_winit::pixels_per_point(&egui_ctx, &window);
209210

210211
egui_ctx.input_mut(|i| {

crates/egui-winit/src/lib.rs

Lines changed: 78 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,9 @@ use winit::{
3232
window::{CursorGrabMode, Window, WindowButtons, WindowLevel},
3333
};
3434

35-
pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
36-
let size = if cfg!(target_os = "ios") {
37-
// `outer_size` Includes the area behind the "dynamic island".
38-
// It is up to the eframe user to make sure the dynamic island doesn't cover anything important.
39-
// That will be easier once https://github.com/rust-windowing/winit/pull/3890 lands
40-
window.outer_size()
41-
} else {
42-
window.inner_size()
43-
};
44-
egui::vec2(size.width as f32, size.height as f32)
45-
}
46-
4735
/// Calculate the `pixels_per_point` for a given window, given the current egui zoom factor
36+
///
37+
/// Only use this during startup when a [`WindowEvent::ScaleFactorChanged`] was not yet delivered!
4838
pub fn pixels_per_point(egui_ctx: &egui::Context, window: &Window) -> f32 {
4939
let native_pixels_per_point = window.scale_factor() as f32;
5040
let egui_zoom_factor = egui_ctx.zoom_factor();
@@ -236,25 +226,25 @@ impl State {
236226

237227
self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
238228

239-
// On Windows, a minimized window will have 0 width and height.
240-
// See: https://github.com/rust-windowing/winit/issues/208
241-
// This solves an issue where egui window positions would be changed when minimizing on Windows.
242-
let screen_size_in_pixels = screen_size_in_pixels(window);
243-
let screen_size_in_points =
244-
screen_size_in_pixels / pixels_per_point(&self.egui_ctx, window);
229+
// // On Windows, a minimized window will have 0 width and height.
230+
// // See: https://github.com/rust-windowing/winit/issues/208
231+
// // This solves an issue where egui window positions would be changed when minimizing on Windows.
232+
// let screen_size_in_pixels = screen_size_in_pixels(window);
233+
// let screen_size_in_points =
234+
// screen_size_in_pixels / pixels_per_point(&self.egui_ctx, window);
245235

246-
self.egui_input.screen_rect = (screen_size_in_points.x > 0.0
247-
&& screen_size_in_points.y > 0.0)
248-
.then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points));
236+
// self.egui_input.screen_rect = (screen_size_in_points.x > 0.0
237+
// && screen_size_in_points.y > 0.0)
238+
// .then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points));
249239

250240
// Tell egui which viewport is now active:
251241
self.egui_input.viewport_id = self.viewport_id;
252242

253-
self.egui_input
254-
.viewports
255-
.entry(self.viewport_id)
256-
.or_default()
257-
.native_pixels_per_point = Some(window.scale_factor() as f32);
243+
// self.egui_input
244+
// .viewports
245+
// .entry(self.viewport_id)
246+
// .or_default()
247+
// .native_pixels_per_point = Some(window.scale_factor() as f32);
258248

259249
self.egui_input.take()
260250
}
@@ -305,6 +295,31 @@ impl State {
305295
consumed: false,
306296
}
307297
}
298+
WindowEvent::Resized(size) => {
299+
// TODO: This event doesn't deliver outer size
300+
// let size = if cfg!(target_os = "ios") {
301+
// // `outer_size` Includes the area behind the "dynamic island".
302+
// // It is up to the eframe user to make sure the dynamic island doesn't cover anything important.
303+
// // That will be easier once https://github.com/rust-windowing/winit/pull/3890 lands
304+
// window.outer_size()
305+
// } else {
306+
// window.inner_size()
307+
// };
308+
let screen_size_in_pixels = egui::vec2(size.width as f32, size.height as f32);
309+
let screen_size_in_points = screen_size_in_pixels / self.pixels_per_point();
310+
311+
// On Windows, a minimized window will have 0 width and height.
312+
// See: https://github.com/rust-windowing/winit/issues/208
313+
// This solves an issue where egui window positions would be changed when minimizing on Windows.
314+
self.egui_input.screen_rect = (screen_size_in_points.x > 0.0
315+
&& screen_size_in_points.y > 0.0)
316+
.then(|| Rect::from_min_size(Pos2::ZERO, screen_size_in_points));
317+
318+
EventResponse {
319+
repaint: true,
320+
consumed: false,
321+
}
322+
}
308323
WindowEvent::MouseInput { state, button, .. } => {
309324
self.on_mouse_button_input(*state, *button);
310325
EventResponse {
@@ -505,7 +520,6 @@ impl State {
505520
| WindowEvent::CursorEntered { .. }
506521
| WindowEvent::Destroyed
507522
| WindowEvent::Occluded(_)
508-
| WindowEvent::Resized(_)
509523
| WindowEvent::Moved(_)
510524
| WindowEvent::TouchpadPressure { .. }
511525
| WindowEvent::CloseRequested => EventResponse {
@@ -546,11 +560,9 @@ impl State {
546560
}
547561

548562
WindowEvent::PanGesture { delta, phase, .. } => {
549-
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
550-
551563
self.egui_input.events.push(egui::Event::MouseWheel {
552564
unit: egui::MouseWheelUnit::Point,
553-
delta: Vec2::new(delta.x, delta.y) / pixels_per_point,
565+
delta: Vec2::new(delta.x, delta.y) / self.pixels_per_point(),
554566
phase: to_egui_touch_phase(*phase),
555567
modifiers: self.egui_input.modifiers,
556568
});
@@ -645,12 +657,8 @@ impl State {
645657
window: &Window,
646658
pos_in_pixels: winit::dpi::PhysicalPosition<f64>,
647659
) {
648-
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
649-
650-
let pos_in_points = egui::pos2(
651-
pos_in_pixels.x as f32 / pixels_per_point,
652-
pos_in_pixels.y as f32 / pixels_per_point,
653-
);
660+
let pos_in_points =
661+
egui::pos2(pos_in_pixels.x as f32, pos_in_pixels.y as f32) / self.pixels_per_point();
654662
self.pointer_pos_in_points = Some(pos_in_points);
655663

656664
if self.simulate_touch_screen {
@@ -675,17 +683,13 @@ impl State {
675683
}
676684

677685
fn on_touch(&mut self, window: &Window, touch: &winit::event::Touch) {
678-
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
679-
680686
// Emit touch event
681687
self.egui_input.events.push(egui::Event::Touch {
682688
device_id: egui::TouchDeviceId(egui::epaint::util::hash(touch.device_id)),
683689
id: egui::TouchId::from(touch.id),
684690
phase: to_egui_touch_phase(touch.phase),
685-
pos: egui::pos2(
686-
touch.location.x as f32 / pixels_per_point,
687-
touch.location.y as f32 / pixels_per_point,
688-
),
691+
pos: egui::pos2(touch.location.x as f32, touch.location.y as f32)
692+
/ self.pixels_per_point(),
689693
force: match touch.force {
690694
Some(winit::event::Force::Normalized(force)) => Some(force as f32),
691695
Some(winit::event::Force::Calibrated {
@@ -740,30 +744,23 @@ impl State {
740744
delta: winit::event::MouseScrollDelta,
741745
phase: winit::event::TouchPhase,
742746
) {
743-
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
744-
745-
{
746-
let (unit, delta) = match delta {
747-
winit::event::MouseScrollDelta::LineDelta(x, y) => {
748-
(egui::MouseWheelUnit::Line, egui::vec2(x, y))
749-
}
750-
winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition {
751-
x,
752-
y,
753-
}) => (
754-
egui::MouseWheelUnit::Point,
755-
egui::vec2(x as f32, y as f32) / pixels_per_point,
756-
),
757-
};
758-
let phase = to_egui_touch_phase(phase);
759-
let modifiers = self.egui_input.modifiers;
760-
self.egui_input.events.push(egui::Event::MouseWheel {
761-
unit,
762-
delta,
763-
phase,
764-
modifiers,
765-
});
766-
}
747+
let (unit, delta) = match delta {
748+
winit::event::MouseScrollDelta::LineDelta(x, y) => {
749+
(egui::MouseWheelUnit::Line, egui::vec2(x, y))
750+
}
751+
winit::event::MouseScrollDelta::PixelDelta(winit::dpi::PhysicalPosition { x, y }) => (
752+
egui::MouseWheelUnit::Point,
753+
egui::vec2(x as f32, y as f32) / self.pixels_per_point(),
754+
),
755+
};
756+
let phase = to_egui_touch_phase(phase);
757+
let modifiers = self.egui_input.modifiers;
758+
self.egui_input.events.push(egui::Event::MouseWheel {
759+
unit,
760+
delta,
761+
phase,
762+
modifiers,
763+
});
767764
}
768765

769766
fn on_keyboard_input(&mut self, event: &winit::event::KeyEvent) {
@@ -917,8 +914,7 @@ impl State {
917914
}
918915

919916
if let Some(ime) = ime {
920-
let pixels_per_point = pixels_per_point(&self.egui_ctx, window);
921-
let ime_rect_px = pixels_per_point * ime.rect;
917+
let ime_rect_px = self.pixels_per_point() * ime.rect;
922918
if self.ime_rect_px != Some(ime_rect_px)
923919
|| self.egui_ctx.input(|i| !i.events.is_empty())
924920
{
@@ -973,6 +969,17 @@ impl State {
973969
self.current_cursor_icon = None;
974970
}
975971
}
972+
973+
/// Calculate the `pixels_per_point` for a given window, given the current egui zoom factor
974+
fn pixels_per_point(&self) -> f32 {
975+
self.egui_input
976+
.viewports
977+
.get(&self.viewport_id)
978+
.expect("No viewport")
979+
.native_pixels_per_point
980+
.expect("ScaleFactorChanged must have been raised")
981+
* self.egui_ctx.zoom_factor()
982+
}
976983
}
977984

978985
fn to_egui_touch_phase(phase: winit::event::TouchPhase) -> egui::TouchPhase {
@@ -1027,6 +1034,7 @@ pub fn update_viewport_info(
10271034
is_init: bool,
10281035
) {
10291036
profiling::function_scope!();
1037+
// TODO: Keep size and scale consistent with the event loop?
10301038
let pixels_per_point = pixels_per_point(egui_ctx, window);
10311039

10321040
let has_a_position = match window.is_minimized() {
@@ -1406,6 +1414,7 @@ fn process_viewport_command(
14061414

14071415
log::trace!("Processing ViewportCommand::{command:?}");
14081416

1417+
// TODO: keep this consistent with the event loop instead of requerying it from the window
14091418
let pixels_per_point = pixels_per_point(egui_ctx, window);
14101419

14111420
match command {
@@ -1441,6 +1450,7 @@ fn process_viewport_command(
14411450
// because the linux backend converts physical to logical and back again.
14421451
// So let's just assume it worked:
14431452

1453+
// TODO: Don't call inner_size(), but use the returned size
14441454
info.inner_rect = inner_rect_in_points(window, pixels_per_point);
14451455
info.outer_rect = outer_rect_in_points(window, pixels_per_point);
14461456
} else {

0 commit comments

Comments
 (0)