use std::cell::RefCell;
use std::rc::Rc;
use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, WPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DestroyWindow, SW_SHOWNA, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
SWP_NOZORDER, SetWindowPos, ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WM_ACTIVATE, WS_CHILD,
WS_EX_TOOLWINDOW, WS_POPUP, WS_VISIBLE,
};
use windows::core::{PCWSTR, w};
use super::{SinkObservationPending, is_behind_foreground, mark_pair_sink_observation};
use crate::api::get_window_below;
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::zorder_pair::{FrontScan, measure_windows_in_front};
use crate::ecs::window::{
KeepDirectlyAbove, SetWindowPosCommand, WindowHandle, ZOrderPairStrategy,
apply_zorder_pair_maintenance,
};
use crate::ecs::world::EcsWorld;
use crate::executor::util::WindowMessage;
const CAPTURE_DIRECTIVES: &str =
"info,wintf::ecs::window::zorder_pair=debug,wintf::ecs::window::command=debug";
const SINK_TAG: &str = "[zorder-pair] sink-observed";
const SKIP_TAG: &str = "[zorder-pair] skip";
const SET_WINDOW_POS_TAG: &str = "[guarded_set_window_pos] Calling SetWindowPos";
const ORPHAN_MARK: &str = "沈降観測の目印がペア宣言の無い窓";
const WA_INACTIVE: usize = 0;
const WA_ACTIVE: usize = 1;
fn create_window(
title: PCWSTR,
ex_style: WINDOW_EX_STYLE,
style: WINDOW_STYLE,
parent: Option<HWND>,
) -> HWND {
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
unsafe {
let hinstance = GetModuleHandleW(None).expect("GetModuleHandleW");
CreateWindowExW(
ex_style,
w!("Static"),
title,
style,
0,
0,
0,
0,
parent,
None,
Some(hinstance.into()),
None,
)
.expect("CreateWindowExW should create a test window")
}
}
fn create_test_parent(title: PCWSTR) -> HWND {
let parent = create_window(
title,
WINDOW_EX_STYLE(WS_EX_TOOLWINDOW.0),
WINDOW_STYLE(WS_POPUP.0),
None,
);
unsafe {
let _ = ShowWindow(parent, SW_SHOWNA);
}
parent
}
fn create_test_child(parent: HWND, title: PCWSTR) -> HWND {
create_window(
title,
WINDOW_EX_STYLE(0),
WINDOW_STYLE(WS_CHILD.0 | WS_VISIBLE.0),
Some(parent),
)
}
fn create_hidden_test_child(parent: HWND, title: PCWSTR) -> HWND {
create_window(
title,
WINDOW_EX_STYLE(0),
WINDOW_STYLE(WS_CHILD.0),
Some(parent),
)
}
fn insert_after(hwnd: HWND, after: HWND) {
unsafe {
SetWindowPos(
hwnd,
Some(after),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
)
.expect("SetWindowPos should reorder self-owned test windows");
}
}
fn destroy_test_window(hwnd: HWND) {
unsafe {
let _ = DestroyWindow(hwnd);
}
}
fn fake_hwnd(v: usize) -> HWND {
HWND(v as *mut _)
}
fn spawn_window(world: &mut World, hwnd: HWND) -> Entity {
world
.spawn(WindowHandle {
hwnd,
instance: HINSTANCE::default(),
})
.id()
}
fn declare_pair(world: &mut World, balloon: Entity, character: Entity) {
world
.entity_mut(balloon)
.insert(KeepDirectlyAbove { peer: character });
}
fn maintain_schedule() -> Schedule {
let mut schedule = Schedule::default();
schedule.set_executor(SingleThreadedExecutor::new());
schedule.add_systems(apply_zorder_pair_maintenance);
schedule
}
fn issue_control_command(hwnd: HWND) {
SetWindowPosCommand::enqueue(SetWindowPosCommand::new(
hwnd,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER,
None,
));
SetWindowPosCommand::flush();
}
fn lines_with<'a>(out: &'a str, tag: &str) -> Vec<&'a str> {
out.lines().filter(|l| l.contains(tag)).collect()
}
fn assert_line_count(out: &str, tag: &str, expected: usize, what: &str) {
let found = lines_with(out, tag);
assert_eq!(
found.len(),
expected,
"{what}: `{tag}` の行は {expected} 本のはずが {} 本だった\n---捕捉全文---\n{out}",
found.len()
);
}
struct Pair {
parent: HWND,
character: HWND,
balloon: HWND,
}
impl Pair {
fn create(tag: PCWSTR) -> Self {
let parent = create_test_parent(tag);
let character = create_test_child(parent, w!("character"));
let balloon = create_test_child(parent, w!("balloon"));
insert_after(character, balloon);
let pair = Self {
parent,
character,
balloon,
};
assert_eq!(
get_window_below(pair.balloon).expect("GetWindow は成功するはず"),
Some(pair.character),
"前提: バルーンの 1 つ背後はキャラ窓(=隣接している)"
);
pair
}
fn destroy(self) {
destroy_test_window(self.parent);
}
}
fn dispatch_activate(world: &Rc<RefCell<EcsWorld>>, entity: Entity, activation_state: usize) {
let message = WindowMessage {
hwnd: HWND(std::ptr::null_mut()),
msg: WM_ACTIVATE,
wparam: WPARAM(activation_state),
lparam: LPARAM(0),
};
crate::ecs::dispatch_window_message(world, entity, &message);
}
#[test]
fn deactivation_marks_only_and_the_measurement_lands_on_the_next_pass() {
let observed = Pair::create(w!("wintf-zorder-sink-delayed"));
let control = Pair::create(w!("wintf-zorder-sink-delayed-control"));
let world = Rc::new(RefCell::new(EcsWorld::new()));
let (character, balloon, control_balloon) = {
let mut borrow = world.borrow_mut();
let w = borrow.world_mut();
w.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(w, observed.character);
let balloon = spawn_window(w, observed.balloon);
declare_pair(w, balloon, character);
let control_character = spawn_window(w, control.character);
let control_balloon = spawn_window(w, control.balloon);
declare_pair(w, control_balloon, control_character);
w.entity_mut(control_balloon).insert(SinkObservationPending);
(character, balloon, control_balloon)
};
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(world.borrow_mut().world_mut());
dispatch_activate(&world, character, WA_INACTIVE);
});
assert_line_count(
&out,
SINK_TAG,
1,
"非活性化の巡には測らない(出るのは対照の 1 本だけ)",
);
assert!(
lines_with(&out, SINK_TAG)[0].contains(&format!("entity={control_balloon:?}")),
"この巡の記録は対照ペアを指すはず: {}",
lines_with(&out, SINK_TAG)[0]
);
assert!(
world
.borrow()
.world()
.entity(balloon)
.contains::<SinkObservationPending>(),
"非活性化の受理で、宣言を持つ側(バルーン窓)へ目印が付くはず"
);
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(world.borrow_mut().world_mut());
});
assert_line_count(&out, SINK_TAG, 1, "遅らせた実測は次の巡にちょうど 1 本");
let line = lines_with(&out, SINK_TAG)[0];
assert!(
line.contains(&format!("entity={balloon:?}")),
"記録は非活性化されたペアの宣言を持つ側を指すはず: {line}"
);
assert!(
line.contains("adjacency_ok=true"),
"沈んでもペアの隣接は保たれているはず: {line}"
);
assert!(
!world
.borrow()
.world()
.entity(balloon)
.contains::<SinkObservationPending>(),
"目印は一回限りで、測った巡に消えるはず"
);
control.destroy();
observed.destroy();
}
#[test]
fn activation_does_not_mark_a_sink_observation() {
let pair = Pair::create(w!("wintf-zorder-sink-activate"));
let world = Rc::new(RefCell::new(EcsWorld::new()));
let (character, balloon) = {
let mut borrow = world.borrow_mut();
let w = borrow.world_mut();
let character = spawn_window(w, pair.character);
let balloon = spawn_window(w, pair.balloon);
declare_pair(w, balloon, character);
(character, balloon)
};
dispatch_activate(&world, character, WA_ACTIVE);
assert!(
!world
.borrow()
.world()
.entity(balloon)
.contains::<SinkObservationPending>(),
"活性化では目印を付けない"
);
dispatch_activate(&world, character, WA_INACTIVE);
assert!(
world
.borrow()
.world()
.entity(balloon)
.contains::<SinkObservationPending>(),
"非活性化なら目印が付くはず"
);
pair.destroy();
}
#[test]
fn the_sink_observation_happens_only_once_per_mark() {
let pair = Pair::create(w!("wintf-zorder-sink-once"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, pair.character);
let balloon = spawn_window(&mut world, pair.balloon);
declare_pair(&mut world, balloon, character);
world.entity_mut(balloon).insert(SinkObservationPending);
let mut schedule = maintain_schedule();
let first = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
});
assert_line_count(&first, SINK_TAG, 1, "目印の次の巡に 1 本");
let second = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
schedule.run(&mut world);
issue_control_command(pair.balloon);
});
assert_line_count(&second, SINK_TAG, 0, "目印が消えた後は無音");
assert_line_count(
&second,
SET_WINDOW_POS_TAG,
1,
"対照の 1 本だけ(捕捉は生きている)",
);
pair.destroy();
}
#[test]
fn the_sink_observation_issues_no_command_and_moves_nothing() {
let parent = create_test_parent(w!("wintf-zorder-sink-readonly"));
let character_hwnd = create_test_child(parent, w!("character"));
let intruder_hwnd = create_test_child(parent, w!("intruder"));
let balloon_hwnd = create_test_child(parent, w!("balloon"));
insert_after(intruder_hwnd, balloon_hwnd);
insert_after(character_hwnd, intruder_hwnd);
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, character_hwnd);
let balloon = spawn_window(&mut world, balloon_hwnd);
declare_pair(&mut world, balloon, character);
world.entity_mut(balloon).insert(SinkObservationPending);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
SetWindowPosCommand::flush();
issue_control_command(balloon_hwnd);
});
assert_line_count(&out, SINK_TAG, 1, "観測の記録は 1 本");
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"観測は指令を出さない(出るのは対照の 1 本だけ)",
);
let line = lines_with(&out, SINK_TAG)[0];
assert!(
line.contains("adjacency_ok=false"),
"隣接が壊れていることが記録に出るはず: {line}"
);
assert_eq!(
get_window_below(balloon_hwnd).expect("GetWindow は成功するはず"),
Some(intruder_hwnd),
"観測は重なりを動かさない"
);
destroy_test_window(parent);
}
#[test]
fn the_sink_record_carries_the_foreground_relation() {
let pair = Pair::create(w!("wintf-zorder-sink-foreground"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, pair.character);
let balloon = spawn_window(&mut world, pair.balloon);
declare_pair(&mut world, balloon, character);
world.entity_mut(balloon).insert(SinkObservationPending);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
});
let line = lines_with(&out, SINK_TAG)[0];
assert!(line.contains("foreground="), "前面窓の欄が無い: {line}");
assert!(
line.contains("behind_foreground="),
"前面窓との相対の欄が無い: {line}"
);
assert!(
!line.contains("behind_foreground=true"),
"兄弟列の外に居る前面窓に対して「背面に居る」とは判定されないはず: {line}"
);
pair.destroy();
}
#[test]
fn the_mark_always_lands_on_the_declaration_holder() {
let mut world = World::new();
let character = spawn_window(&mut world, fake_hwnd(0x0E01));
let balloon = spawn_window(&mut world, fake_hwnd(0x0E02));
declare_pair(&mut world, balloon, character);
assert_eq!(
mark_pair_sink_observation(&mut world, character),
Some(balloon),
"peer 側の非活性化でも、目印は宣言を持つ側へ付くはず"
);
assert!(world.entity(balloon).contains::<SinkObservationPending>());
assert!(
!world.entity(character).contains::<SinkObservationPending>(),
"宣言を持たない側には付けない"
);
world.entity_mut(balloon).remove::<SinkObservationPending>();
assert_eq!(
mark_pair_sink_observation(&mut world, balloon),
Some(balloon)
);
assert!(world.entity(balloon).contains::<SinkObservationPending>());
let stray = spawn_window(&mut world, fake_hwnd(0x0E03));
assert_eq!(
mark_pair_sink_observation(&mut world, stray),
None,
"ペアの当事者でなければ観測すべきものが無い"
);
assert!(!world.entity(stray).contains::<SinkObservationPending>());
}
#[test]
fn a_mark_whose_peer_is_gone_skips_with_a_reason() {
let pair = Pair::create(w!("wintf-zorder-sink-peer-gone"));
let control = Pair::create(w!("wintf-zorder-sink-peer-gone-control"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, pair.character);
let balloon = spawn_window(&mut world, pair.balloon);
declare_pair(&mut world, balloon, character);
world.entity_mut(balloon).insert(SinkObservationPending);
world.entity_mut(character).despawn();
let control_character = spawn_window(&mut world, control.character);
let control_balloon = spawn_window(&mut world, control.balloon);
declare_pair(&mut world, control_balloon, control_character);
world
.entity_mut(control_balloon)
.insert(SinkObservationPending);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
});
assert_line_count(&out, SKIP_TAG, 1, "見送りは理由つきで 1 本だけ");
let skip = lines_with(&out, SKIP_TAG)[0];
assert!(
skip.contains("reason=PeerMissing"),
"見送りの理由は「相手が居ない」であるはず: {skip}"
);
assert!(
skip.contains(&format!("entity={balloon:?}")),
"見送りの記録は相手を失ったペアを指すはず: {skip}"
);
assert_line_count(&out, SINK_TAG, 1, "測れたのは対照のペアだけ");
assert!(
lines_with(&out, SINK_TAG)[0].contains(&format!("entity={control_balloon:?}")),
"実測記録は対照のペアを指すはず"
);
assert!(
!world.entity(balloon).contains::<SinkObservationPending>(),
"測れなくても目印はこの巡で消える(毎巡の再評価を残さない)"
);
control.destroy();
pair.destroy();
}
#[test]
fn a_mark_without_handles_skips_with_a_reason() {
let control = Pair::create(w!("wintf-zorder-sink-handleless-control"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = world.spawn_empty().id();
let balloon = spawn_window(&mut world, fake_hwnd(0x0F01));
declare_pair(&mut world, balloon, character);
world.entity_mut(balloon).insert(SinkObservationPending);
let control_character = spawn_window(&mut world, control.character);
let control_balloon = spawn_window(&mut world, control.balloon);
declare_pair(&mut world, control_balloon, control_character);
world
.entity_mut(control_balloon)
.insert(SinkObservationPending);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
});
assert_line_count(&out, SKIP_TAG, 1, "見送りは理由つきで 1 本だけ");
let skip = lines_with(&out, SKIP_TAG)[0];
assert!(skip.contains("reason=HandleMissing"), "{skip}");
assert!(skip.contains(&format!("entity={balloon:?}")), "{skip}");
assert_line_count(&out, SINK_TAG, 1, "測れたのは対照のペアだけ");
assert!(lines_with(&out, SINK_TAG)[0].contains(&format!("entity={control_balloon:?}")));
control.destroy();
}
#[test]
fn a_mark_on_a_window_without_declaration_is_dropped_with_a_record() {
let control = Pair::create(w!("wintf-zorder-sink-orphan-control"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let stray = spawn_window(&mut world, fake_hwnd(0x0F11));
world.entity_mut(stray).insert(SinkObservationPending);
let control_character = spawn_window(&mut world, control.character);
let control_balloon = spawn_window(&mut world, control.balloon);
declare_pair(&mut world, control_balloon, control_character);
world
.entity_mut(control_balloon)
.insert(SinkObservationPending);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
});
assert_line_count(&out, ORPHAN_MARK, 1, "宣言の無い目印は 1 度だけ記録される");
assert!(
lines_with(&out, ORPHAN_MARK)[0].contains(&format!("entity={stray:?}")),
"記録は宣言の無い窓を指すはず"
);
assert_line_count(&out, SINK_TAG, 1, "測れたのは対照のペアだけ");
assert!(
!world.entity(stray).contains::<SinkObservationPending>(),
"落とした目印は残さない"
);
control.destroy();
}
fn scan(windows: &[HWND], reached_top: bool) -> FrontScan {
FrontScan {
windows: windows.to_vec(),
reached_top,
}
}
#[test]
fn is_behind_foreground_reads_the_front_chain() {
let target = fake_hwnd(0x0101);
let front = fake_hwnd(0x0102);
let other = fake_hwnd(0x0103);
assert_eq!(
is_behind_foreground(target, Some(front), &scan(&[other, front], true)),
Some(true),
"前面窓が自分より手前の列に居れば背面に居る"
);
assert_eq!(
is_behind_foreground(target, Some(front), &scan(&[other], true)),
Some(false),
"最前面まで辿って前面窓が居なければ、自分は前面窓より背面ではない"
);
assert_eq!(
is_behind_foreground(target, Some(target), &scan(&[], true)),
Some(false),
"自分自身が前面窓なら背面ではない"
);
}
#[test]
fn is_behind_foreground_reports_absence_rather_than_a_false_negative() {
let target = fake_hwnd(0x0201);
let front = fake_hwnd(0x0202);
assert_eq!(
is_behind_foreground(target, None, &scan(&[], true)),
None,
"前面窓が取れなければ判定できない"
);
assert_eq!(
is_behind_foreground(target, Some(front), &scan(&[], false)),
None,
"列を最前面まで辿れていなければ「背面ではない」と断じない"
);
assert_eq!(
is_behind_foreground(target, Some(front), &scan(&[front], false)),
Some(true),
"見つかった時点で判定は確定する"
);
}
#[test]
fn measure_windows_in_front_collects_the_visible_chain_up_to_the_top() {
let parent = create_test_parent(w!("wintf-zorder-sink-scan"));
let back = create_test_child(parent, w!("back"));
let middle = create_test_child(parent, w!("middle"));
let hidden = create_hidden_test_child(parent, w!("hidden"));
let front = create_test_child(parent, w!("front"));
insert_after(hidden, front);
insert_after(middle, hidden);
insert_after(back, middle);
let scanned = measure_windows_in_front(back);
assert_eq!(
scanned.windows,
vec![middle, front],
"手前へ向かう順に集まり、不可視の窓は入らないはず"
);
assert!(
!scanned.windows.contains(&hidden),
"不可視の窓は手前の列に数えない"
);
assert!(scanned.reached_top, "最前面まで辿り着いたはず");
let from_front = measure_windows_in_front(front);
assert!(from_front.windows.is_empty());
assert!(from_front.reached_top);
destroy_test_window(parent);
}
#[test]
fn measure_windows_in_front_reports_failure_as_an_incomplete_chain() {
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
let scanned = measure_windows_in_front(HWND::default());
assert!(scanned.windows.is_empty());
assert!(
!scanned.reached_top,
"失敗を「最前面まで辿った」と偽らない(偽の判定を作らない)"
);
});
assert!(
!out.trim().is_empty(),
"走査の失敗は記録に残るはず(黙って潰さない)"
);
}