Tainted \\ Coders

Bevy Windows

Last updated:

A window is the thing our app gets rendered on to. Windows are an abstraction for interfacing with the OS your app runs on, so some settings will be platform specific.

Windows are provided in bevy by a collaboration between the bevy::window and bevy::winit crates. The former is a clean internal ECS representation of windows and the latter provides a platform specific implementation of the window event loop.

Before Bevy 0.10 used to manage our windows through a Windows resource but has since been changed to a component Window.

Cross platform support is generally good except for wayland on nvidia GPUs.

Size matters

There are three sizes associated with a window:

  1. Physical Size which represents the actual height and width in physical pixels the window occupies on the monitor
  2. Logical Size which represents the size that should be used to scale elements inside the window, measured in logical pixels
  3. Requested Size measured in logical pixels, which is the value submitted to the API when creating the window, or requesting that it be resized.

The reason logical size and physical size are separated and can be different is to account for:

  • Differences in monitor pixel densities
  • Operating system settings for pixel densities
  • Bevy App specified scale factor between both

The physical size is the base size, completely unscaled. When we apply the scale_factor to our window we get the logical size.

The different sizes are handled through the WindowResolution struct (not a component) which is on the Window component. The default looks like:

WindowResolution {
    physical_width: 1280,
    physical_height: 720,
    scale_factor_override: None,
    scale_factor: 1.0,
}

You can use multiple windows on an app. By default, a PrimaryWindow gets spawned by WindowPlugin, contained in DefaultPlugins.

The default plugin looks like:

WindowPlugin {
    primary_window: Some(Window::default()), // Spawns this component with a PrimaryWindow component
    exit_condition: ExitCondition::OnAllClosed, // Close our app when all windows close
    close_when_requested: true, // Should we close windows when the user does?
}

The plugin is adding some default systems for window handling and some events we can read from:

  • WindowResized
  • WindowCreated
  • WindowClosed
  • WindowCloseRequested
  • RequestRedraw
  • CursorMoved
  • CursorEntered
  • CursorLeft
  • ReceivedCharacter
  • Ime
  • WindowFocused
  • WindowScaleFactorChanged
  • WindowBackendScaleFactorChanged
  • FileDragAndDrop
  • WindowMoved
  • WindowThemeChanged

Its common to want to exit your game with a hotkey (such as esc) when prototyping. Bevy exposes a system you can include in your own game bevy::window::close_on_esc. Lets see how they interact with our windows:

// Close the focused window whenever the escape key (<kbd>Esc</kbd>) is pressed
//
// This is useful for examples or prototyping.
pub fn close_on_esc(
    mut commands: Commands,
    focused_windows: Query<(Entity, &Window)>,
    input: Res<Input<KeyCode>>,
) {
    for (window, focus) in focused_windows.iter() {
        if !focus.focused {
            continue;
        }

        if input.just_pressed(KeyCode::Escape) {
            commands.entity(window).despawn();
        }
    }
}

The Window that got created can be queried just like our other components. When we hit the Esc key our app will close any focused windows by despawning their entities.

But in reality, the actual closing of the windows on your OS is done through the bevy::winit::despawn_window system which reads our internal representation we create through bevy::window (see further down the page).

If we had multiple windows and wanted to reference a specific window we should use a WindowRef:

#[derive(Default, Copy, Clone, Debug, Reflect, FromReflect)]
pub enum WindowRef {
    // This will be linked to the primary window that is created by default
    // in the [`WindowPlugin`](crate::WindowPlugin::primary_window).
    #[default]
    Primary,
    // A more direct link to a window entity.
    //
    // Use this if you want to reference a secondary/tertiary/... window.
    //
    // To create a new window you can spawn an entity with a [`Window`],
    // then you can use that entity here for usage in cameras.
    Entity(Entity),
}

The actual Window component contains settings for how our windows should be positioned and rendered on the screen:

pub struct Window {
    // The cursor of this window.
    pub cursor: Cursor,
    // What presentation mode to give the window.
    pub present_mode: PresentMode,
    // Which fullscreen or windowing mode should be used.
    pub mode: WindowMode,
    // Where the window should be placed.
    pub position: WindowPosition,
    // What resolution the window should have.
    pub resolution: WindowResolution,
    // Stores the title of the window.
    pub title: String,
    // How the alpha channel of textures should be handled while compositing.
    pub composite_alpha_mode: CompositeAlphaMode,
    // The limits of the window's logical size
    // (found in its [`resolution`](WindowResolution)) when resizing.
    pub resize_constraints: WindowResizeConstraints,
    // Should the window be resizable?
    // Note: This does not stop the program from fullscreening/setting
    // the size programmatically.
    pub resizable: bool,
    // Should the window have decorations enabled?
    // (Decorations are the minimize, maximize, and close buttons on desktop apps)
    pub decorations: bool,
    // Defines whether the background of the window should be transparent.
    pub transparent: bool,
    // Get/set whether the window is focused.
    pub focused: bool,
    // Where should the window appear relative to other overlapping window.
    pub window_level: WindowLevel,
    // The "html canvas" element selector.
    // This value has no effect on non-web platforms.
    pub canvas: Option<String>,
    // Whether or not to fit the canvas element's size to its parent element's size.
    // This value has no effect on non-web platforms.
    pub fit_canvas_to_parent: bool,
    // Whether or not to stop events from propagating out of the canvas element
    // This value has no effect on non-web platforms.
    pub prevent_default_event_handling: bool,
    // Stores internal state that isn't directly accessible.
    pub internal: InternalWindowState,
    // Should the window use Input Method Editor?
    //
    // If enabled, the window will receive [`Ime`](crate::Ime) events instead of
    // [`ReceivedCharacter`](crate::ReceivedCharacter) or
    // [`KeyboardInput`](bevy_input::keyboard::KeyboardInput).
    //
    // IME should be enabled during text input, but not when you expect to get the exact key pressed.
    pub ime_enabled: bool,
    // Sets location of IME candidate box in client area coordinates relative to the top left.
    pub ime_position: Vec2,
    // Sets a specific theme for the window.
    // If `None` is provided, the window will use the system theme.
    pub window_theme: Option<WindowTheme>,
}

Windows control their rendering depending on the use case with a bevy::winit::WinitSettings resource:

fn main() {
    App::new()
        // Set the background color of our window
        .insert_resource(ClearColor(Color::rgb(0.5, 0.5, 0.9)))
        // Continuous rendering for games - bevy's default.
        .insert_resource(WinitSettings::game())
        // Power-saving reactive rendering for applications.
        .insert_resource(WinitSettings::desktop_app());
}

WininitSettings is from another bevy crate bevy::wininit which handles actually creating the window on our OS. So there is a clean internal representation of your windows in bevy::window but the reality is that every platform has its own way of creating windows.

So when your Window is despawning its actually calling out to a system provided by the WininitPlugin which is also included in the DefaultPlugins:

// This is a system provided by the `WininitPlugin`
pub(crate) fn despawn_window(
    mut closed: RemovedComponents<Window>,
    window_entities: Query<&Window>,
    mut close_events: EventWriter<WindowClosed>,
    mut winit_windows: NonSendMut<WinitWindows>,
) {
    for window in closed.iter() {
        info!("Closing window {:?}", window);
        // Guard to verify that the window is in fact actually gone,
        // rather than having the component added and removed in the same frame.
        if !window_entities.contains(window) {
            winit_windows.remove_window(window);
            close_events.send(WindowClosed { window });
        }
    }
}

Sometimes you may need to override the scale of the window from the OS scale:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                resolution: WindowResolution::new(500., 300.).with_scale_factor_override(1.0),
                ..default()
            }),
            ..default()
        }))
}

We can initialize a Window through the WindowPlugin like:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "I am a window!".into(),
                resolution: (500., 300.).into(),
                present_mode: PresentMode::AutoVsync,
                // Tells wasm to resize the window according to the available canvas
                fit_canvas_to_parent: true,
                // Tells wasm not to override default event handling, like F5, Ctrl+R etc.
                prevent_default_event_handling: false,
                ..default()
            }),
            ..default()
        }))
}

Whatever we provide to the primary_window field of the WindowPlugin will become an entity with both whatever we passed in plus an additional PrimaryWindow component.

This PrimaryWindow marker component can be useful for managing the main window of your game if you have to manage multiple windows.

fn get_window_dimensions(
    window_query: Query<&Window, With<PrimaryWindow>>
) {
    let window = window_query.single();

    info!(
        "The windows resolution is {:0.0} by {:0.0}",
        window.resolution.width(),
        window.resolution.height()
    )
}

Reading and writing window data

Your Window can be accessed in a normal Query and its data can be modified (or just read). Once we have our Window component we can query it:

pub fn inspect_window(
    windows: Query<(&Window)>,
) {
    for (window) in windows.iter() {
        let focused = window.focused;
        println!("Window is focused: {:?}", focused);

        // The size after scaling:
        let logical_width = window.width();
        let logical_height = window.height();
        println!("Logical size: {:?} x {:?}", logical_width, logical_height);

        // The size before scaling:
        let physical_width = window.physical_width();
        let physical_height = window.physical_height();
        println!("physical size: {:?} x {:?}", physical_width, physical_height);

        // Cursor position in logical sizes, this would return None if our
        // cursor is outside of the window:
        if let Some(logical_cursor_position) = window.cursor_position() {
            println!("Logical cursor position: {:?}", logical_cursor_position);
        }

        // Cursor position in physical sizes, this would return None if our
        // cursor is outside of the window:
        if let Some(physical_cursor_position) = window.physical_cursor_position() {
            println!("Physical cursor position: {:?}", physical_cursor_position);
        }
    }
}

For example we can change the resolution:

// This system shows how to request the window to resize the resolution
fn toggle_resolution(
    keys: Res<Input<KeyCode>>,
    mut windows: Query<&mut Window>,
    resolution: Res<ResolutionSettings>,
) {
    let mut window = windows.single_mut();

    if keys.just_pressed(KeyCode::Key1) {
        let res = resolution.small;
        window.resolution.set(res.x, res.y);
    }
    if keys.just_pressed(KeyCode::Key2) {
        let res = resolution.medium;
        window.resolution.set(res.x, res.y);
    }
    if keys.just_pressed(KeyCode::Key3) {
        let res = resolution.large;
        window.resolution.set(res.x, res.y);
    }
}

However we will also need to react to changes for things like Text:

// This system shows how to respond to a window being resized.
// Whenever the window is resized, the text will update with the new resolution.
fn on_resize_system(
    mut q: Query<&mut Text, With<ResolutionText>>,
    mut resize_reader: EventReader<WindowResized>,
) {
    let mut text = q.single_mut();
    for e in resize_reader.iter() {
        // When resolution is being changed
        text.sections[0].value = format!("{:.1} x {:.1}", e.width, e.height);
    }
}

Changing a title:

// This system will then change the title during execution
fn change_title(mut windows: Query<&mut Window>, time: Res<Time>) {
    let mut window = windows.single_mut();
    window.title = format!(
        "Seconds since startup: {}",
        time.elapsed().as_secs_f32().round()
    );
}

Cursors

A Cursor is our interface to interacting with the mouse. Each cursor has a CursorGrabMode which lets us define how a users cursor is grabbed by the window:

pub enum CursorGrabMode {
    // The cursor can freely leave the window.
    #[default]
    None,
    // The cursor is confined to the window area.
    Confined,
    // The cursor is locked inside the window area to a certain position.
    Locked,
}

We can use this to toggle our cursors on certain actions in a game. We can imagine building an FPS and wanting our cursor to constantly place itself in the center to keep our mouse locked to the game screen during gameplay:

fn toggle_cursor(mut windows: Query<&mut Window>, input: Res<Input<KeyCode>>) {
    if input.just_pressed(KeyCode::Space) {
        let mut window = windows.single_mut();

        window.cursor.visible = !window.cursor.visible;
        window.cursor.grab_mode = match window.cursor.grab_mode {
            CursorGrabMode::None => CursorGrabMode::Locked,
            CursorGrabMode::Locked | CursorGrabMode::Confined => CursorGrabMode::None,
        };
    }
}

Reactive windows

We can initialize our windows to be reactive instead of continuous:

fn main() {
    App::new()
        .insert_resource(WinitSettings {
            focused_mode: bevy::winit::UpdateMode::Continuous,
            unfocused_mode: bevy::winit::UpdateMode::ReactiveLowPower {
                max_wait: Duration::from_millis(10),
            },
            ..default()
        })
}

When your window is in UpdateMode::ReactiveLowPower it will only re-render when it receives input. This is great for saving CPU when the app is not doing anything.

UpdateMode::Continuous is the default and renders each tick of your app.

This is quite an all or nothing type of setting so a crate like bevy_framepace can help give us even more control.

bevy_framepace works in a similar way to FixedUpdate systems where it measures the time that passes and triggers its render pipeline manually based on the delta.

Taking a screenshot

Taking a screenshot can be done with bevy::render::view::screenshot::ScreenshotManager

fn take_screenshot(
    mut screenshot_manager: ResMut<ScreenshotManager>,
    input: Res<Input<KeyCode>>,
    primary_window: Query<Entity, With<PrimaryWindow>>,
) {
    if input.just_pressed(KeyCode::Space) {
        screenshot_manager
            .save_screenshot_to_disk(primary_window.single(), "screenshot.png")
            .unwrap();
    }
}

Read more