Tainted\\Coders

Bevy Windows

Bevy version: 0.19Last updated:

A window is the thing our app gets rendered on.

Windows are an abstraction for interfacing with the operating system your app runs on. That means some settings will be platform specific.

Windows are provided through a collaboration between the bevy_window and bevy_winit crates. bevy_window is a clean internal ECS representation of windows and bevy_winit provides a platform specific implementation of the window event loop.

Cross platform support is generally good, though some backends have known limitations (for example Wayland on Nvidia GPUs). Window and cursor behavior can also differ per platform, so those caveats are called out throughout this page.

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:

The physical size is the base size, completely unscaled. Dividing the physical size by the scale_factor gives us the logical size (equivalently, physical = logical * scale_factor).

The different sizes are handled through the WindowResolution struct (not a component) which is on the Window component. Its fields are private, so you read them through methods like:

The default looks like:

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

Note that WindowResolution::new(..) takes physical pixels, while WindowResolution::set(..) (and the Window::width/height methods) work in logical pixels.

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 {
  // Spawns this component with a PrimaryWindow component
  primary_window: Some(Window::default()),
  // Configures the cursor of the primary window
  primary_cursor_options: Some(CursorOptions::default()),
  // Close our app when all windows close
  exit_condition: ExitCondition::OnAllClosed,
  // Should we close windows when the user does?
  close_when_requested: true,
}

WindowPlugin registers the window messages and adds the systems that handle closing and exiting. Those exit systems run in the Last schedule and are grouped in the ExitSystems set.

The messages we can read from are:

pub enum WindowEvent {
  AppLifecycle(AppLifecycle),
  CursorEntered(CursorEntered),
  CursorLeft(CursorLeft),
  CursorMoved(CursorMoved),
  FileDragAndDrop(FileDragAndDrop),
  Ime(Ime),
  RequestRedraw(RequestRedraw),

  WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged),
  WindowCloseRequested(WindowCloseRequested),
  WindowCreated(WindowCreated),
  WindowDestroyed(WindowDestroyed),
  WindowFocused(WindowFocused),
  WindowMoved(WindowMoved),
  WindowOccluded(WindowOccluded),
  WindowResized(WindowResized),
  WindowScaleFactorChanged(WindowScaleFactorChanged),
  WindowThemeChanged(WindowThemeChanged),

  MouseButtonInput(MouseButtonInput),
  MouseMotion(MouseMotion),
  MouseWheel(MouseWheel),

  PinchGesture(PinchGesture),
  RotationGesture(RotationGesture),
  DoubleTapGesture(DoubleTapGesture),
  PanGesture(PanGesture),

  TouchInput(TouchInput),

  KeyboardInput(KeyboardInput),
  KeyboardFocusLost(KeyboardFocusLost),
}

WindowClosing and WindowClosed are also registered messages, but they are not variants of WindowEvent.

It's common to want to exit your game with a hotkey when prototyping. We can put a system like this into your game to close when pressing Esc

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

pub fn close_on_esc(
  mut commands: Commands,
  focused_windows: Query<(Entity, &Window)>,
  input: Res<ButtonInput<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.

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

How often the app updates based on the focus state of its windows is controlled with the bevy::winit::WinitSettings resource:

use bevy::{prelude::*, winit::WinitSettings};
use std::time::Duration;

fn main() {
  App::new()
    // Set the background color of our window
    .insert_resource(ClearColor(Color::srgb(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())
    // Or we can customize the settings ourselves
    .insert_resource(WinitSettings {
      focused_mode: bevy::winit::UpdateMode::Continuous,
      unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(
        Duration::from_millis(10),
      ),
    });
}

WinitSettings comes from the bevy::winit crate, 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.

This is the division of labour between the two crates. WindowPlugin registers the messages and the close/exit systems, while WinitPlugin owns the OS window and runs the internal despawn_windows system that actually closes it.

We can initialize a Window through the WindowPlugin like:

use bevy::{
  prelude::*,
  window::{PresentMode, WindowResolution},
};

fn main() {
  App::new().add_plugins(DefaultPlugins.set(WindowPlugin {
    primary_window: Some(Window {
      title: "I am a window!".into(),
      resolution:
        WindowResolution::new(500, 300).with_scale_factor_override(1.0),
      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()
  }));
}

Bevy lets us customize plugin sets like DefaultPlugins which let us override the defaults of any core Bevy plugin like we do above.

Whatever we provide to the primary_window field of the WindowPlugin will become an entity with whatever we passed in plus a PrimaryWindow marker, and the required CursorOptions component.

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

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

fn get_window_dimensions(window_query: Query<&Window, With<PrimaryWindow>>) {
  if let Ok(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 Query and its data can be modified just like your other components.

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

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

Changing the resolution

For example we can change the resolution by overriding certain settings:

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

// This system shows how to request the window to resize the resolution
fn adjust_resolution(
  keys: Res<ButtonInput<KeyCode>>,
  mut window: Single<&mut Window>,
) {
  if keys.just_pressed(KeyCode::ArrowUp) {
    let scale_factor_override = window.resolution.scale_factor_override();

    window
      .resolution
      .set_scale_factor_override(scale_factor_override.map(|n| n + 1.0));
  }
}

More commonly you would set up some kind of resolution settings where you could toggle between fixed sizes:

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

/// Stores the various window-resolutions we can select between.
#[derive(Resource)]
struct ResolutionSettings {
  large: Vec2,
  medium: Vec2,
  small: Vec2,
}

fn toggle_resolution(
  keys: Res<ButtonInput<KeyCode>>,
  mut windows: Query<&mut Window>,
  resolution: Res<ResolutionSettings>,
) {
  if let Ok(mut window) = windows.single_mut() {
    if keys.just_pressed(KeyCode::Digit1) {
      let res = resolution.small;
      window.resolution.set(res.x, res.y);
    }
    if keys.just_pressed(KeyCode::Digit2) {
      let res = resolution.medium;
      window.resolution.set(res.x, res.y);
    }
    if keys.just_pressed(KeyCode::Digit3) {
      let res = resolution.large;
      window.resolution.set(res.x, res.y);
    }
  }
}

Updating text

We will also need to react to changes for things like Text:

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

/// Marker component for the text that displays the current resolution.
#[derive(Component)]
struct ResolutionText;

// 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: MessageReader<WindowResized>,
) {
  if let Ok(mut text) = q.single_mut() {
    for event in resize_reader.read() {
      // When resolution is being changed
      text.0 = format!("{:.1} x {:.1}", event.width, event.height);
    }
  }
}

Changing the window title:

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

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

Cursors

Each Window entity automatically gets a CursorOptions component. CursorOptions is our interface to interacting with the mouse and controls whether the cursor is visible, how it is grabbed through grab_mode, and whether mouse events hit_test this window.

The grab_mode field holds a CursorGrabMode which lets us define how a user's 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,
}

Support for the grab modes is platform specific: macOS does not support Confined and falls back to None, while X11 does not support Locked and falls back to Confined. The visual cursor itself is set separately with the CursorIcon component.

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:

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

fn toggle_cursor(
  mut cursor_options: Single<&mut CursorOptions, With<Window>>,
  input: Res<ButtonInput<KeyCode>>,
) {
  if input.just_pressed(KeyCode::Space) {
    cursor_options.visible = !cursor_options.visible;
    cursor_options.grab_mode = match cursor_options.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:

use bevy::{prelude::*, winit::WinitSettings};
use std::time::Duration;

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .insert_resource(WinitSettings {
      focused_mode: bevy::winit::UpdateMode::Continuous,
      unfocused_mode: bevy::winit::UpdateMode::reactive_low_power(
        Duration::from_millis(10),
      ),
    });
}

UpdateMode controls how often the app updates, independently of VSync. When a window is in UpdateMode::reactive_low_power the app updates in response to window and user events, after the configured wait duration, or when a RequestRedraw is sent. It deliberately ignores device events such as raw mouse motion, which is great for saving CPU when the app is not being interacted with.

UpdateMode::Continuous updates as fast as it can, and is the default for a focused window. When unfocused bevy switches to reactive_low_power by default.

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

To take screenshots we spawn a Screenshot component on the render target we are interested in capturing.

To actually save them we can use the save_to_disk observer.

use bevy::{
  prelude::*,
  render::view::screenshot::{save_to_disk, Screenshot},
  window::{
    CursorGrabMode, CursorOptions, PresentMode, PrimaryWindow, WindowResized,
    WindowResolution,
  },
  winit::WinitSettings,
};
use std::time::Duration;

fn take_screenshot(
  mut commands: Commands,
  input: Res<ButtonInput<KeyCode>>,
  primary_window: Query<Entity, With<PrimaryWindow>>,
) {
  if input.just_pressed(KeyCode::Space) {
    let path = "screenshot.png";
    commands
      .spawn(Screenshot::primary_window())
      .observe(save_to_disk(path));
  }
}