use bevy_ecs::hierarchy::ChildOf;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use std::cell::{Cell, RefCell};
use tracing::{debug, trace, warn};
use windows::Win32::Foundation::*;
use windows::Win32::UI::HiDpi::{AdjustWindowRectExForDpi, GetDpiForWindow};
use windows::Win32::UI::WindowsAndMessaging::*;
use crate::api::*;
use crate::ecs::Visual;
use crate::ecs::layout::LayoutRoot;
#[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
})
}
}
thread_local! {
static IS_SELF_INITIATED: Cell<bool> = const { Cell::new(false) };
}
pub fn is_self_initiated() -> bool {
IS_SELF_INITIATED.get()
}
struct SetWindowPosGuard;
impl Drop for SetWindowPosGuard {
fn drop(&mut self) {
IS_SELF_INITIATED.set(false);
trace!(is_initiated = false, "IS_SELF_INITIATED reset by guard");
}
}
pub unsafe fn guarded_set_window_pos(
hwnd: HWND,
hwnd_insert_after: Option<HWND>,
x: i32,
y: i32,
cx: i32,
cy: i32,
flags: SET_WINDOW_POS_FLAGS,
) -> windows::core::Result<()> {
IS_SELF_INITIATED.set(true);
let _guard = SetWindowPosGuard;
trace!(
hwnd = format!("0x{:X}", hwnd.0 as usize),
x = x, y = y, cx = cx, cy = cy,
flags = ?flags,
"[guarded_set_window_pos] Calling SetWindowPos"
);
unsafe { SetWindowPos(hwnd, hwnd_insert_after, x, y, cx, cy, flags) }?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct SetWindowPosCommand {
pub hwnd: HWND,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub flags: SET_WINDOW_POS_FLAGS,
pub hwnd_insert_after: Option<HWND>,
}
thread_local! {
static WINDOW_POS_COMMANDS: RefCell<Vec<SetWindowPosCommand>> = const { RefCell::new(Vec::new()) };
}
impl SetWindowPosCommand {
pub fn new(
hwnd: HWND,
x: i32,
y: i32,
width: i32,
height: i32,
flags: SET_WINDOW_POS_FLAGS,
hwnd_insert_after: Option<HWND>,
) -> Self {
Self {
hwnd,
x,
y,
width,
height,
flags,
hwnd_insert_after,
}
}
pub fn enqueue(cmd: SetWindowPosCommand) {
trace!(
hwnd = ?cmd.hwnd,
x = cmd.x,
y = cmd.y,
width = cmd.width,
height = cmd.height,
"SetWindowPosCommand::enqueue"
);
WINDOW_POS_COMMANDS.with(|cell| {
cell.borrow_mut().push(cmd);
});
}
pub fn flush() {
WINDOW_POS_COMMANDS.with(|cell| {
let commands: Vec<_> = cell.borrow_mut().drain(..).collect();
if commands.is_empty() {
return;
}
trace!(
count = commands.len(),
"SetWindowPosCommand::flush processing"
);
for cmd in commands {
let result = unsafe {
guarded_set_window_pos(
cmd.hwnd,
cmd.hwnd_insert_after,
cmd.x,
cmd.y,
cmd.width,
cmd.height,
cmd.flags,
)
};
if let Err(e) = result {
warn!(
hwnd = ?cmd.hwnd,
error = ?e,
"SetWindowPos failed"
);
} else {
trace!(hwnd = ?cmd.hwnd, "SetWindowPos succeeded");
}
}
});
}
}
pub fn flush_window_pos_commands() {
SetWindowPosCommand::flush();
}
pub fn find_owner_window(world: &World, entity: Entity) -> Option<Entity> {
if world.get::<Window>(entity).is_some() {
return Some(entity);
}
let mut current = entity;
while let Some(child_of) = world.get::<ChildOf>(current) {
let parent = child_of.parent();
if world.get::<Window>(parent).is_some() {
return Some(parent);
}
current = parent;
}
None
}
#[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, Debug, Copy, Clone)]
#[component(storage = "SparseSet", on_add = on_window_handle_add, on_remove = on_window_handle_remove)]
pub struct WindowHandle {
pub hwnd: HWND,
pub instance: HINSTANCE,
}
unsafe impl Send for WindowHandle {}
unsafe impl Sync for WindowHandle {}
impl WindowHandle {
pub fn get_dpi(&self) -> u32 {
unsafe { GetDpiForWindow(self.hwnd) }
}
pub fn get_style(&self) -> Result<(WINDOW_STYLE, WINDOW_EX_STYLE), String> {
let style = match get_window_long_ptr(self.hwnd, GWL_STYLE) {
Ok(v) => WINDOW_STYLE(v as u32),
Err(e) => {
return Err(format!(
"GetWindowLongPtrW(GWL_STYLE) failed for HWND {:?}: {:?}",
self.hwnd, e
));
}
};
let ex_style = match get_window_long_ptr(self.hwnd, GWL_EXSTYLE) {
Ok(v) => WINDOW_EX_STYLE(v as u32),
Err(e) => {
return Err(format!(
"GetWindowLongPtrW(GWL_EXSTYLE) failed for HWND {:?}: {:?}",
self.hwnd, e
));
}
};
Ok((style, ex_style))
}
pub fn client_to_window_rect(&self, client_rect: RECT) -> Result<RECT, String> {
let (style, ex_style) = self.get_style()?;
let dpi = self.get_dpi();
if dpi == 0 {
return Err(format!(
"GetDpiForWindow returned 0 for HWND {:?}",
self.hwnd
));
}
let mut rect = client_rect;
let result = unsafe { AdjustWindowRectExForDpi(&mut rect, style, false, ex_style, dpi) };
if result.is_err() {
return Err(format!(
"AdjustWindowRectExForDpi failed for HWND {:?}: {:?}",
self.hwnd, result
));
}
Ok(rect)
}
pub fn window_to_client_rect(&self, window_rect: RECT) -> Result<RECT, String> {
let origin_client = RECT {
left: 0,
top: 0,
right: 100, bottom: 100,
};
let origin_window = self.client_to_window_rect(origin_client)?;
let left_diff = origin_window.left - origin_client.left;
let top_diff = origin_window.top - origin_client.top;
let right_diff = origin_window.right - origin_client.right;
let bottom_diff = origin_window.bottom - origin_client.bottom;
Ok(RECT {
left: window_rect.left - left_diff,
top: window_rect.top - top_diff,
right: window_rect.right - right_diff,
bottom: window_rect.bottom - bottom_diff,
})
}
pub fn client_to_window_coords(
&self,
position: POINT,
size: SIZE,
) -> Result<(i32, i32, i32, i32), String> {
let client_rect = RECT {
left: position.x,
top: position.y,
right: position.x + size.cx,
bottom: position.y + size.cy,
};
let window_rect = self.client_to_window_rect(client_rect)?;
Ok((
window_rect.left,
window_rect.top,
window_rect.right - window_rect.left,
window_rect.bottom - window_rect.top,
))
}
pub fn window_to_client_coords(
&self,
x: i32,
y: i32,
width: i32,
height: i32,
) -> Result<(POINT, SIZE), String> {
let window_rect = RECT {
left: x,
top: y,
right: x + width,
bottom: y + height,
};
let client_rect = self.window_to_client_rect(window_rect)?;
Ok((
POINT {
x: client_rect.left,
y: client_rect.top,
},
SIZE {
cx: client_rect.right - client_rect.left,
cy: client_rect.bottom - client_rect.top,
},
))
}
}
fn on_window_handle_add(
mut world: bevy_ecs::world::DeferredWorld,
hook: bevy_ecs::lifecycle::HookContext,
) {
let entity = hook.entity;
if let Some(handle) = world.get::<WindowHandle>(entity) {
let hwnd = handle.hwnd;
debug!(
entity = ?entity,
hwnd = ?hwnd,
"WindowHandle added"
);
let dpi_value = unsafe { GetDpiForWindow(hwnd) };
let dpi_component = if dpi_value > 0 {
DPI::from_dpi(dpi_value as u16, dpi_value as u16)
} else {
DPI::default() };
debug!(
entity = ?entity,
dpi_x = dpi_component.dpi_x,
dpi_y = dpi_component.dpi_y,
scale_x = format_args!("{:.2}", dpi_component.scale_x()),
scale_y = format_args!("{:.2}", dpi_component.scale_y()),
"DPI component inserted"
);
world.commands().entity(entity).insert(dpi_component);
if let Some(mut app) = world.get_resource_mut::<crate::ecs::app::App>() {
app.on_window_created(entity);
}
}
}
fn on_window_handle_remove(
mut world: bevy_ecs::world::DeferredWorld,
hook: bevy_ecs::lifecycle::HookContext,
) {
let entity = hook.entity;
if let Some(handle) = world.get::<WindowHandle>(entity) {
let hwnd = handle.hwnd;
debug!(
entity = ?entity,
hwnd = ?hwnd,
"Entity being removed, sending WM_CLOSE"
);
if let Some(mut app) = world.get_resource_mut::<crate::ecs::app::App>() {
app.on_window_destroyed(entity);
}
unsafe {
let _ = PostMessageW(Some(hwnd), WM_CLOSE, WPARAM(0), LPARAM(0));
}
}
}
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[component(storage = "SparseSet")]
pub struct DPI {
pub dpi_x: u16,
pub dpi_y: u16,
}
impl Default for DPI {
fn default() -> Self {
Self {
dpi_x: 96,
dpi_y: 96,
}
}
}
impl DPI {
pub fn from_dpi(x_dpi: u16, y_dpi: u16) -> Self {
Self {
dpi_x: x_dpi,
dpi_y: y_dpi,
}
}
#[allow(non_snake_case)]
pub fn from_WM_DPICHANGED(wparam: WPARAM, _lparam: LPARAM) -> Self {
let x_dpi = (wparam.0 & 0xFFFF) as u16;
let y_dpi = ((wparam.0 >> 16) & 0xFFFF) as u16;
Self::from_dpi(x_dpi, y_dpi)
}
pub fn scale_x(&self) -> f32 {
self.dpi_x as f32 / 96.0
}
pub fn scale_y(&self) -> f32 {
self.dpi_y as f32 / 96.0
}
#[inline]
pub fn to_logical_x(&self, physical: i32) -> f32 {
physical as f32 / self.scale_x()
}
#[inline]
pub fn to_logical_y(&self, physical: i32) -> f32 {
physical as f32 / self.scale_y()
}
#[inline]
pub fn to_logical_size(&self, width: i32, height: i32) -> (f32, f32) {
(self.to_logical_x(width), self.to_logical_y(height))
}
#[inline]
pub fn to_logical_point(&self, x: i32, y: i32) -> (f32, f32) {
(self.to_logical_x(x), self.to_logical_y(y))
}
#[inline]
pub fn to_physical_x(&self, logical: f32) -> i32 {
(logical * self.scale_x()).round() as i32
}
#[inline]
pub fn to_physical_y(&self, logical: f32) -> i32 {
(logical * self.scale_y()).round() as i32
}
#[inline]
pub fn to_physical_size(&self, width: f32, height: f32) -> (i32, i32) {
(self.to_physical_x(width), self.to_physical_y(height))
}
#[inline]
pub fn to_physical_point(&self, x: f32, y: f32) -> (i32, i32) {
(self.to_physical_x(x), self.to_physical_y(y))
}
}
#[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_OVERLAPPEDWINDOW | WS_VISIBLE,
ex_style: WS_EX_NOREDIRECTIONBITMAP,
}
}
}
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 })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZOrder {
NoChange,
TopMost,
NoTopMost,
Top,
Bottom,
InsertAfter(HWND),
}
impl Default for ZOrder {
fn default() -> Self {
ZOrder::NoChange
}
}
unsafe impl Send for ZOrder {}
unsafe impl Sync for ZOrder {}
#[derive(Component, Debug, Clone, Copy, PartialEq)]
pub struct WindowPos {
pub zorder: ZOrder,
pub position: Option<POINT>,
pub size: Option<SIZE>,
pub no_redraw: bool, pub no_activate: bool, pub frame_changed: bool, pub show_window: bool, pub hide_window: bool, pub no_copy_bits: bool, pub no_owner_zorder: bool, pub no_send_changing: bool, pub defer_erase: bool, pub async_window_pos: bool, }
impl Default for WindowPos {
fn default() -> Self {
Self {
zorder: ZOrder::NoChange,
position: Some(POINT {
x: CW_USEDEFAULT,
y: CW_USEDEFAULT,
}),
size: Some(SIZE {
cx: CW_USEDEFAULT,
cy: CW_USEDEFAULT,
}),
no_redraw: false,
no_activate: false,
frame_changed: false,
show_window: false,
hide_window: false,
no_copy_bits: false,
no_owner_zorder: false,
no_send_changing: false,
defer_erase: false,
async_window_pos: false,
}
}
}
unsafe impl Send for WindowPos {}
unsafe impl Sync for WindowPos {}
impl WindowPos {
pub fn new() -> Self {
Self::default()
}
pub fn with_zorder(mut self, zorder: ZOrder) -> Self {
self.zorder = zorder;
self
}
pub fn zorder_no_change(mut self) -> Self {
self.zorder = ZOrder::NoChange;
self
}
pub fn zorder_topmost(mut self) -> Self {
self.zorder = ZOrder::TopMost;
self
}
pub fn zorder_notopmost(mut self) -> Self {
self.zorder = ZOrder::NoTopMost;
self
}
pub fn zorder_top(mut self) -> Self {
self.zorder = ZOrder::Top;
self
}
pub fn zorder_bottom(mut self) -> Self {
self.zorder = ZOrder::Bottom;
self
}
pub fn zorder_insert_after(mut self, hwnd: HWND) -> Self {
self.zorder = ZOrder::InsertAfter(hwnd);
self
}
pub fn with_position(mut self, position: POINT) -> Self {
self.position = Some(position);
self
}
pub fn with_size(mut self, size: SIZE) -> Self {
self.size = Some(size);
self
}
pub fn no_redraw(mut self, value: bool) -> Self {
self.no_redraw = value;
self
}
pub fn no_activate(mut self, value: bool) -> Self {
self.no_activate = value;
self
}
pub fn frame_changed(mut self, value: bool) -> Self {
self.frame_changed = value;
self
}
pub fn show_window(mut self, value: bool) -> Self {
self.show_window = value;
self
}
pub fn hide_window(mut self, value: bool) -> Self {
self.hide_window = value;
self
}
pub fn no_copy_bits(mut self, value: bool) -> Self {
self.no_copy_bits = value;
self
}
pub fn no_owner_zorder(mut self, value: bool) -> Self {
self.no_owner_zorder = value;
self
}
pub fn no_send_changing(mut self, value: bool) -> Self {
self.no_send_changing = value;
self
}
pub fn defer_erase(mut self, value: bool) -> Self {
self.defer_erase = value;
self
}
pub fn async_window_pos(mut self, value: bool) -> Self {
self.async_window_pos = value;
self
}
fn build_flags(&self) -> SET_WINDOW_POS_FLAGS {
let mut flags = SET_WINDOW_POS_FLAGS(0);
if self.position.is_none() {
flags |= SWP_NOMOVE;
}
if self.size.is_none() {
flags |= SWP_NOSIZE;
}
if self.zorder == ZOrder::NoChange {
flags |= SWP_NOZORDER;
}
if self.no_redraw {
flags |= SWP_NOREDRAW;
}
if self.no_activate {
flags |= SWP_NOACTIVATE;
}
if self.frame_changed {
flags |= SWP_FRAMECHANGED;
}
if self.show_window {
flags |= SWP_SHOWWINDOW;
}
if self.hide_window {
flags |= SWP_HIDEWINDOW;
}
if self.no_copy_bits {
flags |= SWP_NOCOPYBITS;
}
if self.no_owner_zorder {
flags |= SWP_NOOWNERZORDER;
}
if self.no_send_changing {
flags |= SWP_NOSENDCHANGING;
}
if self.defer_erase {
flags |= SWP_DEFERERASE;
}
if self.async_window_pos {
flags |= SWP_ASYNCWINDOWPOS;
}
flags
}
pub fn build_flags_for_system(&self) -> SET_WINDOW_POS_FLAGS {
self.build_flags()
}
pub fn get_hwnd_insert_after(&self) -> Option<HWND> {
match self.zorder {
ZOrder::NoChange => None,
ZOrder::TopMost => Some(HWND_TOPMOST),
ZOrder::NoTopMost => Some(HWND_NOTOPMOST),
ZOrder::Top => Some(HWND_TOP),
ZOrder::Bottom => Some(HWND_BOTTOM),
ZOrder::InsertAfter(hwnd) => Some(hwnd),
}
}
pub fn set_window_pos(&self, hwnd: HWND) -> windows::core::Result<()> {
let (x, y) = if let Some(pos) = self.position {
(pos.x, pos.y)
} else {
(0, 0)
};
let (width, height) = if let Some(size) = self.size {
(size.cx, size.cy)
} else {
(0, 0)
};
let flags = self.build_flags();
let hwnd_insert_after = self.get_hwnd_insert_after();
unsafe { SetWindowPos(hwnd, hwnd_insert_after, x, y, width, height, flags) }
}
pub fn to_window_coords(
&self,
window_handle: &WindowHandle,
) -> Result<(i32, i32, i32, i32), String> {
let position = self.position.unwrap_or(POINT { x: 0, y: 0 });
let size = self.size.unwrap_or(SIZE { cx: 0, cy: 0 });
window_handle.client_to_window_coords(position, size)
}
pub fn to_window_coords_for_creation(
&self,
style: WINDOW_STYLE,
ex_style: WINDOW_EX_STYLE,
_dpi: u32,
) -> (i32, i32, i32, i32) {
let position = self.position.unwrap_or(POINT {
x: CW_USEDEFAULT,
y: CW_USEDEFAULT,
});
let size = self.size.unwrap_or(SIZE {
cx: CW_USEDEFAULT,
cy: CW_USEDEFAULT,
});
if position.x == CW_USEDEFAULT || size.cx == CW_USEDEFAULT {
return (position.x, position.y, size.cx, size.cy);
}
let dpi = 96;
let mut rect = RECT {
left: position.x,
top: position.y,
right: position.x + size.cx,
bottom: position.y + size.cy,
};
let result = unsafe { AdjustWindowRectExForDpi(&mut rect, style, false, ex_style, dpi) };
if result.is_err() {
warn!(
error = ?result,
"AdjustWindowRectExForDpi failed during window creation, using original values"
);
return (position.x, position.y, size.cx, size.cy);
}
(
rect.left,
rect.top,
rect.right - rect.left,
rect.bottom - rect.top,
)
}
}
struct SetWindowParentToLayoutRoot {
entity: Entity,
}
impl Command for SetWindowParentToLayoutRoot {
fn apply(self, world: &mut World) {
let mut query = world.query_filtered::<Entity, With<LayoutRoot>>();
let layout_root = query.iter(world).next();
if let Some(root) = layout_root {
if let Ok(mut entity_mut) = world.get_entity_mut(self.entity) {
if !entity_mut.contains::<ChildOf>() {
debug!(
root = ?root,
entity = ?self.entity,
"Setting ChildOf for Window entity"
);
entity_mut.insert(ChildOf(root));
}
}
}
}
}
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"
);
}
}