use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use std::cell::RefCell;
use std::path::PathBuf;
use tracing::{debug, trace};
use windows::Win32::Foundation::*;
use windows::Win32::UI::HiDpi::GetDpiForSystem;
use windows::Win32::UI::WindowsAndMessaging::*;
use crate::api::*;
use crate::ecs::Visual;
use super::dpi::DPI;
use super::window_pos::{SetWindowParentToLayoutRoot, WindowPos};
#[derive(Debug, Clone)]
pub struct DpiChangeContext {
pub new_dpi: DPI,
pub suggested_rect: RECT,
}
thread_local! {
static DPI_CHANGE_CONTEXT: RefCell<Option<DpiChangeContext>> = const { RefCell::new(None) };
}
impl DpiChangeContext {
pub fn new(new_dpi: DPI, suggested_rect: RECT) -> Self {
Self {
new_dpi,
suggested_rect,
}
}
pub fn set(ctx: DpiChangeContext) {
trace!(
dpi_x = ctx.new_dpi.dpi_x,
dpi_y = ctx.new_dpi.dpi_y,
suggested_left = ctx.suggested_rect.left,
suggested_top = ctx.suggested_rect.top,
suggested_right = ctx.suggested_rect.right,
suggested_bottom = ctx.suggested_rect.bottom,
"DpiChangeContext::set"
);
DPI_CHANGE_CONTEXT.with(|cell| {
*cell.borrow_mut() = Some(ctx);
});
}
pub fn take() -> Option<DpiChangeContext> {
DPI_CHANGE_CONTEXT.with(|cell| {
let ctx = cell.borrow_mut().take();
if let Some(ref c) = ctx {
trace!(
dpi_x = c.new_dpi.dpi_x,
dpi_y = c.new_dpi.dpi_y,
"DpiChangeContext::take consumed"
);
}
ctx
})
}
}
#[derive(Component, Debug, Clone)]
#[component(on_add = on_window_add)]
pub struct Window {
pub title: String,
pub parent: Option<HWND>,
}
impl Default for Window {
fn default() -> Self {
Self {
title: "Window".to_string(),
parent: None,
}
}
}
unsafe impl Send for Window {}
unsafe impl Sync for Window {}
#[derive(Component, Clone, Copy)]
#[component(storage = "SparseSet")]
pub struct OnCloseRequest(pub fn(world: &mut World, entity: Entity));
#[derive(Component, Clone, Copy)]
#[component(storage = "SparseSet")]
pub struct OnSessionEnd(pub fn(world: &mut World, entity: Entity));
#[derive(Component, Clone, Copy)]
#[component(storage = "SparseSet")]
pub struct OnFilesDropped(pub fn(world: &mut World, entity: Entity, paths: Vec<PathBuf>));
#[derive(Component, Debug, Clone, Copy, PartialEq)]
pub struct WindowStyle {
pub style: WINDOW_STYLE,
pub ex_style: WINDOW_EX_STYLE,
}
impl Default for WindowStyle {
fn default() -> Self {
Self {
style: WS_POPUP | WS_VISIBLE,
ex_style: WS_EX_LAYERED,
}
}
}
impl WindowStyle {
pub fn from_hwnd(hwnd: HWND) -> windows::core::Result<Self> {
let style = WINDOW_STYLE(get_window_long_ptr(hwnd, GWL_STYLE)? as u32);
let ex_style = WINDOW_EX_STYLE(get_window_long_ptr(hwnd, GWL_EXSTYLE)? as u32);
Ok(Self { style, ex_style })
}
}
fn on_window_add(mut world: DeferredWorld, context: HookContext) {
let entity = context.entity;
world
.commands()
.queue(SetWindowParentToLayoutRoot { entity });
if world.get::<Visual>(entity).is_none() {
world.commands().entity(entity).insert(Visual::default());
}
if world.get::<WindowPos>(entity).is_none() {
world.commands().entity(entity).insert(WindowPos::default());
debug!(
entity = ?entity,
"WindowPos component inserted in on_window_add"
);
}
if world.get::<DPI>(entity).is_none() {
let system_dpi = unsafe { GetDpiForSystem() } as u16;
let dpi = if system_dpi > 0 {
DPI::from_dpi(system_dpi, system_dpi)
} else {
DPI::default()
};
world.commands().entity(entity).insert(dpi);
debug!(
entity = ?entity,
dpi_x = dpi.dpi_x,
dpi_y = dpi.dpi_y,
"DPI pre-initialized with system DPI in on_window_add"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_window_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Window>();
}
#[test]
fn test_window_default_fields() {
let w = Window::default();
assert_eq!(w.title, "Window");
assert!(w.parent.is_none());
}
#[test]
fn test_window_style_default_is_popup_visible_layered() {
let style = WindowStyle::default();
assert_eq!(style.style, WS_POPUP | WS_VISIBLE);
assert_eq!(style.ex_style, WS_EX_LAYERED);
}
#[test]
fn test_window_style_default_does_not_use_overlappedwindow() {
let style = WindowStyle::default();
assert_eq!(
style.style & WS_OVERLAPPEDWINDOW,
WS_POPUP & WS_OVERLAPPEDWINDOW,
"既定スタイルは WS_OVERLAPPEDWINDOW のキャプション/ボーダービットを含まない"
);
assert_eq!(style.style & WS_CAPTION, WINDOW_STYLE(0));
}
#[test]
fn test_window_style_partial_eq() {
let a = WindowStyle::default();
let b = WindowStyle::default();
assert_eq!(a, b);
let c = WindowStyle {
style: WS_POPUP,
ex_style: WS_EX_LAYERED,
};
assert_ne!(a, c);
}
#[test]
fn test_dpi_change_context_new_stores_fields() {
let dpi = DPI::from_dpi(144, 144);
let rect = RECT {
left: 1,
top: 2,
right: 3,
bottom: 4,
};
let ctx = DpiChangeContext::new(dpi, rect);
assert_eq!(ctx.new_dpi, dpi);
assert_eq!(ctx.suggested_rect.left, 1);
assert_eq!(ctx.suggested_rect.bottom, 4);
}
#[test]
fn test_dpi_change_context_take_returns_none_when_unset() {
let _ = DpiChangeContext::take();
assert!(DpiChangeContext::take().is_none());
}
#[test]
fn test_dpi_change_context_set_then_take_consumes() {
let _ = DpiChangeContext::take();
let dpi = DPI::from_dpi(120, 120);
let rect = RECT {
left: 10,
top: 20,
right: 110,
bottom: 120,
};
DpiChangeContext::set(DpiChangeContext::new(dpi, rect));
let taken = DpiChangeContext::take().expect("context should be present after set");
assert_eq!(taken.new_dpi, dpi);
assert_eq!(taken.suggested_rect.right, 110);
assert!(
DpiChangeContext::take().is_none(),
"take は消費的(take 後は None に戻る)"
);
}
}