use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DestroyWindow, SW_SHOWNA, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE,
SetWindowPos, ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WS_CHILD, WS_EX_TOOLWINDOW, WS_POPUP,
WS_VISIBLE,
};
use windows::core::{PCWSTR, w};
use super::{SIBLING_SCAN_LIMIT, measure_window_above, measure_window_below};
use crate::api::{get_window_above, get_window_below, is_window_visible};
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::{
KeepDirectlyAbove, ReassertZOrder, SetWindowPosCommand, WindowHandle, ZOrderPairStrategy,
apply_zorder_pair_maintenance,
};
const CAPTURE_DIRECTIVES: &str = "info,wintf::ecs::window::zorder_pair=debug";
const MAINTAIN_CAPTURE_DIRECTIVES: &str =
"info,wintf::ecs::window::zorder_pair=debug,wintf::ecs::window::command=debug";
const LIMIT_MARK: &str = "走査が上限に達しました";
const FAILURE_MARK: &str = "走査に失敗しました";
const FIX_TAG: &str = "[zorder-pair] fix";
const VERIFY_FAILED_TAG: &str = "[zorder-pair] verify-failed";
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_visible_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);
}
assert!(
is_window_visible(parent),
"前提: 親窓が可視でなければ子窓は決して可視にならない"
);
parent
}
fn create_visible_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_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);
}
}
struct Trio {
parent: HWND,
balloon: HWND,
intruder: HWND,
character: HWND,
}
impl Trio {
fn create(tag: PCWSTR, visible_intruder: bool) -> Self {
let parent = create_visible_test_parent(tag);
let balloon = create_visible_child(parent, w!("balloon"));
let intruder = if visible_intruder {
create_visible_child(parent, w!("intruder"))
} else {
create_hidden_child(parent, w!("intruder"))
};
let character = create_visible_child(parent, w!("character"));
insert_after(intruder, balloon);
insert_after(character, intruder);
let trio = Self {
parent,
balloon,
intruder,
character,
};
assert_eq!(
get_window_below(trio.balloon).expect("GetWindow は成功するはず"),
Some(trio.intruder),
"前提: 素の 1 歩ではバルーンの背後は割り込み窓"
);
assert!(
is_window_visible(trio.balloon) && is_window_visible(trio.character),
"前提: ペアの 2 窓はどちらも可視"
);
assert_eq!(
is_window_visible(trio.intruder),
visible_intruder,
"前提: 割り込み窓の可視性は指定どおり"
);
trio
}
fn destroy(self) {
destroy_test_window(self.parent);
}
}
#[test]
fn measurement_skips_invisible_windows_and_reports_the_nearest_visible_neighbour() {
let trio = Trio::create(w!("wintf-zorder-measure-hidden"), false);
assert_eq!(
measure_window_below(trio.balloon),
Some(trio.character),
"不可視の窓は遮蔽しない——バルーンの最も近い可視の背後はキャラ窓"
);
assert_eq!(
measure_window_above(trio.character),
Some(trio.balloon),
"逆向きの実測も同じ——キャラ窓の最も近い可視の手前はバルーン窓"
);
trio.destroy();
}
#[test]
fn measurement_stops_at_a_visible_intruder() {
let trio = Trio::create(w!("wintf-zorder-measure-visible"), true);
assert_eq!(
measure_window_below(trio.balloon),
Some(trio.intruder),
"可視の窓は読み飛ばさない——返るのは割り込み窓であってキャラ窓ではない"
);
assert_ne!(
measure_window_below(trio.balloon),
Some(trio.character),
"可視の窓を挟んだ状態を隣接と判定してはならない"
);
assert_eq!(
measure_window_above(trio.character),
Some(trio.intruder),
"逆向きの実測も同じ"
);
trio.destroy();
}
fn lines_with<'a>(out: &'a str, mark: &str) -> Vec<&'a str> {
out.lines().filter(|l| l.contains(mark)).collect()
}
fn assert_line_count(out: &str, mark: &str, expected: usize, what: &str) {
let found = lines_with(out, mark);
assert_eq!(
found.len(),
expected,
"{what}: `{mark}` の行は {expected} 本のはずが {} 本だった\n---捕捉全文---\n{out}",
found.len()
);
}
#[test]
fn measurement_falls_back_to_absence_when_the_scan_hits_its_limit() {
let parent = create_visible_test_parent(w!("wintf-zorder-measure-limit"));
let balloon = create_visible_child(parent, w!("balloon"));
let character = create_visible_child(parent, w!("character"));
insert_after(character, balloon);
let mut tail = balloon;
let mut first_hidden = None;
for _ in 0..SIBLING_SCAN_LIMIT {
let hidden = create_hidden_child(parent, w!("hidden"));
insert_after(hidden, tail);
first_hidden.get_or_insert(hidden);
tail = hidden;
}
insert_after(character, tail);
let first_hidden = first_hidden.expect("上限は 1 以上のはず");
assert_eq!(
get_window_below(balloon).expect("GetWindow は成功するはず"),
Some(first_hidden),
"前提: バルーンの背後は不可視窓の列"
);
assert_eq!(
get_window_below(character).expect("GetWindow は成功するはず"),
None,
"前提: キャラ窓は列の最後尾"
);
assert_eq!(
measure_window_above(first_hidden),
Some(balloon),
"対照: 上限に達しない距離の可視窓は返る(常に不在へ倒す実装なら赤になる)"
);
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
assert_eq!(
measure_window_below(balloon),
None,
"上限に達したら不在(番兵)へ倒す——偽の隣接も偽の失敗も作らない"
);
assert_eq!(measure_window_below(HWND::default()), None);
});
assert_line_count(&out, LIMIT_MARK, 1, "打切りはちょうど 1 本記録される");
assert_line_count(&out, FAILURE_MARK, 1, "対照の走査失敗もちょうど 1 本");
assert!(
lines_with(&out, LIMIT_MARK)[0].contains(&format!("limit={SIBLING_SCAN_LIMIT}")),
"打切りの記録には上限値が載るはず\n---捕捉全文---\n{out}"
);
destroy_test_window(parent);
}
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 request_reassert(world: &mut World, balloon: Entity) {
world.entity_mut(balloon).insert(ReassertZOrder::default());
}
fn maintain_schedule() -> Schedule {
let mut schedule = Schedule::default();
schedule.set_executor(SingleThreadedExecutor::new());
schedule.add_systems(apply_zorder_pair_maintenance);
schedule
}
fn hwnd_hex(hwnd: HWND) -> String {
format!("0x{:X}", hwnd.0 as usize)
}
#[test]
fn an_invisible_window_between_the_pair_still_verifies_as_adjacent() {
let parent = create_visible_test_parent(w!("wintf-zorder-measure-maintain"));
let hidden = create_hidden_child(parent, w!("hidden"));
let character_hwnd = create_visible_child(parent, w!("character"));
let balloon_hwnd = create_visible_child(parent, w!("balloon"));
insert_after(character_hwnd, hidden);
insert_after(balloon_hwnd, character_hwnd);
assert_eq!(
get_window_above(character_hwnd).expect("GetWindow は成功するはず"),
Some(hidden),
"前提: キャラ窓の 1 つ手前は不可視の窓"
);
let control = Trio::create(w!("wintf-zorder-measure-maintain-control"), true);
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);
request_reassert(&mut world, balloon);
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);
let mut schedule = maintain_schedule();
let out = capture_under_filter(MAINTAIN_CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
SetWindowPosCommand::flush();
request_reassert(&mut world, control_balloon);
schedule.run(&mut world);
schedule.run(&mut world);
SetWindowPosCommand::flush();
});
assert_line_count(&out, FIX_TAG, 1, "是正の記録は対象のペアの 1 本だけ");
assert_line_count(
&out,
VERIFY_FAILED_TAG,
1,
"検証不一致は対照のペアの 1 本だけ",
);
let fix = lines_with(&out, FIX_TAG)[0];
assert!(
fix.contains(&format!("entity={balloon:?}")),
"是正の記録は対象のペアのバルーン窓を指すはず: {fix}"
);
assert!(
fix.contains(&format!(
"measured_next_after_fix={}",
hwnd_hex(character_hwnd)
)),
"実測は不可視窓を読み飛ばしてキャラ窓になるはず: {fix}"
);
let failed = lines_with(&out, VERIFY_FAILED_TAG)[0];
assert!(
failed.contains(&format!("entity={control_balloon:?}")),
"検証不一致の記録は対照のペアのバルーン窓を指すはず: {failed}"
);
assert_eq!(
get_window_below(balloon_hwnd).expect("GetWindow は成功するはず"),
Some(hidden),
"素の 1 歩ではバルーンの背後は不可視の窓のまま"
);
assert_eq!(
measure_window_below(balloon_hwnd),
Some(character_hwnd),
"可視の窓だけを見ればバルーンのすぐ背後はキャラ窓"
);
control.destroy();
destroy_test_window(parent);
}