use bevy::prelude::*;
use bevy::ui::IsDefaultUiCamera;
use serde::Serialize;
use ts_rs::TS;
use crate::event::ReactEvents;
use crate::reconcile::OpApplyStats;
use crate::request::Request;
use crate::{react_event, react_request};
#[derive(Serialize, TS)]
pub struct WindowSize {
pub width: f32,
pub height: f32,
}
#[react_event(name = "resize")]
pub struct Resize(pub WindowSize);
#[react_request(name = "window.size", response = WindowSize)]
pub struct WindowSizeGet;
pub(crate) fn ui_viewport_size(
cameras: &Query<&Camera, With<IsDefaultUiCamera>>,
windows: &Query<&Window>,
) -> Option<Vec2> {
if let Ok(camera) = cameras.single()
&& let Some(size) = camera.logical_viewport_size()
{
return Some(size);
}
windows
.single()
.ok()
.map(|window| Vec2::new(window.width(), window.height()))
}
pub(crate) fn send_resize_events(
stats: Res<OpApplyStats>,
cameras: Query<&Camera, With<IsDefaultUiCamera>>,
windows: Query<&Window>,
events: ReactEvents,
mut last: Local<Option<Vec2>>,
) {
if stats.applied_count == 0 {
return;
}
let Some(size) = ui_viewport_size(&cameras, &windows) else {
return;
};
if *last != Some(size) {
*last = Some(size);
events.send(&Resize(WindowSize {
width: size.x,
height: size.y,
}));
}
}
pub(crate) fn handle_window_size_request(
req: On<Request<WindowSizeGet>>,
cameras: Query<&Camera, With<IsDefaultUiCamera>>,
windows: Query<&Window>,
) {
match ui_viewport_size(&cameras, &windows) {
Some(size) => req.respond(WindowSize {
width: size.x,
height: size.y,
}),
None => req.respond_err("no UI viewport available"),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ReactAppExt;
use crate::bridge::{OutboundResource, OutboundSender};
use crate::protocol::{Outbound, ResponseResult};
use crate::request::{RawRequest, ReactRequestRegistry};
use bevy::ecs::world::CommandQueue;
use bevy::window::WindowResolution;
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
fn test_app() -> (App, UnboundedReceiver<Outbound>) {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.init_resource::<OpApplyStats>();
let (tx, rx) = unbounded_channel::<Outbound>();
app.insert_resource(OutboundResource(tx));
app.add_systems(Update, send_resize_events);
(app, rx)
}
fn spawn_window(app: &mut App, width: u32, height: u32) -> Entity {
app.world_mut()
.spawn(Window {
resolution: WindowResolution::new(width, height),
..default()
})
.id()
}
fn expect_resize(rx: &mut UnboundedReceiver<Outbound>, width: f32, height: f32) {
match rx.try_recv().expect("a resize event") {
Outbound::Event { name, value } => {
assert_eq!(name, "resize");
assert_eq!(value["width"], width);
assert_eq!(value["height"], height);
}
other => panic!("expected Outbound::Event, got {other:?}"),
}
}
#[test]
fn resize_gates_on_first_batch_then_fires_once_per_change() {
let (mut app, mut rx) = test_app();
let window = spawn_window(&mut app, 800, 600);
app.update();
assert!(rx.try_recv().is_err(), "gated until a batch has applied");
app.world_mut().resource_mut::<OpApplyStats>().applied_count = 1;
app.update();
expect_resize(&mut rx, 800.0, 600.0);
app.update();
assert!(rx.try_recv().is_err(), "unchanged size must not re-send");
app.world_mut()
.get_mut::<Window>(window)
.unwrap()
.resolution = WindowResolution::new(1024, 768);
app.update();
expect_resize(&mut rx, 1024.0, 768.0);
app.update();
assert!(rx.try_recv().is_err());
}
fn dispatch_size_request(app: &mut App, tx: &OutboundSender, id: u64) {
app.world_mut()
.resource_scope(|world, registry: Mut<ReactRequestRegistry>| {
let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, world);
registry.dispatch(
RawRequest {
id,
name: "window.size".into(),
value: serde_json::Value::Null,
},
tx,
&mut commands,
);
queue.apply(world);
});
}
#[test]
fn window_size_request_responds_or_rejects() {
let mut app = App::new();
app.add_react_request_handler(handle_window_size_request);
let (tx, mut rx): (OutboundSender, UnboundedReceiver<Outbound>) = unbounded_channel();
dispatch_size_request(&mut app, &tx, 1);
assert!(matches!(
rx.try_recv(),
Ok(Outbound::Response {
id: 1,
result: ResponseResult::Err { .. },
})
));
spawn_window(&mut app, 640, 480);
dispatch_size_request(&mut app, &tx, 2);
match rx.try_recv() {
Ok(Outbound::Response {
id: 2,
result: ResponseResult::Ok { value },
}) => {
assert_eq!(value["width"], 640.0);
assert_eq!(value["height"], 480.0);
}
other => panic!("expected Ok response, got {other:?}"),
}
}
}