Rework WindowMode::Fullscreen API (#17525)

# Objective

- #16883 
- Improve the default behaviour of the exclusive fullscreen API.

## Solution

This PR changes the exclusive fullscreen window mode to require the type
`WindowMode::Fullscreen(MonitorSelection, VideoModeSelection)` and
removes `WindowMode::SizedFullscreen`. This API somewhat intentionally
more closely resembles Winit 0.31's upcoming fullscreen and video mode
API.

The new VideoModeSelection enum is specified as follows:

```rust
pub enum VideoModeSelection {
    /// Uses the video mode that the monitor is already in.
    Current,
    /// Uses a given [`crate::monitor::VideoMode`]. A list of video modes supported by the monitor
    /// is supplied by [`crate::monitor::Monitor::video_modes`].
    Specific(VideoMode),
}
```

### Changing default behaviour

This might be contentious because it removes the previous behaviour of
`WindowMode::Fullscreen` which selected the highest resolution possible.
While the previous behaviour would be quite easy to re-implement as
additional options, or as an impl method on Monitor, I would argue that
this isn't an implementation that should be encouraged.

From the perspective of a Windows user, I prefer what the majority of
modern games do when entering fullscreen which is to preserve the OS's
current resolution settings, which allows exclusive fullscreen to be
entered faster, and to only have it change if I manually select it in
either the options of the game or the OS. The highest resolution
available is not necessarily what the user prefers.

I am open to changing this if I have just missed a good use case for it.

Likewise, the only functionality that `WindowMode::SizedFullscreen`
provided was that it selected the resolution closest to the current size
of the window so it was removed since this behaviour can be replicated
via the new `VideoModeSelection::Specific` if necessary.

## Out of scope

WindowResolution and scale factor act strangely in exclusive fullscreen,
this PR doesn't address it or regress it.

## Testing

- Tested on Windows 11 and macOS 12.7
- Linux untested

## Migration Guide

`WindowMode::SizedFullscreen(MonitorSelection)` and
`WindowMode::Fullscreen(MonitorSelection)` has become
`WindowMode::Fullscreen(MonitorSelection, VideoModeSelection)`.
Previously, the VideoMode was selected based on the closest resolution
to the current window size for SizedFullscreen and the largest
resolution for Fullscreen. It is possible to replicate that behaviour by
searching `Monitor::video_modes` and selecting it with
`VideoModeSelection::Specific(VideoMode)` but it is recommended to use
`VideoModeSelection::Current` as the default video mode when entering
fullscreen.
This commit is contained in:
jf908 2025-03-11 01:20:53 +00:00 committed by GitHub
parent 4a41525cd0
commit bfe932d1f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 103 additions and 94 deletions

View File

@ -980,7 +980,7 @@ pub fn camera_system(
}
}
}
// This check is needed because when changing WindowMode to SizedFullscreen, the viewport may have invalid
// This check is needed because when changing WindowMode to Fullscreen, the viewport may have invalid
// arguments due to a sudden change on the window size to a lower value.
// If the size of the window is lower, the viewport will match that lower value.
if let Some(viewport) = &mut camera.viewport {

View File

@ -45,8 +45,9 @@ pub use window::*;
pub mod prelude {
#[doc(hidden)]
pub use crate::{
CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection, Window,
WindowMoved, WindowPlugin, WindowPosition, WindowResizeConstraints,
CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, MonitorSelection,
VideoModeSelection, Window, WindowMoved, WindowPlugin, WindowPosition,
WindowResizeConstraints,
};
}

View File

@ -55,7 +55,7 @@ impl Monitor {
}
/// Represents a video mode that a monitor supports
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(

View File

@ -17,6 +17,8 @@ use {
#[cfg(all(feature = "serialize", feature = "bevy_reflect"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use crate::VideoMode;
/// Marker [`Component`] for the window considered the primary window.
///
/// Currently this is assumed to only exist on 1 entity at a time.
@ -127,11 +129,12 @@ impl EntityBorrow for NormalizedWindowRef {
/// ```
/// # use bevy_ecs::query::With;
/// # use bevy_ecs::system::Query;
/// # use bevy_window::{WindowMode, PrimaryWindow, Window, MonitorSelection};
/// # use bevy_window::{WindowMode, PrimaryWindow, Window, MonitorSelection, VideoModeSelection};
/// fn change_window_mode(mut windows: Query<&mut Window, With<PrimaryWindow>>) {
/// // Query returns one window typically.
/// for mut window in windows.iter_mut() {
/// window.mode = WindowMode::Fullscreen(MonitorSelection::Current);
/// window.mode =
/// WindowMode::Fullscreen(MonitorSelection::Current, VideoModeSelection::Current);
/// }
/// }
/// ```
@ -1123,6 +1126,24 @@ pub enum MonitorSelection {
Entity(Entity),
}
/// References an exclusive fullscreen video mode.
///
/// Used when setting [`WindowMode::Fullscreen`] on a window.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
#[reflect(Debug, PartialEq)]
pub enum VideoModeSelection {
/// Uses the video mode that the monitor is already in.
Current,
/// Uses a given [`crate::monitor::VideoMode`]. A list of video modes supported by the monitor
/// is supplied by [`crate::monitor::Monitor::video_modes`].
Specific(VideoMode),
}
/// Presentation mode for a [`Window`].
///
/// The presentation mode specifies when a frame is presented to the window. The [`Fifo`]
@ -1286,28 +1307,15 @@ pub enum WindowMode {
/// If you want to avoid that behavior, you can use the [`WindowResolution::set_scale_factor_override`] function
/// or the [`WindowResolution::with_scale_factor_override`] builder method to set the scale factor to 1.0.
BorderlessFullscreen(MonitorSelection),
/// The window should be in "true"/"legacy" Fullscreen mode on the given [`MonitorSelection`].
/// The window should be in "true"/"legacy"/"exclusive" Fullscreen mode on the given [`MonitorSelection`].
///
/// When setting this, the operating system will be requested to use the
/// **closest** resolution available for the current monitor to match as
/// closely as possible the window's physical size.
/// After that, the window's physical size will be modified to match
/// that monitor resolution, and the logical size will follow based on the
/// scale factor, see [`WindowResolution`].
SizedFullscreen(MonitorSelection),
/// The window should be in "true"/"legacy" Fullscreen mode on the given [`MonitorSelection`].
///
/// When setting this, the operating system will be requested to use the
/// **biggest** resolution available for the current monitor.
/// After that, the window's physical size will be modified to match
/// that monitor resolution, and the logical size will follow based on the
/// scale factor, see [`WindowResolution`].
/// The resolution, refresh rate, and bit depth are selected based on the given [`VideoModeSelection`].
///
/// Note: As this mode respects the scale factor provided by the operating system,
/// the window's logical size may be different from its physical size.
/// If you want to avoid that behavior, you can use the [`WindowResolution::set_scale_factor_override`] function
/// or the [`WindowResolution::with_scale_factor_override`] builder method to set the scale factor to 1.0.
Fullscreen(MonitorSelection),
Fullscreen(MonitorSelection, VideoModeSelection),
}
/// Specifies where a [`Window`] should appear relative to other overlapping windows (on top or under) .

View File

@ -34,7 +34,7 @@ use crate::{
convert_enabled_buttons, convert_resize_direction, convert_window_level,
convert_window_theme, convert_winit_theme,
},
get_best_videomode, get_fitting_videomode, select_monitor,
get_selected_videomode, select_monitor,
state::react_to_resize,
winit_monitors::WinitMonitors,
CreateMonitorParams, CreateWindowParams, WinitWindows,
@ -314,10 +314,8 @@ pub(crate) fn changed_windows(
&monitor_selection,
))))
}
mode @ (WindowMode::Fullscreen(_) | WindowMode::SizedFullscreen(_)) => {
let videomode = match mode {
WindowMode::Fullscreen(monitor_selection) => get_best_videomode(
&select_monitor(
WindowMode::Fullscreen(monitor_selection, video_mode_selection) => {
let monitor = &select_monitor(
&monitors,
winit_window.primary_monitor(),
winit_window.current_monitor(),
@ -325,25 +323,18 @@ pub(crate) fn changed_windows(
)
.unwrap_or_else(|| {
panic!("Could not find monitor for {:?}", monitor_selection)
}),
),
WindowMode::SizedFullscreen(monitor_selection) => get_fitting_videomode(
&select_monitor(
&monitors,
winit_window.primary_monitor(),
winit_window.current_monitor(),
&monitor_selection,
)
.unwrap_or_else(|| {
panic!("Could not find monitor for {:?}", monitor_selection)
}),
window.width() as u32,
window.height() as u32,
),
_ => unreachable!(),
};
});
Some(Some(winit::window::Fullscreen::Exclusive(videomode)))
if let Some(video_mode) = get_selected_videomode(monitor, &video_mode_selection)
{
Some(Some(winit::window::Fullscreen::Exclusive(video_mode)))
} else {
warn!(
"Could not find valid fullscreen video mode for {:?} {:?}",
monitor_selection, video_mode_selection
);
None
}
}
WindowMode::Windowed => Some(None),
};

View File

@ -4,8 +4,8 @@ use bevy_ecs::entity::Entity;
use bevy_ecs::entity::hash_map::EntityHashMap;
use bevy_platform_support::collections::HashMap;
use bevy_window::{
CursorGrabMode, MonitorSelection, Window, WindowMode, WindowPosition, WindowResolution,
WindowWrapper,
CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition,
WindowResolution, WindowWrapper,
};
use tracing::warn;
@ -61,8 +61,7 @@ impl WinitWindows {
let maybe_selected_monitor = &match window.mode {
WindowMode::BorderlessFullscreen(monitor_selection)
| WindowMode::Fullscreen(monitor_selection)
| WindowMode::SizedFullscreen(monitor_selection) => select_monitor(
| WindowMode::Fullscreen(monitor_selection, _) => select_monitor(
monitors,
event_loop.primary_monitor(),
None,
@ -74,23 +73,22 @@ impl WinitWindows {
winit_window_attributes = match window.mode {
WindowMode::BorderlessFullscreen(_) => winit_window_attributes
.with_fullscreen(Some(Fullscreen::Borderless(maybe_selected_monitor.clone()))),
WindowMode::Fullscreen(_) => {
WindowMode::Fullscreen(monitor_selection, video_mode_selection) => {
let select_monitor = &maybe_selected_monitor
.clone()
.expect("Unable to get monitor.");
let videomode = get_best_videomode(select_monitor);
winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode)))
}
WindowMode::SizedFullscreen(_) => {
let select_monitor = &maybe_selected_monitor
.clone()
.expect("Unable to get monitor.");
let videomode = get_fitting_videomode(
select_monitor,
window.width() as u32,
window.height() as u32,
if let Some(video_mode) =
get_selected_videomode(select_monitor, &video_mode_selection)
{
winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(video_mode)))
} else {
warn!(
"Could not find valid fullscreen video mode for {:?} {:?}",
monitor_selection, video_mode_selection
);
winit_window_attributes.with_fullscreen(Some(Fullscreen::Exclusive(videomode)))
winit_window_attributes
}
}
WindowMode::Windowed => {
if let Some(position) = winit_window_position(
@ -337,30 +335,35 @@ impl WinitWindows {
}
}
/// Gets the "best" video mode which fits the given dimensions.
///
/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
pub fn get_fitting_videomode(monitor: &MonitorHandle, width: u32, height: u32) -> VideoModeHandle {
monitor
.video_modes()
.max_by_key(|x| {
(
x.size().width.abs_diff(width),
x.size().height.abs_diff(height),
x.refresh_rate_millihertz(),
)
})
.unwrap()
/// Returns some [`winit::monitor::VideoModeHandle`] given a [`MonitorHandle`] and a
/// [`VideoModeSelection`] or None if no valid matching video mode was found.
pub fn get_selected_videomode(
monitor: &MonitorHandle,
selection: &VideoModeSelection,
) -> Option<VideoModeHandle> {
match selection {
VideoModeSelection::Current => get_current_videomode(monitor),
VideoModeSelection::Specific(specified) => monitor.video_modes().find(|mode| {
mode.size().width == specified.physical_size.x
&& mode.size().height == specified.physical_size.y
&& mode.refresh_rate_millihertz() == specified.refresh_rate_millihertz
&& mode.bit_depth() == specified.bit_depth
}),
}
}
/// Gets the "best" video-mode handle from a monitor.
/// Gets a monitor's current video-mode.
///
/// The heuristic for "best" prioritizes width, height, and refresh rate in that order.
pub fn get_best_videomode(monitor: &MonitorHandle) -> VideoModeHandle {
/// TODO: When Winit 0.31 releases this function can be removed and replaced with
/// `MonitorHandle::current_video_mode()`
fn get_current_videomode(monitor: &MonitorHandle) -> Option<VideoModeHandle> {
monitor
.video_modes()
.max_by_key(|x| (x.size(), x.refresh_rate_millihertz()))
.unwrap()
.filter(|mode| {
mode.size() == monitor.size()
&& Some(mode.refresh_rate_millihertz()) == monitor.refresh_rate_millihertz()
})
.max_by_key(VideoModeHandle::bit_depth)
}
pub(crate) fn attempt_grab(

View File

@ -44,7 +44,10 @@ fn update(
.spawn((
Window {
title: name.clone(),
mode: WindowMode::Fullscreen(MonitorSelection::Entity(entity)),
mode: WindowMode::Fullscreen(
MonitorSelection::Entity(entity),
VideoModeSelection::Current,
),
position: WindowPosition::Centered(MonitorSelection::Entity(entity)),
..default()
},

View File

@ -1,8 +1,8 @@
//! a test that confirms that 'bevy' does not panic while changing from Windowed to SizedFullscreen when viewport is set
//! a test that confirms that 'bevy' does not panic while changing from Windowed to Fullscreen when viewport is set
use bevy::{prelude::*, render::camera::Viewport, window::WindowMode};
//Having a viewport set to the same size as a window used to cause panic on some occasions when switching to SizedFullscreen
//Having a viewport set to the same size as a window used to cause panic on some occasions when switching to Fullscreen
const WINDOW_WIDTH: f32 = 1366.0;
const WINDOW_HEIGHT: f32 = 768.0;
@ -47,9 +47,12 @@ fn toggle_window_mode(mut qry_window: Query<&mut Window>) {
window.mode = match window.mode {
WindowMode::Windowed => {
//it takes a while for the window to change from windowed to sizedfullscreen and back
//it takes a while for the window to change from windowed to fullscreen and back
std::thread::sleep(std::time::Duration::from_secs(4));
WindowMode::SizedFullscreen
WindowMode::Fullscreen(
MonitorSelection::Entity(entity),
VideoModeSelection::Current,
)
}
_ => {
std::thread::sleep(std::time::Duration::from_secs(4));