Skip to main content

wde_renderer/core/
window.rs

1//! Window plugin and related components
2//! This module contains the window plugin and related components.
3//! It is responsible for creating and managing the window.
4
5use bevy::{
6    a11y::AccessibilityPlugin,
7    app::{PluginGroup, PluginGroupBuilder},
8    ecs::{message::Message, system::NonSendMarker},
9    prelude::{Entity, Event, MessageReader, MessageWriter, Query, Res, ResMut, Resource, With},
10    utils::default,
11    window::{PresentMode, PrimaryWindow, Window, WindowPlugin, WindowResized, WindowTheme},
12    winit::{WINIT_WINDOWS, WinitPlugin}
13};
14use wde_wgpu::instance;
15
16use crate::core::RenderInstance;
17
18use super::extract_macros::ExtractWorld;
19
20/// Raw RGBA8 icon data used for the window icon shown in the OS taskbar/title bar.
21/// Not supported on all platforms (e.g. Wayland ignores it; use a `.desktop` file there instead).
22#[derive(Clone)]
23pub struct WindowIcon {
24    pub rgba: Vec<u8>,
25    pub width: u32,
26    pub height: u32
27}
28impl WindowIcon {
29    /// Decodes an encoded image (PNG, JPEG, ...) into an icon, for example from `include_bytes!`.
30    pub fn from_bytes(bytes: &[u8]) -> Result<Self, image::ImageError> {
31        let image = image::load_from_memory(bytes)?.into_rgba8();
32        let (width, height) = image.dimensions();
33        Ok(Self {
34            rgba: image.into_raw(),
35            width,
36            height
37        })
38    }
39}
40
41/// Holds the icon to apply to the primary window once it has been created.
42#[derive(Resource, Default)]
43pub(crate) struct PrimaryWindowIcon(pub Option<WindowIcon>);
44
45/// Applies the configured icon (if any) to the primary window.
46/// Must run after the primary window has been created by winit (e.g. in `Startup`).
47///
48/// `WinitWindows` isn't stored as a regular non-send ECS resource in this bevy version (it lives in
49/// a thread-local instead), so `NonSendMarker` is used to force this system onto the main thread,
50/// which is the only thread the winit windows thread-local is populated on.
51pub(crate) fn apply_window_icon(
52    icon: Res<PrimaryWindowIcon>,
53    _main_thread: NonSendMarker,
54    primary_window: Query<Entity, With<PrimaryWindow>>
55) {
56    let Some(icon) = &icon.0 else { return };
57    let Ok(entity) = primary_window.single() else {
58        return;
59    };
60
61    WINIT_WINDOWS.with_borrow(|winit_windows| {
62        let Some(winit_window) = winit_windows.get_window(entity) else {
63            return;
64        };
65
66        match winit::window::Icon::from_rgba(icon.rgba.clone(), icon.width, icon.height) {
67            Ok(winit_icon) => winit_window.set_window_icon(Some(winit_icon)),
68            Err(err) => wde_logger::warn!("Failed to set window icon: {err}")
69        }
70    });
71}
72
73/// An event that is sent when the surface is resized.
74/// This event is sent with the new width and height of the surface. It is used to update the surface configuration and resize the swap chain.
75#[derive(Debug, Event, Message)]
76pub struct SurfaceResized {
77    pub width: u32,
78    pub height: u32
79}
80
81pub(crate) struct WindowPlugins {
82    pub title: String,
83    pub resolution: (u32, u32)
84}
85impl Default for WindowPlugins {
86    fn default() -> Self {
87        Self {
88            title: "WaterDropEngine".into(),
89            resolution: (600, 500)
90        }
91    }
92}
93impl PluginGroup for WindowPlugins {
94    fn build(self) -> PluginGroupBuilder {
95        let mut group = PluginGroupBuilder::start::<Self>();
96
97        // Add window and winit plugins
98        group = group
99            .add(WindowPlugin {
100                primary_window: Some(Window {
101                    title: self.title,
102                    name: Some("waterdropengine".into()),
103                    resolution: self.resolution.into(),
104                    present_mode: PresentMode::AutoVsync,
105                    fit_canvas_to_parent: true,
106                    prevent_default_event_handling: false,
107                    window_theme: Some(WindowTheme::Dark),
108                    enabled_buttons: bevy::window::EnabledButtons {
109                        maximize: true,
110                        ..Default::default()
111                    },
112                    visible: true,
113                    ..default()
114                }),
115                ..default()
116            })
117            .add::<WinitPlugin>(WinitPlugin::default())
118            .add(AccessibilityPlugin);
119
120        group
121    }
122}
123
124/// Send surface resized events with the physical window size.
125pub(crate) fn send_surface_resized(
126    mut events_writer: MessageWriter<SurfaceResized>,
127    mut events_reader: MessageReader<WindowResized>,
128    window: Query<&Window>
129) {
130    for _ in events_reader.read() {
131        if let Ok(window) = window.single() {
132            let (width, height) = (
133                window.resolution.physical_width().max(1),
134                window.resolution.physical_height().max(1)
135            );
136
137            // Check if window was minimized
138            if width == 0 && height == 0 {
139                continue;
140            }
141
142            // Send the surface resized event
143            events_writer.write(SurfaceResized { width, height });
144        }
145    }
146}
147
148/// Extract the window size from the primary window and update the surface configuration.
149pub(crate) fn extract_surface_size(
150    render_instance: ResMut<RenderInstance>,
151    windows: ExtractWorld<Query<&Window>>
152) {
153    // Check if there is a window
154    if windows.iter().count() == 0 {
155        return;
156    }
157
158    // Get the window size
159    let window = windows.single().unwrap();
160    let (width, height) = (
161        window.resolution.physical_width().max(1),
162        window.resolution.physical_height().max(1)
163    );
164
165    // Check if size different from old one
166    let mut render_instance = render_instance.0.write().unwrap();
167    let old_size = render_instance.surface_config.as_ref().unwrap();
168    if width == old_size.width && height == old_size.height {
169        return;
170    }
171
172    // Update the surface configuration
173    let surface_config = render_instance.surface_config.as_mut().unwrap();
174    surface_config.width = width;
175    surface_config.height = height;
176
177    instance::resize(
178        &render_instance.device,
179        render_instance.surface.as_ref().unwrap(),
180        render_instance.surface_config.as_ref().unwrap()
181    );
182}