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, 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, once winit has created it (which
46/// happens after `Startup`): the icon is consumed when applied.
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    mut icon: ResMut<PrimaryWindowIcon>,
53    _main_thread: NonSendMarker,
54    primary_window: Query<Entity, With<PrimaryWindow>>
55) {
56    let Some(window_icon) = &icon.0 else { return };
57    let Ok(entity) = primary_window.single() else {
58        return;
59    };
60
61    let applied = WINIT_WINDOWS.with_borrow(|winit_windows| {
62        let Some(winit_window) = winit_windows.get_window(entity) else {
63            return false;
64        };
65        match winit::window::Icon::from_rgba(
66            window_icon.rgba.clone(),
67            window_icon.width,
68            window_icon.height
69        ) {
70            Ok(winit_icon) => winit_window.set_window_icon(Some(winit_icon)),
71            Err(err) => wde_logger::warn!("Failed to set window icon: {err}")
72        }
73        true
74    });
75    if applied {
76        icon.0 = None;
77    }
78}
79
80/// An event that is sent when the surface is resized.
81/// 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.
82#[derive(Debug, Event, Message)]
83pub struct SurfaceResized {
84    pub width: u32,
85    pub height: u32
86}
87
88pub(crate) struct WindowPlugins {
89    pub title: String,
90    pub resolution: (u32, u32)
91}
92impl Default for WindowPlugins {
93    fn default() -> Self {
94        Self {
95            title: "WaterDropEngine".into(),
96            resolution: (600, 500)
97        }
98    }
99}
100impl PluginGroup for WindowPlugins {
101    fn build(self) -> PluginGroupBuilder {
102        let mut group = PluginGroupBuilder::start::<Self>();
103
104        // Add window and winit plugins
105        group = group
106            .add(WindowPlugin {
107                primary_window: Some(Window {
108                    title: self.title,
109                    name: Some("waterdropengine".into()),
110                    resolution: self.resolution.into(),
111                    present_mode: PresentMode::AutoVsync,
112                    fit_canvas_to_parent: true,
113                    prevent_default_event_handling: false,
114                    window_theme: Some(WindowTheme::Dark),
115                    enabled_buttons: bevy::window::EnabledButtons {
116                        maximize: true,
117                        ..Default::default()
118                    },
119                    visible: true,
120                    ..default()
121                }),
122                ..default()
123            })
124            .add::<WinitPlugin>(WinitPlugin::default())
125            .add(AccessibilityPlugin);
126
127        group
128    }
129}
130
131/// Send surface resized events with the physical window size.
132pub(crate) fn send_surface_resized(
133    mut events_writer: MessageWriter<SurfaceResized>,
134    mut events_reader: MessageReader<WindowResized>,
135    window: Query<&Window>
136) {
137    for _ in events_reader.read() {
138        if let Ok(window) = window.single() {
139            let (width, height) = (
140                window.resolution.physical_width().max(1),
141                window.resolution.physical_height().max(1)
142            );
143
144            // Check if window was minimized
145            if width == 0 && height == 0 {
146                continue;
147            }
148
149            // Send the surface resized event
150            events_writer.write(SurfaceResized { width, height });
151        }
152    }
153}
154
155/// Extract the window size from the primary window and update the surface configuration.
156pub(crate) fn extract_surface_size(
157    render_instance: ResMut<RenderInstance>,
158    windows: ExtractWorld<Query<&Window>>
159) {
160    // Check if there is a window
161    if windows.iter().count() == 0 {
162        return;
163    }
164
165    // Get the window size
166    let window = windows.single().unwrap();
167    let (width, height) = (
168        window.resolution.physical_width().max(1),
169        window.resolution.physical_height().max(1)
170    );
171
172    // Check if size different from old one
173    let mut render_instance = render_instance.0.write().unwrap();
174    let old_size = render_instance.surface_config.as_ref().unwrap();
175    if width == old_size.width && height == old_size.height {
176        return;
177    }
178
179    // Update the surface configuration
180    let surface_config = render_instance.surface_config.as_mut().unwrap();
181    surface_config.width = width;
182    surface_config.height = height;
183
184    instance::resize(
185        &render_instance.device,
186        render_instance.surface.as_ref().unwrap(),
187        render_instance.surface_config.as_ref().unwrap()
188    );
189}