use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM};
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DestroyWindow, EnumThreadWindows, GW_HWNDNEXT, GW_OWNER, GetTopWindow,
GetWindow, HWND_TOP, IsWindowVisible, SW_SHOWNOACTIVATE, SWP_NOACTIVATE, SWP_NOMOVE,
SWP_NOSIZE, SetWindowPos, ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WS_EX_TOOLWINDOW,
WS_POPUP,
};
use windows::core::BOOL;
use windows::core::{PCWSTR, w};
use super::apply_zorder_chain;
use crate::api::clear_window_owner;
use crate::ecs::window::zorder_pair::measure_windows_in_front;
use crate::ecs::window::{
ChainPlan, ChainSegment, CrossEdge, CrossOwnerLink, WindowHandle, ZOrderChainPlan,
};
const CHAIN_SIZE: usize = 4;
const DECLARED: [usize; CHAIN_SIZE] = [0, 1, 2, 3];
const REVERSED: [usize; CHAIN_SIZE] = [3, 2, 1, 0];
pub(super) fn create_chain_window(title: PCWSTR) -> HWND {
ensure_ime_anchor();
create_raw_window(title)
}
thread_local! {
static IME_ANCHOR_MADE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
fn ensure_ime_anchor() {
IME_ANCHOR_MADE.with(|made| {
if made.get() {
return;
}
made.set(true);
let _ = create_raw_window(w!("zorder-chain-order/ime-anchor"));
});
}
fn create_raw_window(title: PCWSTR) -> HWND {
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
unsafe {
let hinstance = GetModuleHandleW(None).expect("GetModuleHandleW");
let hwnd = CreateWindowExW(
WINDOW_EX_STYLE(WS_EX_TOOLWINDOW.0),
w!("Static"),
title,
WINDOW_STYLE(WS_POPUP.0),
10,
10,
0,
0,
None,
None,
Some(hinstance.into()),
None,
)
.expect("CreateWindowExW should create a test window");
let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
hwnd
}
}
fn owns_any_thread_window(hwnd: HWND) -> bool {
struct Probe {
target: HWND,
found: bool,
}
unsafe extern "system" fn visit(candidate: HWND, lparam: LPARAM) -> BOOL {
let probe = unsafe { &mut *(lparam.0 as *mut Probe) };
let owner = unsafe { GetWindow(candidate, GW_OWNER) }.ok();
if owner == Some(probe.target) {
probe.found = true;
return BOOL(0); }
BOOL(1)
}
let mut probe = Probe {
target: hwnd,
found: false,
};
unsafe {
let _ = EnumThreadWindows(
GetCurrentThreadId(),
Some(visit),
LPARAM(&raw mut probe as isize),
);
}
probe.found
}
pub(super) fn head_owns_nothing(set: &[HWND]) -> bool {
set.first()
.is_some_and(|head| !owns_any_thread_window(*head))
}
pub(super) fn is_visible(hwnd: HWND) -> bool {
unsafe { IsWindowVisible(hwnd) }.as_bool()
}
fn owner_of(hwnd: HWND) -> Option<HWND> {
unsafe { GetWindow(hwnd, GW_OWNER) }.ok()
}
fn unlink_all(windows: &[HWND]) {
for hwnd in windows {
if owner_of(*hwnd).is_none() {
continue;
}
if let Err(err) = clear_window_owner(*hwnd) {
eprintln!(
"[zorder-chain-order] 後始末の切離しに失敗しました hwnd={hwnd:?} error={err}"
);
}
}
}
fn destroy_all(windows: &[HWND]) {
for hwnd in windows {
unsafe {
let _ = DestroyWindow(*hwnd);
}
}
}
pub(super) fn teardown(windows: &[HWND]) {
unlink_all(windows);
destroy_all(windows);
}
pub(super) const Z_SCAN_ATTEMPTS: usize = 8;
fn relative_z_order(windows: &[HWND]) -> Vec<HWND> {
let mut best: Vec<HWND> = Vec::new();
for _ in 0..Z_SCAN_ATTEMPTS {
let seen = scan_z_once(windows);
if seen.len() == windows.len() {
return seen;
}
if seen.len() > best.len() {
best = seen;
}
}
best
}
fn scan_z_once(windows: &[HWND]) -> Vec<HWND> {
let mut result = Vec::new();
let mut cursor = unsafe { GetTopWindow(None) }.ok();
let mut steps = 0usize;
while let Some(hwnd) = cursor {
if hwnd.is_invalid() {
break;
}
if windows.contains(&hwnd) && !result.contains(&hwnd) {
result.push(hwnd);
if result.len() == windows.len() {
break;
}
}
steps += 1;
if steps > 100_000 {
break;
}
cursor = unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok();
}
result
}
pub(super) fn z_shape(set: &[HWND]) -> Vec<usize> {
relative_z_order(set)
.iter()
.filter_map(|hwnd| set.iter().position(|w| w == hwnd))
.collect()
}
fn place_after(hwnd: HWND, after: HWND) {
unsafe {
let _ = SetWindowPos(
hwnd,
Some(after),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
);
}
}
pub(super) fn arrange_z(set: &[HWND], order: &[usize]) {
for pair in order.windows(2) {
place_after(set[pair[1]], set[pair[0]]);
}
}
pub(super) const Z_RAISE_ATTEMPTS: usize = 8;
pub(super) struct RaiseTrace {
pub(super) landed: bool,
pub(super) command_ok: bool,
pub(super) attempts: usize,
pub(super) misses: Vec<Vec<usize>>,
pub(super) shape: Vec<usize>,
}
impl RaiseTrace {
pub(super) fn note(&self) -> String {
if self.misses.is_empty() {
"持ち上げは 1 回目で届いている".to_string()
} else {
format!(
"持ち上げが {} 回届かなかった(Win32 が成功を返しながら並びを 1 つも変えない事象・届かなかった巡の並び: {:?})",
self.misses.len(),
self.misses
)
}
}
}
pub(super) fn ensure_front_until(
target: HWND,
witness: &[HWND],
landed: impl Fn(&[usize]) -> bool,
) -> RaiseTrace {
let shape = z_shape(witness);
if landed(&shape) {
return RaiseTrace {
landed: true,
command_ok: true,
attempts: 0,
misses: Vec::new(),
shape,
};
}
raise_to_front_until(target, witness, landed)
}
pub(super) fn raise_to_front_until(
target: HWND,
witness: &[HWND],
landed: impl Fn(&[usize]) -> bool,
) -> RaiseTrace {
let mut misses = Vec::new();
let mut command_ok = false;
for attempt in 1..=Z_RAISE_ATTEMPTS {
command_ok = unsafe {
SetWindowPos(
target,
Some(HWND_TOP),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
)
}
.is_ok();
let shape = z_shape(witness);
if landed(&shape) {
return RaiseTrace {
landed: true,
command_ok,
attempts: attempt,
misses,
shape,
};
}
misses.push(shape);
std::thread::sleep(std::time::Duration::from_millis(2));
}
RaiseTrace {
landed: false,
command_ok,
attempts: Z_RAISE_ATTEMPTS,
misses,
shape: z_shape(witness),
}
}
fn chain_schedule() -> Schedule {
let mut schedule = Schedule::default();
schedule.set_executor(SingleThreadedExecutor::new());
schedule.add_systems(apply_zorder_chain);
schedule
}
fn spawn_window(world: &mut World, hwnd: HWND) -> Entity {
world
.spawn(WindowHandle {
hwnd,
instance: HINSTANCE::default(),
})
.id()
}
fn publish_full_chain(world: &mut World, members: &[Entity]) {
let cross_edges: Vec<CrossEdge> = members
.windows(2)
.map(|pair| CrossEdge {
owned: pair[0],
owner: pair[1],
segment: ChainSegment::Group(0),
})
.collect();
world.insert_resource(ZOrderChainPlan {
chain: Some(ChainPlan {
members: members.to_vec(),
cross_edges,
absent: Vec::new(),
}),
dirty: true,
});
}
fn chain_fixture(title: PCWSTR) -> (Vec<HWND>, World, Vec<Entity>) {
let set: Vec<HWND> = (0..CHAIN_SIZE)
.map(|_| create_chain_window(title))
.collect();
let mut world = World::new();
let members: Vec<Entity> = set.iter().map(|h| spawn_window(&mut world, *h)).collect();
publish_full_chain(&mut world, &members);
(set, world, members)
}
pub(super) fn ledger_shape(world: &mut World, members: &[Entity]) -> Vec<(usize, usize)> {
let mut found: Vec<(usize, usize)> = world
.query::<(Entity, &CrossOwnerLink)>()
.iter(world)
.filter_map(|(owned, link)| {
let owned_at = members.iter().position(|m| *m == owned)?;
let owner_at = members.iter().position(|m| *m == link.owner)?;
Some((owned_at, owner_at))
})
.collect();
found.sort_unstable();
found
}
pub(super) fn owner_shape(set: &[HWND]) -> Vec<Option<usize>> {
set.iter()
.map(|h| owner_of(*h).and_then(|o| set.iter().position(|w| *w == o)))
.collect()
}
#[test]
fn a_declared_chain_lands_in_the_declared_order_on_real_windows() {
let (set, mut world, members) = chain_fixture(w!("zorder-chain-order/lands"));
let all_visible = set.iter().all(|h| is_visible(*h));
arrange_z(&set, &REVERSED);
let start = z_shape(&set);
let head_bare = head_owns_nothing(&set);
chain_schedule().run(&mut world);
let landed = z_shape(&set);
let owners = owner_shape(&set);
let ledger = ledger_shape(&mut world, &members);
let production_scan: Vec<usize> = measure_windows_in_front(set[CHAIN_SIZE - 1])
.windows
.iter()
.filter_map(|found| set.iter().position(|w| w == found))
.collect();
teardown(&set);
assert!(
all_visible,
"4 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert_eq!(
start,
REVERSED.to_vec(),
"始点が宣言の逆順に揃っていない: {start:?}"
);
assert!(
head_bare,
"鎖の先頭が窓を所有している=受け皿が効かず、本番より易しい配置で測っている"
);
assert_eq!(
owners,
vec![Some(1), Some(2), Some(3), None],
"本番の適用系が張った所有関係が一直線の鎖になっていない(末尾が根)"
);
assert_eq!(
ledger,
vec![(0, 1), (1, 2), (2, 3)],
"本 spec が張った繋ぎの帳簿が鎖の連続対と一致しない"
);
assert_eq!(
landed,
DECLARED.to_vec(),
"本番の適用系を 1 巡回しても宣言順の重なりに着かない(要件 1.1/1.2/14.1)"
);
assert_eq!(
production_scan,
vec![2, 1, 0],
"本番の実測経路が鎖の窓を宣言どおりに拾わない(要件 9.3・不可視の窓は列に入らない)"
);
}
#[test]
fn a_chain_whose_root_already_sits_behind_its_anchor_still_lands() {
const ROOT_ADJACENT_SEED: [usize; CHAIN_SIZE] = [1, 0, 2, 3];
let (set, mut world, _members) = chain_fixture(w!("zorder-chain-order/root-adjacent"));
let all_visible = set.iter().all(|h| is_visible(*h));
arrange_z(&set, &ROOT_ADJACENT_SEED);
let start = z_shape(&set);
let head_bare = head_owns_nothing(&set);
chain_schedule().run(&mut world);
let landed = z_shape(&set);
teardown(&set);
assert!(
all_visible,
"4 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert_eq!(
start,
ROOT_ADJACENT_SEED.to_vec(),
"始点が組めていない: {start:?}"
);
assert!(
head_bare,
"鎖の先頭が窓を所有している=受け皿が効かず、本番より易しい配置で測っている"
);
assert_eq!(
landed,
DECLARED.to_vec(),
"素直な後押しが空振りする配置で鎖が宣言順へ着かない(要件 1.1/1.2)"
);
}
#[test]
fn a_settled_chain_holds_its_order_when_the_deepest_window_is_raised_to_the_front() {
let (set, mut world, _members) = chain_fixture(w!("zorder-chain-order/disturb"));
let all_visible = set.iter().all(|h| is_visible(*h));
arrange_z(&set, &REVERSED);
let head_bare = head_owns_nothing(&set);
chain_schedule().run(&mut world);
let settled = z_shape(&set);
let control = create_chain_window(w!("zorder-chain-order/disturb-control"));
let with_control: Vec<HWND> = set.iter().copied().chain([control]).collect();
let control_at = |shape: &[usize]| shape.iter().position(|i| *i == CHAIN_SIZE);
let seat_control =
ensure_front_until(control, &with_control, |shape| control_at(shape) == Some(0));
let before_shape = seat_control.shape.clone();
let control_before = control_at(&before_shape);
let raise_root = raise_to_front_until(set[CHAIN_SIZE - 1], &with_control, |shape| {
control_at(shape) == Some(CHAIN_SIZE)
});
let raise_root_ok = raise_root.command_ok;
let after_disturb = z_shape(&set);
let after_shape = raise_root.shape.clone();
let control_after = control_at(&after_shape);
let raise_head_ok = unsafe {
SetWindowPos(
set[0],
Some(HWND_TOP),
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
)
}
.is_ok();
let after_second_disturb = z_shape(&set);
teardown(&set);
destroy_all(&[control]);
assert!(
all_visible,
"4 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert!(
head_bare,
"鎖の先頭が窓を所有している=受け皿が効かず、本番より易しい配置で測っている"
);
assert_eq!(
settled,
DECLARED.to_vec(),
"攪乱の前に鎖が宣言順へ収まっていない(この比較の前提): {settled:?}"
);
assert!(
raise_root_ok,
"最も奥の窓を最前面へ持ち上げる指令そのものが失敗した(攪乱が起きていない)"
);
assert!(
raise_head_ok,
"先頭の窓を最前面へ持ち上げる指令そのものが失敗した(2 度目の攪乱が起きていない)"
);
assert_eq!(
control_before,
Some(0),
"攪乱の前に検体窓が鎖より手前に居ない(この位置から始めないと刺激の有無が見えない): {before_shape:?}({}・試行 {} 回)",
seat_control.note(),
seat_control.attempts
);
assert_eq!(
control_after,
Some(CHAIN_SIZE),
"攪乱で鎖が検体窓より手前へ出ていない=刺激が届いておらず、以下の比較は空虚である: {after_shape:?}({}・試行 {} 回)",
raise_root.note(),
raise_root.attempts
);
assert_eq!(
after_disturb,
DECLARED.to_vec(),
"最も奥の窓を最前面へ持ち上げると鎖の順が崩れた(要件 1.3/14.3・是正の巡は回していない)"
);
assert_eq!(
after_second_disturb,
DECLARED.to_vec(),
"先頭の窓を最前面へ持ち上げると鎖の順が崩れた(要件 1.3/14.3)"
);
}
#[test]
fn a_chain_holds_its_relative_order_when_every_window_falls_behind_an_outsider() {
let (set, mut world, _members) = chain_fixture(w!("zorder-chain-order/behind"));
let all_visible = set.iter().all(|h| is_visible(*h));
arrange_z(&set, &REVERSED);
let head_bare = head_owns_nothing(&set);
chain_schedule().run(&mut world);
let settled = z_shape(&set);
let outsider = create_chain_window(w!("zorder-chain-order/outsider"));
place_after(outsider, set[CHAIN_SIZE - 1]);
let with_outsider: Vec<HWND> = set.iter().copied().chain([outsider]).collect();
let outsider_at = |shape: &[usize]| shape.iter().position(|i| *i == CHAIN_SIZE);
let before_shape = z_shape(&with_outsider);
let outsider_before = outsider_at(&before_shape);
place_after(set[0], outsider);
let after_behind = z_shape(&set);
let after_shape = z_shape(&with_outsider);
let outsider_after = outsider_at(&after_shape);
teardown(&set);
destroy_all(&[outsider]);
assert!(
all_visible,
"4 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert!(
head_bare,
"鎖の先頭が窓を所有している=受け皿が効かず、本番より易しい配置で測っている"
);
assert_eq!(
settled,
DECLARED.to_vec(),
"背面へ回す前に鎖が宣言順へ収まっていない(この比較の前提): {settled:?}"
);
assert_eq!(
outsider_before,
Some(CHAIN_SIZE),
"始点で検体窓が全窓より奥に居ない: {before_shape:?}"
);
assert_eq!(
outsider_after,
Some(0),
"ゴーストの全窓が検体窓の背面へ回っていない(この状況で測らなければ要件 7.5 の検査にならない): {after_shape:?}"
);
assert_eq!(
after_behind,
DECLARED.to_vec(),
"全窓が鎖の外の窓の背面へ回ると鎖の中の相対順が崩れた(要件 7.5)"
);
}