Skip to content

Resizable window jumps to incorrect size on trying to resize #8284

Description

@matanox

On first resize interaction, or a first window refocus after initial window display whichever comes first, the window size jumps to double its original size, on a dual monitor desktop where one monitor is set to 200% scaling and the other to 100%.

This is consistent on every run of the application.

The window is created with:

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("eframe window size jump repro")
            .with_decorations(false)
            .with_resizable(true)
            .with_inner_size(INITIAL_INNER_SIZE)
            .with_min_inner_size(MIN_INNER_SIZE),
        ..Default::default()
    };

Code for a bare window reproducing the issue is attached at the very bottom below.

Application/window setup:

  • Rust eframe app using eframe 0.34.3 / winit 0.30.13
  • Wayland session
  • Undecorated, resizable native window
  • Requested initial inner size: 800x300
  • Requested minimum inner size: 400x150
  • egui first UI pass reported:
    • content_size=800x300
    • viewport_size=800x300
    • pixels_per_point=1.0
    • native_pixels_per_point=1.0
    • zoom_factor=1.0

Observed behavior:

  • On first custom edge resize interaction, before sending the resize command, the app logged:

    • content_size=800x300
    • viewport_size=800x300
    • pixels_per_point=1.0
    • native_pixels_per_point=1.0
    • zoom_factor=1.0
    • monitor_size_points=3840x2160
    • monitor_size_physical_estimate=3840x2160
    • resize direction=East
  • Immediately after the resize command, the next viewport geometry event reported:

    • content_size changed from 800x300 to 1600x600
    • viewport_size changed from 800x300 to 1600x600
    • ratio=2.000x2.000
    • pixels_per_point remained 1.0
    • native_pixels_per_point remained 1.0
    • zoom_factor remained 1.0
    • monitor_size_points remained 3840x2160
    • monitor_size_physical_estimate remained 3840x2160
  • It's consistently only the first window resize or refocus which trigger the size jump; subsequent resize events behave as expected.

Issue Summary

  • The window initially honors the requested size.
  • The first resize interaction, or just a first focus regain as in the below reproducing code, causes a one-time 2x jump to 1600x600.
  • The jump mirrors the scale factor of the other monitor in the mixed-scale setup: one attached monitor is at 200% scale and the other at 100%.
  • The app-observed egui/winit scale fields remain at 1.0 during the jump, so this does not look like an intentional scale-factor update being surfaced to the app.
  • This appears specific to a GNOME/Mutter Wayland mixed-scale multi-monitor setup with an undecorated resizable eframe/winit window.
  • Reproduces also with eframe 0.35.0.
  • Reproduces regardless the initial window size you set in code: the size doubles on first resize/refocus interaction.

Environment:

  • Ubuntu 24.04.4 LTS, with its default desktop stack:
    • GNOME Shell 46.0
    • Wayland session
    • Mutter library package: libmutter-14-0 46.2-1ubuntu0.24.04.14
  • Renderer from Mutter DisplayConfig: native
  • DisplayConfig layout-mode: 2
  • DisplayConfig legacy-ui-scaling-factor: 2
  • Two external 3840x2160 monitors:
    • Logical monitor A: position=(0,0), scale=2.0, primary=true
    • Logical monitor B: position=(3840,0), scale=1.0, primary=false
  • GNOME interface scaling-factor: uint32 0
  • GNOME text-scaling-factor: 1.0
  • GNOME Mutter experimental-features: []

minimally reproducing code:

src/main.rs

use eframe::egui;

const INITIAL_INNER_SIZE: [f32; 2] = [800.0, 300.0];
const MIN_INNER_SIZE: [f32; 2] = [400.0, 150.0];

fn main() -> eframe::Result {
    println!(
        "[repro] requested viewport: decorations=false resizable=true initial_inner_size={} min_inner_size={} eframe=0.35.0",
        format_size(INITIAL_INNER_SIZE),
        format_size(MIN_INNER_SIZE),
    );
    println!(
        "[repro] session: XDG_SESSION_TYPE={:?} XDG_CURRENT_DESKTOP={:?} WAYLAND_DISPLAY={:?} DISPLAY={:?}",
        std::env::var("XDG_SESSION_TYPE").ok(),
        std::env::var("XDG_CURRENT_DESKTOP").ok(),
        std::env::var("WAYLAND_DISPLAY").ok(),
        std::env::var("DISPLAY").ok(),
    );

    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("eframe window size jump repro")
            .with_decorations(false)
            .with_resizable(true)
            .with_inner_size(INITIAL_INNER_SIZE)
            .with_min_inner_size(MIN_INNER_SIZE),
        ..Default::default()
    };

    eframe::run_native(
        "eframe window size jump repro",
        options,
        Box::new(|_cc| Ok(Box::<ReproApp>::default())),
    )
}

#[derive(Default)]
struct ReproApp {
    last_geometry: Option<Geometry>,
}

impl eframe::App for ReproApp {
    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        self.log_geometry_if_changed(ui.ctx());

        egui::CentralPanel::default().show(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.add_space(90.0);
                ui.label("Switch focus to another window and back");
                ui.label("Watch stdout for a 2x size jump");
            });
        });
    }
}

impl ReproApp {
    fn log_geometry_if_changed(&mut self, ctx: &egui::Context) {
        let current = Geometry::from_context(ctx);
        match self.last_geometry.as_ref() {
            None => {
                println!("[repro] first-ui-pass: {}", current.describe());
            }
            Some(previous) if previous != &current => {
                println!(
                    "[repro] viewport-geometry-changed: resized={} content_size={} viewport_size={} pixels_per_point={} native_pixels_per_point={} zoom_factor={} monitor_size_points={} focused={:?}->{:?}",
                    previous.content_size != current.content_size
                        || previous.viewport_size != current.viewport_size,
                    format_vec2_transition(previous.content_size, current.content_size),
                    format_vec2_transition(previous.viewport_size, current.viewport_size),
                    format_f32_transition(previous.pixels_per_point, current.pixels_per_point),
                    format_optional_f32_transition(
                        previous.native_pixels_per_point,
                        current.native_pixels_per_point,
                    ),
                    format_f32_transition(previous.zoom_factor, current.zoom_factor),
                    format_optional_vec2_transition(previous.monitor_size, current.monitor_size),
                    previous.focused,
                    current.focused,
                );
            }
            Some(_) => return,
        }
        self.last_geometry = Some(current);
    }
}

#[derive(Debug, Clone, PartialEq)]
struct Geometry {
    content_size: egui::Vec2,
    viewport_size: egui::Vec2,
    pixels_per_point: f32,
    zoom_factor: f32,
    native_pixels_per_point: Option<f32>,
    monitor_size: Option<egui::Vec2>,
    focused: Option<bool>,
}

impl Geometry {
    fn from_context(ctx: &egui::Context) -> Self {
        let (viewport, content_size, viewport_size) = ctx.input(|input| {
            (
                input.viewport().clone(),
                input.content_rect().size(),
                input.viewport_rect().size(),
            )
        });

        Self {
            content_size,
            viewport_size,
            pixels_per_point: ctx.pixels_per_point(),
            zoom_factor: ctx.zoom_factor(),
            native_pixels_per_point: viewport.native_pixels_per_point,
            monitor_size: viewport.monitor_size,
            focused: viewport.focused,
        }
    }

    fn describe(&self) -> String {
        format!(
            "content_size={} viewport_size={} pixels_per_point={:.3} native_pixels_per_point={} zoom_factor={:.3} monitor_size_points={} focused={:?}",
            format_vec2(self.content_size),
            format_vec2(self.viewport_size),
            self.pixels_per_point,
            format_optional_f32(self.native_pixels_per_point),
            self.zoom_factor,
            format_optional_vec2(self.monitor_size),
            self.focused,
        )
    }
}

fn format_size(value: [f32; 2]) -> String {
    format!("{:.1}x{:.1}", value[0], value[1])
}

fn format_optional_f32(value: Option<f32>) -> String {
    value.map_or_else(|| "unknown".to_string(), |value| format!("{value:.3}"))
}

fn format_f32_transition(previous: f32, current: f32) -> String {
    format!("{previous:.3}->{current:.3}")
}

fn format_optional_f32_transition(previous: Option<f32>, current: Option<f32>) -> String {
    format!(
        "{}->{}",
        format_optional_f32(previous),
        format_optional_f32(current)
    )
}

fn format_optional_vec2(value: Option<egui::Vec2>) -> String {
    value.map_or_else(|| "unknown".to_string(), format_vec2)
}

fn format_vec2_transition(previous: egui::Vec2, current: egui::Vec2) -> String {
    format!(
        "{}->{} ratio={}",
        format_vec2(previous),
        format_vec2(current),
        format_vec2_ratio(previous, current)
    )
}

fn format_optional_vec2_transition(
    previous: Option<egui::Vec2>,
    current: Option<egui::Vec2>,
) -> String {
    match (previous, current) {
        (Some(previous), Some(current)) => format_vec2_transition(previous, current),
        _ => format!(
            "{}->{}",
            format_optional_vec2(previous),
            format_optional_vec2(current)
        ),
    }
}

fn format_vec2_ratio(previous: egui::Vec2, current: egui::Vec2) -> String {
    format!(
        "{:.3}x{:.3}",
        ratio_component(previous.x, current.x),
        ratio_component(previous.y, current.y)
    )
}

fn ratio_component(previous: f32, current: f32) -> f32 {
    if previous.abs() <= f32::EPSILON {
        f32::NAN
    } else {
        current / previous
    }
}

fn format_vec2(value: egui::Vec2) -> String {
    format!("{:.1}x{:.1}", value.x, value.y)
}

Cargo.toml

[package]
name = "window-size-jump-issue-repro"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
eframe = "0.35.0"

Related issues:
#7095
rust-windowing/winit#2921
rust-windowing/winit#3485

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions