use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND};
use windows::core::{PCWSTR, w};
use super::apply_zorder_chain;
use super::zorder_chain_order_tests::{
arrange_z, create_chain_window, is_visible, ledger_shape, owner_shape, raise_to_front_until,
teardown, z_shape,
};
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::{ChainPlan, ChainSegment, CrossEdge, WindowHandle, ZOrderChainPlan};
const SIGNOFF_DIRECTIVES: &str = "info,wintf::ecs::window::zorder_chain=debug";
const ZORDER_TAG: &str = "[zorder-";
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 bare_fixture(count: usize, title: PCWSTR) -> (Vec<HWND>, World, Vec<Entity>) {
let set: Vec<HWND> = (0..count).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();
(set, world, members)
}
fn zorder_lines(out: &str) -> Vec<&str> {
out.lines().filter(|l| l.contains(ZORDER_TAG)).collect()
}
const CHAIN: usize = 3;
const FRONT_OUTSIDER: usize = CHAIN;
const BACK_OUTSIDER: usize = CHAIN + 1;
#[test]
fn outsiders_keep_their_relative_order_across_the_nudge() {
let (set, mut world, members) = bare_fixture(CHAIN, w!("zorder-chain-outsider/nudge"));
let front = create_chain_window(w!("zorder-chain-outsider/front"));
let back = create_chain_window(w!("zorder-chain-outsider/back"));
let all: Vec<HWND> = set.iter().copied().chain([front, back]).collect();
let all_visible = all.iter().all(|h| is_visible(*h));
let seed = [FRONT_OUTSIDER, 2, 1, 0, BACK_OUTSIDER];
arrange_z(&all, &seed);
let start = z_shape(&all);
let chain_before = z_shape(&set);
let outsiders_before = outsider_gap(&start);
publish_full_chain(&mut world, &members);
chain_schedule().run(&mut world);
let after = z_shape(&all);
let chain_after = z_shape(&set);
let outsiders_after = outsider_gap(&after);
let owners = owner_shape(&set);
let ledger = ledger_shape(&mut world, &members);
teardown(&all);
assert!(
all_visible,
"5 枚が表示状態になっていない(不可視の窓では後押しの実測経路が働かない)"
);
assert_eq!(
start,
seed.to_vec(),
"助走が §12.2 の逐語値 [3,2,1,0,4] に揃っていない: {start:?}"
);
assert_eq!(
chain_before,
vec![2, 1, 0],
"助走の時点で鎖が宣言の逆順に居ない: {chain_before:?}"
);
assert_eq!(
chain_after,
vec![0, 1, 2],
"1 巡のあと鎖が宣言順へ収まっていない=後押しが届いておらず、部外者の比較は空虚である: {chain_after:?}"
);
assert_eq!(
owners,
vec![Some(1), Some(2), None],
"本番の適用系が張った所有関係が一直線の鎖になっていない(末尾が根)"
);
assert_eq!(
ledger,
vec![(0, 1), (1, 2)],
"本 spec が張った繋ぎの帳簿が鎖の連続対と一致しない"
);
assert_eq!(
outsiders_before,
Some(true),
"助走の時点で前の部外者が後ろの部外者より手前に居ない: {start:?}"
);
assert_eq!(
outsiders_after,
Some(true),
"後押しのあと部外者どうしの前後が入れ替わった(`research.md` §12.2/§12.5 の実測が変わった): {after:?}"
);
}
fn outsider_gap(shape: &[usize]) -> Option<bool> {
let front = shape.iter().position(|v| *v == FRONT_OUTSIDER)?;
let back = shape.iter().position(|v| *v == BACK_OUTSIDER)?;
Some(front < back)
}
const DEFAULT_SIZE: usize = 4;
#[test]
fn the_default_state_issues_nothing_and_pins_no_pair_of_windows() {
let (set, mut world, members) = bare_fixture(DEFAULT_SIZE, w!("zorder-chain-outsider/default"));
let all_visible = set.iter().all(|h| is_visible(*h));
let mut schedule = chain_schedule();
let seed: Vec<usize> = (0..DEFAULT_SIZE).collect();
arrange_z(&set, &seed);
let start = z_shape(&set);
let quiet = capture_under_filter(SIGNOFF_DIRECTIVES, || {
schedule.run(&mut world);
});
let after_idle = z_shape(&set);
let owners_idle = owner_shape(&set);
let ledger_idle = ledger_shape(&mut world, &members);
let raise = raise_to_front_until(set[DEFAULT_SIZE - 1], &set, |shape| {
shape.first().copied() == Some(DEFAULT_SIZE - 1)
});
let raised_ok = raise.command_ok;
let after_raise = raise.shape.clone();
let quiet_after_raise = capture_under_filter(SIGNOFF_DIRECTIVES, || {
schedule.run(&mut world);
});
let after_second_idle = z_shape(&set);
let owners_after_raise = owner_shape(&set);
publish_full_chain(&mut world, &members);
let control = capture_under_filter(SIGNOFF_DIRECTIVES, || {
schedule.run(&mut world);
});
let after_control = z_shape(&set);
let owners_control = owner_shape(&set);
let ledger_control = ledger_shape(&mut world, &members);
teardown(&set);
assert!(
all_visible,
"4 枚が表示状態になっていない(不可視の窓では後押しの実測経路が働かない)"
);
assert_eq!(start, seed, "助走が揃っていない: {start:?}");
assert_eq!(
owners_control,
vec![Some(1), Some(2), Some(3), None],
"対照の巡で所有の鎖が立たなかった=この檻は機構へ届いていない(既定状態の主張は空虚)"
);
assert_eq!(
ledger_control,
vec![(0, 1), (1, 2), (2, 3)],
"対照の巡で帳簿に繋ぎが載らなかった=この檻は機構へ届いていない"
);
assert_eq!(
after_control, seed,
"対照の巡で重なりが宣言順へ動かなかった=この檻は実窓へ届いていない: {after_control:?}"
);
assert!(
!zorder_lines(&control).is_empty(),
"対照の巡で重なり順系の記録が 1 行も出なかった=捕捉窓そのものが働いていない"
);
assert_eq!(
owners_idle,
vec![None; DEFAULT_SIZE],
"既定状態で所有関係が立った(要件 6.1/6.4)"
);
assert!(
ledger_idle.is_empty(),
"既定状態で帳簿に繋ぎが載った(要件 6.1/6.4): {ledger_idle:?}"
);
assert_eq!(
after_idle, seed,
"既定状態の 1 巡で重なりが動いた(要件 6.4): {after_idle:?}"
);
assert!(
zorder_lines(&quiet).is_empty(),
"既定状態で重なり順系の記録が出た(要件 6.1/6.4): {:?}",
zorder_lines(&quiet)
);
assert!(
raised_ok,
"最奥の窓を最前面へ持ち上げる指令そのものが失敗した(刺激が起きていない)"
);
let expected_after_raise: Vec<usize> = std::iter::once(DEFAULT_SIZE - 1)
.chain(0..DEFAULT_SIZE - 1)
.collect();
assert_eq!(
after_raise,
expected_after_raise,
"活性化そのものが重なりを動かしていない=以下の比較は空虚である: {after_raise:?}({}・試行 {} 回)",
raise.note(),
raise.attempts
);
assert_eq!(
after_second_idle, expected_after_raise,
"既定状態なのに活性化した窓が押し戻された(固定の規則で前後を決めている・要件 6.1): {after_second_idle:?}"
);
assert_eq!(
owners_after_raise,
vec![None; DEFAULT_SIZE],
"既定状態の活性化に応じて所有関係が立った(要件 6.1/6.2)"
);
assert!(
zorder_lines(&quiet_after_raise).is_empty(),
"既定状態の活性化に応じて重なり順系の記録が出た(要件 6.2): {:?}",
zorder_lines(&quiet_after_raise)
);
let others_before: Vec<usize> = after_idle
.iter()
.copied()
.filter(|v| *v != DEFAULT_SIZE - 1)
.collect();
let others_after: Vec<usize> = after_second_idle
.iter()
.copied()
.filter(|v| *v != DEFAULT_SIZE - 1)
.collect();
assert_eq!(
others_before, others_after,
"1 枚の活性化で他の窓どうしの相対順が変わった(要件 6.2): {others_before:?} → {others_after:?}"
);
}