Tainted\\Coders

Bevy Windows

Bevy version: 0.15Last 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.

Before Bevy 0.10, it 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:

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

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

Bevy before 0.14 used to have ReceivedCharacter, but it is now deprecated in favor of KeyboardInput while winit reworks their keyboard system.

Its common to want to exit your game with a hotkey when prototyping. Bevy used to expose a system (in < 0.14) bevy::window::close_on_esc that got removed. But its common to put a system like this into your game to close when pressing Esc

// Close the focused window whenever the escape key (Esc) is pressed
// This is useful for examples or prototyping.
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.

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).

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

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

use bevy::winit::WinitSettings;

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

WininitSettings is from another bevy crate bevy::winit 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 the despawn_windows system provided by the WininitPlugin.

We can initialize a Window through the WindowPlugin like:

use bevy::window::PresentMode;

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 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.

use bevy::window::PrimaryWindow;

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 Query and its data can be modified just like your other components.

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:

// This system shows how to request the window to resize the resolution
fn adjust_resolution(
  keys: Res<ButtonInput<KeyCode>>,
  mut windows: Query<&mut Window>
) {
  let mut window = windows.single_mut();
  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:

/// 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>,
) {
  let 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:

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

Changing the window 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:

use bevy::window::CursorGrabMode;

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

    window.cursor_options.visible = !window.cursor_options.visible;
    window.cursor_options.grab_mode = match window.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::utils::Duration;

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

When your window is in UpdateMode::reactive_low_power 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

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::render::view::screenshot::{save_to_disk, Screenshot};

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