use std::cell::RefCell;
use std::rc::Rc;
use bevy_ecs::prelude::Entity;
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, RECT, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, RegisterClassExW, SET_WINDOW_POS_FLAGS,
SWP_NOACTIVATE, SWP_NOSIZE, SWP_NOZORDER, WINDOW_EX_STYLE, WINDOWPOS, WM_DISPLAYCHANGE,
WM_DPICHANGED, WM_WINDOWPOSCHANGED, WNDCLASSEXW, WS_EX_TOOLWINDOW, WS_POPUP,
};
use windows::core::{PCWSTR, w};
use crate::ecs::App;
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::transition_diag::{
self, KIND_MSG, KIND_WINDPI, KIND_WRITE, MISSING, MSG_DISPLAYCHANGE, MSG_DPICHANGED,
MSG_WINDOWPOSCHANGED, ORIGIN_DPI_SUGGESTED, RECORD_PREFIX_TAG, TRANSITION_TARGET,
};
use crate::ecs::window::{DPI, DpiSuggestedRectPolicy, SetWindowPosCommand, WindowHandle};
use crate::ecs::world::EcsWorld;
use crate::executor::util::WindowMessage;
const SIGNOFF_DIRECTIVES: &str = "info,wintf::transition=debug";
const DEFAULT_DIRECTIVES: &str = "info";
const CONTROL_LINE: &str = "[transition-probe] alive";
fn emit_control() {
tracing::info!(target: TRANSITION_TARGET, "{CONTROL_LINE}");
}
fn clean_slate() {
let _residue = crate::ecs::window::drain_window_pos_commands();
let _context = crate::ecs::window::DpiChangeContext::take();
transition_diag::reset_for_test();
}
fn transition_lines(captured: &str) -> Vec<&str> {
captured
.lines()
.filter(|line| line.contains(RECORD_PREFIX_TAG))
.collect()
}
fn lines_of_kind<'a>(captured: &'a str, kind: &str) -> Vec<&'a str> {
let needle = format!("kind={kind} ");
transition_lines(captured)
.into_iter()
.filter(|line| line.contains(&needle))
.collect()
}
fn hwnd_field(hwnd: HWND) -> String {
format!("hwnd=0x{:X}", hwnd.0 as usize)
}
fn dpichanged_message(hwnd: HWND, new_dpi: u16, suggested: &RECT) -> WindowMessage {
WindowMessage {
hwnd,
msg: WM_DPICHANGED,
wparam: WPARAM(((new_dpi as usize) << 16) | new_dpi as usize),
lparam: LPARAM(suggested as *const RECT as isize),
}
}
fn capture_dpichanged(directives: &str, policy: Option<DpiSuggestedRectPolicy>) -> String {
clean_slate();
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = {
let mut w = world.borrow_mut();
let mut e = w.world_mut().spawn(DPI::from_dpi(96, 96));
if let Some(p) = policy {
e.insert(p);
}
e.id()
};
let suggested = RECT {
left: 3210,
top: 140,
right: 3810,
bottom: 620,
};
let m = dpichanged_message(HWND(std::ptr::null_mut()), 192, &suggested);
let out = capture_under_filter(directives, || {
emit_control();
let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
});
clean_slate();
out
}
fn capture_dpichanged_with_entity(directives: &str, with_dpi: bool) -> (String, Entity) {
clean_slate();
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = {
let mut w = world.borrow_mut();
let mut e = w.world_mut().spawn(());
if with_dpi {
e.insert(DPI::from_dpi(96, 96));
}
e.id()
};
let suggested = RECT {
left: 3210,
top: 140,
right: 3810,
bottom: 620,
};
let m = dpichanged_message(HWND(std::ptr::null_mut()), 192, &suggested);
let out = capture_under_filter(directives, || {
emit_control();
let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
});
clean_slate();
(out, entity)
}
fn capture_windowposchanged(directives: &str) -> String {
clean_slate();
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = world.borrow_mut().world_mut().spawn(()).id();
let windowpos = WINDOWPOS {
hwnd: HWND(std::ptr::null_mut()),
hwndInsertAfter: HWND(std::ptr::null_mut()),
x: 100,
y: 200,
cx: 640,
cy: 480,
flags: SET_WINDOW_POS_FLAGS(0),
};
let m = WindowMessage {
hwnd: HWND(std::ptr::null_mut()),
msg: WM_WINDOWPOSCHANGED,
wparam: WPARAM(0),
lparam: LPARAM(&windowpos as *const WINDOWPOS as isize),
};
let out = capture_under_filter(directives, || {
emit_control();
let _ = crate::ecs::dispatch_window_message(&world, entity, &m);
});
clean_slate();
out
}
#[test]
fn dpichanged_receipt_is_recorded_with_handle_and_flush_context() {
let captured = capture_dpichanged(SIGNOFF_DIRECTIVES, None);
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
let msgs = lines_of_kind(&captured, KIND_MSG);
assert_eq!(msgs.len(), 1, "受理 1 回につき 1 行: {captured}");
let line = msgs[0];
assert!(
line.contains(&format!("msg={MSG_DPICHANGED} ")),
"メッセージ名が載る: {line}"
);
assert!(
line.contains(&hwnd_field(HWND(std::ptr::null_mut()))),
"対象ハンドルが載る: {line}"
);
assert!(
line.contains("in_swp=false"),
"窓書込の外側で受理したので in_swp は偽: {line}"
);
assert!(
line.contains("since_flush_us=-"),
"一括書込の外で受理したので経過は番兵: {line}"
);
}
#[test]
fn windowposchanged_receipt_is_recorded_outside_a_window_write() {
let captured = capture_windowposchanged(SIGNOFF_DIRECTIVES);
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
let msgs = lines_of_kind(&captured, KIND_MSG);
assert_eq!(msgs.len(), 1, "受理 1 回につき 1 行: {captured}");
assert!(
msgs[0].contains(&format!("msg={MSG_WINDOWPOSCHANGED} ")),
"メッセージ名が載る: {}",
msgs[0]
);
assert!(
msgs[0].contains("in_swp=false") && msgs[0].contains("since_flush_us=-"),
"自前の窓書込を伴わない受理は両フィールドとも「外側」を示す: {}",
msgs[0]
);
}
#[test]
fn displaychange_receipt_is_recorded_and_still_delegates_to_defwindowproc() {
clean_slate();
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = {
let mut w = world.borrow_mut();
w.world_mut().insert_resource(App::new());
w.world_mut().spawn(()).id()
};
let m = WindowMessage {
hwnd: HWND(0x4321 as *mut _),
msg: WM_DISPLAYCHANGE,
wparam: WPARAM(0),
lparam: LPARAM(0),
};
let mut ret: Option<LRESULT> = Some(LRESULT(1));
let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
ret = crate::ecs::dispatch_window_message(&world, entity, &m);
});
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
assert!(
ret.is_none(),
"観測を足したことで WM_DISPLAYCHANGE の戻り値が変わっている(既定手続きへの委譲が失われる)"
);
assert!(
world
.borrow()
.world()
.resource::<App>()
.display_configuration_changed(),
"表示構成変更フラグが立っていない(既存の挙動を壊している)"
);
let msgs = lines_of_kind(&captured, KIND_MSG);
assert_eq!(msgs.len(), 1, "受理 1 回につき 1 行: {captured}");
assert!(
msgs[0].contains(&format!("msg={MSG_DISPLAYCHANGE} "))
&& msgs[0].contains(&hwnd_field(HWND(0x4321 as *mut _))),
"メッセージ名と対象ハンドルが載る: {}",
msgs[0]
);
clean_slate();
}
#[test]
fn dpichanged_records_the_window_origin_when_the_component_is_rewritten() {
let (captured, entity) = capture_dpichanged_with_entity(SIGNOFF_DIRECTIVES, true);
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
let records = lines_of_kind(&captured, KIND_WINDPI);
assert_eq!(
records.len(),
1,
"component の書き換え 1 回につき 1 行: {captured}"
);
let line = records[0];
assert!(
line.contains(&format!("entity={entity:?}")),
"書き換えた窓の entity が載る: {line}"
);
assert!(
line.contains("old_dpi=96 new_dpi=192"),
"新旧の表示 DPI が同一行から復元できない: {line}"
);
}
#[test]
fn dpichanged_records_no_window_origin_without_a_dpi_component() {
let (captured, _entity) = capture_dpichanged_with_entity(SIGNOFF_DIRECTIVES, false);
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
assert_eq!(
lines_of_kind(&captured, KIND_MSG).len(),
1,
"対照の受理行が出ていない=檻の前提が崩れている: {captured}"
);
assert!(
lines_of_kind(&captured, KIND_WINDPI).is_empty(),
"component を書き換えていないのに起点が出ている: {captured}"
);
}
#[test]
fn the_window_origin_is_silent_under_the_default_directives() {
let (captured, _entity) = capture_dpichanged_with_entity(DEFAULT_DIRECTIVES, true);
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
assert!(
lines_of_kind(&captured, KIND_WINDPI).is_empty(),
"観測 target を点けない運転では 1 行も出ない: {captured}"
);
}
#[test]
fn applied_suggested_position_is_recorded_as_a_synchronous_write() {
let captured = capture_dpichanged(
"info,wintf::ecs::window_proc=debug,wintf::transition=debug",
None,
);
assert!(
captured.contains("[WM_DPICHANGED] suggested position write decision")
&& captured.contains("applied=true"),
"採否判定の既存ログが失われている(判定を変えていないことの対照): {captured}"
);
let writes = lines_of_kind(&captured, KIND_WRITE);
assert_eq!(
writes.len(),
1,
"採用した遷移では同期書込が 1 行記録される: {captured}"
);
let write = writes[0];
assert!(
write.contains(" stage=sync "),
"一括書込(stage=flush)と区別できる段で記録する: {write}"
);
assert!(write.contains("seq=0"), "同期書込の通し番号は 0: {write}");
assert!(
write.contains("x=3210 y=140 cx=0 cy=0"),
"採用した提案原点がそのまま載る(寸は SWP_NOSIZE ゆえ 0): {write}"
);
assert!(
write.contains(&format!("origin={ORIGIN_DPI_SUGGESTED} scope=- win_kind=-")),
"経路語は wintf 自身が名乗る(判定側は `path_a_writes` をこの語で数える)。\
キャラ番号と窓種別は areka の語彙ゆえ番兵: {write}"
);
assert_ne!(
ORIGIN_DPI_SUGGESTED, MISSING,
"経路語が番兵と同じでは「タグの付け忘れ」と区別が付かない"
);
assert!(
write.contains("ok=false"),
"null ハンドルへの書込は失敗する: {write}"
);
assert!(
write.contains("ax=- ay=- aw=- ah=-"),
"読み戻せない窓の書込後矩形は 4 フィールドとも番兵: {write}"
);
assert!(write.contains("call_us="), "1 回の所要が載る: {write}");
}
#[test]
fn the_synchronous_write_origin_is_distinct_from_the_enqueue_site_origins() {
use crate::ecs::window::transition_diag::{ORIGIN_WINDOW_POS, ORIGIN_ZORDER_PAIR};
let words = [ORIGIN_DPI_SUGGESTED, ORIGIN_WINDOW_POS, ORIGIN_ZORDER_PAIR];
let mut sorted = words.to_vec();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), words.len(), "経路語が互いに異なっていない");
for word in words {
assert_ne!(word, MISSING, "経路語が番兵と同じ: {word}");
assert!(
!word.contains(' '),
"経路語に空白があると `名前=値` の辞書化が壊れる: {word}"
);
}
}
#[test]
fn a_declined_suggested_position_records_the_receipt_but_no_write() {
let captured = capture_dpichanged(
"info,wintf::ecs::window_proc=debug,wintf::transition=debug",
Some(DpiSuggestedRectPolicy::ExternalAuthority),
);
assert!(
captured.contains("applied=false"),
"位置権威が外部にある窓の採否判定が変わっている(`dpi-window-vanish` の規約): {captured}"
);
assert_eq!(
lines_of_kind(&captured, KIND_MSG).len(),
1,
"書かない窓でも受理そのものは記録される: {captured}"
);
assert!(
lines_of_kind(&captured, KIND_WRITE).is_empty(),
"書かなかったのに書込レコードが出ている: {captured}"
);
}
#[test]
fn message_handlers_emit_nothing_under_the_default_directives() {
for captured in [
capture_dpichanged(DEFAULT_DIRECTIVES, None),
capture_windowposchanged(DEFAULT_DIRECTIVES),
] {
assert!(
captured.contains(CONTROL_LINE),
"対照が拾えていない=捕捉が死んでいる: {captured}"
);
assert!(
transition_lines(&captured).is_empty(),
"観測 target を点けない運転では観測行は 1 行も出ない: {captured}"
);
}
}
thread_local! {
static DISPATCH_TARGET: RefCell<Option<(Rc<RefCell<EcsWorld>>, Entity)>> =
const { RefCell::new(None) };
}
unsafe extern "system" fn test_wndproc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
let target = DISPATCH_TARGET.with(|cell| cell.borrow().clone());
if let Some((world, entity)) = target {
let message = WindowMessage {
hwnd,
msg,
wparam,
lparam,
};
if let Some(result) = crate::ecs::dispatch_window_message(&world, entity, &message) {
return result;
}
}
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
const TEST_CLASS: PCWSTR = w!("wintf-transition-msg-test");
fn register_test_class() {
use std::sync::Once;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
unsafe {
let hinstance: HINSTANCE = GetModuleHandleW(None).expect("GetModuleHandleW").into();
let class = WNDCLASSEXW {
cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
lpfnWndProc: Some(test_wndproc),
hInstance: hinstance,
lpszClassName: TEST_CLASS,
..Default::default()
};
assert_ne!(
RegisterClassExW(&class),
0,
"テスト窓クラスの登録に失敗した"
);
}
});
}
fn create_test_window() -> (HWND, HINSTANCE) {
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
register_test_class();
unsafe {
let hinstance: HINSTANCE = GetModuleHandleW(None).expect("GetModuleHandleW").into();
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE(WS_EX_TOOLWINDOW.0),
TEST_CLASS,
w!("transition-msg"),
WS_POPUP,
10,
10,
0,
0,
None,
None,
Some(hinstance),
None,
)
.expect("CreateWindowExW should create a test window");
(hwnd, hinstance)
}
}
#[test]
fn windowposchanged_received_inside_a_window_write_carries_in_swp_and_flush_elapsed() {
clean_slate();
let (hwnd, instance) = create_test_window();
let world = Rc::new(RefCell::new(EcsWorld::new()));
let entity = world
.borrow_mut()
.world_mut()
.spawn((
WindowHandle { hwnd, instance },
crate::ecs::window::WindowPos::default(),
DPI::from_dpi(96, 96),
))
.id();
DISPATCH_TARGET.with(|cell| *cell.borrow_mut() = Some((world.clone(), entity)));
let captured = capture_under_filter(SIGNOFF_DIRECTIVES, || {
emit_control();
SetWindowPosCommand::enqueue(SetWindowPosCommand::new(
hwnd,
30,
40,
0,
0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
None,
));
crate::ecs::window::flush_window_pos_commands();
});
DISPATCH_TARGET.with(|cell| *cell.borrow_mut() = None);
unsafe {
let _ = DestroyWindow(hwnd);
}
clean_slate();
assert!(captured.contains(CONTROL_LINE), "捕捉が死んでいる");
let lines = transition_lines(&captured);
let begin_at = lines
.iter()
.position(|l| l.contains("kind=flush ") && l.contains("stage=begin"))
.unwrap_or_else(|| panic!("一括書込の開始行が無い: {captured}"));
let write_at = lines
.iter()
.position(|l| l.contains("kind=write "))
.unwrap_or_else(|| panic!("書込レコードが無い: {captured}"));
let msg_at = lines
.iter()
.position(|l| {
l.contains("kind=msg ") && l.contains(&format!("msg={MSG_WINDOWPOSCHANGED} "))
})
.unwrap_or_else(|| {
panic!("`SetWindowPos` が同期送達した受理が記録されていない: {captured}")
});
let line = lines[msg_at];
assert!(
line.contains(&hwnd_field(hwnd)),
"受理行が対象の実窓を指していない: {line}"
);
assert!(
line.contains("in_swp=true"),
"`SetWindowPos` の内側での受理なのに in_swp が偽(同期送達の証跡が消えている): {line}"
);
assert!(
!line.contains("since_flush_us=-"),
"一括書込の内側での受理なのに経過が番兵(入れ子の解除が外側の起点まで消している): {line}"
);
assert!(
begin_at < msg_at && msg_at < write_at,
"受理が一括書込の区間の内側に並んでいない(begin={begin_at} msg={msg_at} write={write_at}): {captured}"
);
}
fn code_only(source: &str) -> String {
source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn message_handlers_keep_the_front_guard_so_the_default_run_pays_no_observation_cost() {
let code = code_only(include_str!("window_pos.rs"));
assert_eq!(
code.matches("transition_diag::is_enabled()").count(),
4,
"前置ガードの数が記録点の数と食い違う——ガードされていない記録点がある"
);
assert_eq!(
code.matches("transition_diag::stamp()").count(),
4,
"刻印を読む回数が記録点の数と食い違う"
);
assert_eq!(
code.matches("transition_diag::since_flush_us()").count(),
2,
"受理レコードは 2 点(`WM_DPICHANGED`・`WM_WINDOWPOSCHANGED`)だけが経過を読む"
);
assert!(
code.contains("let observe = transition_diag::is_enabled();"),
"同期書込の前置ガードが失われている——既定運転でも計時と読み戻しの費用を払う"
);
assert!(
code.contains("observe.then(Instant::now)"),
"同期書込の計時がガードの外で行われている"
);
assert_eq!(
code.matches("Instant::now").count(),
1,
"本文に `Instant::now` が 2 度以上ある(ガードされていない計時が混ざり得る)"
);
assert_eq!(
code.matches("read_back_window_rect(").count(),
1,
"書込後矩形の読み戻しの呼出が 1 箇所でない(ガードの外から呼ばれ得る)"
);
assert!(
code.contains("pub(super) fn WM_WINDOWPOSCHANGED("),
"説明文を落とす処理が本文まで落としている"
);
let lifecycle = code_only(include_str!("lifecycle.rs"));
assert_eq!(
lifecycle.matches("transition_diag::is_enabled()").count(),
1,
"`WM_DISPLAYCHANGE` の記録点の前置ガードが 1 つでない"
);
assert_eq!(
lifecycle.matches("transition_diag::stamp()").count(),
1,
"`WM_DISPLAYCHANGE` が刻印を読む回数が 1 でない"
);
assert!(
lifecycle.contains("pub(super) fn WM_DISPLAYCHANGE("),
"説明文を落とす処理が本文まで落としている"
);
}