Tainted\\Coders

Bevy Windows

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

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

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:

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 which is also included in the DefaultPlugins.

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

use bevy::window::WindowResolution;

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:

use bevy::window::PresentMode;

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.

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

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.sections[0].value = 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.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:

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

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

use bevy::render::view::screenshot::ScreenshotManager;

fn take_screenshot(
  mut screenshot_manager: ResMut<ScreenshotManager>,
  input: Res<ButtonInput<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();
  }
}