use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND};
use windows::Win32::UI::WindowsAndMessaging::{DestroyWindow, IsWindow};
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::window::{
ChainPlan, ChainSegment, CrossEdge, KeepDirectlyAbove, WindowHandle, ZOrderChainPlan,
establish_owner_links,
};
#[derive(Clone, Copy)]
struct Scope {
balloon: Entity,
character: Entity,
}
fn spawn_scopes(world: &mut World, count: usize, title: PCWSTR) -> (Vec<HWND>, Vec<Scope>) {
let mut windows = Vec::with_capacity(count * 2);
let mut scopes = Vec::with_capacity(count);
for _ in 0..count {
let balloon_hwnd = create_chain_window(title);
let character_hwnd = create_chain_window(title);
let character = world
.spawn(WindowHandle {
hwnd: character_hwnd,
instance: HINSTANCE::default(),
})
.id();
let balloon = world
.spawn((
WindowHandle {
hwnd: balloon_hwnd,
instance: HINSTANCE::default(),
},
KeepDirectlyAbove { peer: character },
))
.id();
windows.push(balloon_hwnd);
windows.push(character_hwnd);
scopes.push(Scope { balloon, character });
}
(windows, scopes)
}
fn publish_scope_chain(world: &mut World, order: &[Scope]) -> Vec<Entity> {
let mut members = Vec::with_capacity(order.len() * 2);
for scope in order {
members.push(scope.balloon);
members.push(scope.character);
}
let cross_edges: Vec<CrossEdge> = order
.windows(2)
.map(|pair| CrossEdge {
owned: pair[0].character,
owner: pair[1].balloon,
segment: ChainSegment::Group(0),
})
.collect();
world.insert_resource(ZOrderChainPlan {
chain: Some(ChainPlan {
members: members.clone(),
cross_edges,
absent: Vec::new(),
}),
dirty: true,
});
members
}
fn publish_released(world: &mut World) {
world.insert_resource(ZOrderChainPlan {
chain: None,
dirty: true,
});
}
fn scope_chain_schedule() -> Schedule {
let mut schedule = Schedule::default();
schedule.set_executor(SingleThreadedExecutor::new());
schedule.add_systems((establish_owner_links, apply_zorder_chain).chain());
schedule
}
fn balloon_offsets(shape: &[usize], scope_count: usize) -> Vec<Option<isize>> {
(0..scope_count)
.map(|scope| {
let balloon = shape.iter().position(|v| *v == scope * 2);
let character = shape.iter().position(|v| *v == scope * 2 + 1);
match (balloon, character) {
(Some(b), Some(c)) => Some(c as isize - b as isize),
_ => None,
}
})
.collect()
}
fn reversed(len: usize) -> Vec<usize> {
(0..len).rev().collect()
}
fn declared(len: usize) -> Vec<usize> {
(0..len).collect()
}
fn is_window(hwnd: HWND) -> bool {
unsafe { IsWindow(Some(hwnd)) }.as_bool()
}
#[test]
fn releasing_every_group_drops_only_the_cross_links_and_reorders_nothing() {
const SCOPES: usize = 2;
let mut world = World::new();
let (set, scopes) = spawn_scopes(&mut world, SCOPES, w!("zorder-chain-lifecycle/release"));
let all_visible = set.iter().all(|h| is_visible(*h));
let mut schedule = scope_chain_schedule();
arrange_z(&set, &reversed(set.len()));
let start = z_shape(&set);
let members = publish_scope_chain(&mut world, &scopes);
schedule.run(&mut world);
let landed = z_shape(&set);
let owners_before = owner_shape(&set);
let ledger_before = ledger_shape(&mut world, &members);
let pairs_before = balloon_offsets(&landed, SCOPES);
publish_released(&mut world);
schedule.run(&mut world);
let after_release = z_shape(&set);
let owners_after = owner_shape(&set);
let ledger_after = ledger_shape(&mut world, &members);
let pairs_after = balloon_offsets(&after_release, SCOPES);
let deepest_above_first = |shape: &[usize]| match (
shape.iter().position(|v| *v == SCOPES * 2 - 1),
shape.iter().position(|v| *v == 1),
) {
(Some(deepest), Some(first)) => deepest < first,
_ => false,
};
let raise = raise_to_front_until(set[SCOPES * 2 - 1], &set, deepest_above_first);
let raised_ok = raise.command_ok;
let after_raise = raise.shape.clone();
let released_scope_moved = raise.landed;
let pairs_after_raise = balloon_offsets(&after_raise, SCOPES);
teardown(&set);
assert!(
all_visible,
"4 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert_eq!(
start,
reversed(set.len()),
"始点が宣言の逆順に揃っていない: {start:?}"
);
assert_eq!(
landed,
declared(set.len()),
"解除の前に鎖が宣言順へ収まっていない(この比較の前提): {landed:?}"
);
assert_eq!(
owners_before,
vec![Some(1), Some(2), Some(3), None],
"解除の前に鎖が一直線に張られていない(この比較の前提)"
);
assert_eq!(
ledger_before,
vec![(1, 2)],
"解除の前に本 spec の帳簿が横断 edge 1 本を持っていない(この比較の前提)"
);
assert_eq!(
owners_after,
vec![Some(1), None, Some(3), None],
"解除で消えたのが横断 edge 1 本ではない(ペア edge を外した/横断 edge が残った)"
);
assert!(
ledger_after.is_empty(),
"解除の後も本 spec の帳簿に繋ぎが残っている: {ledger_after:?}"
);
assert_eq!(
after_release, landed,
"解除しただけで重なりが動いた(束縛が消えるだけのはずである): {after_release:?}"
);
assert_eq!(
pairs_before,
vec![Some(1); SCOPES],
"解除の前にバルーンがキャラ窓の直上に居ない: {landed:?}"
);
assert_eq!(
pairs_after,
vec![Some(1); SCOPES],
"解除でバルーンがキャラ窓の直上でなくなった(要件 6.3): {after_release:?}"
);
assert!(
raised_ok,
"最奥のキャラ窓を最前面へ持ち上げる指令そのものが失敗した(自己検査が働いていない)"
);
assert!(
released_scope_moved,
"解除の後なのに最奥のスコープが手前へ出ない=束縛が消えていない(この比較は空虚である): {after_raise:?}({}・試行 {} 回)",
raise.note(),
raise.attempts
);
assert_eq!(
pairs_after_raise,
vec![Some(1); SCOPES],
"既定状態で窓を活性化するとバルーンがキャラ窓の直上でなくなった(要件 6.3): {after_raise:?}"
);
}
#[test]
fn a_late_scope_splices_into_the_middle_of_the_chain_and_leaves_it_unchanged_on_withdrawal() {
let mut world = World::new();
let (mut set, scopes) = spawn_scopes(&mut world, 2, w!("zorder-chain-lifecycle/splice"));
let all_visible = set.iter().all(|h| is_visible(*h));
let mut schedule = scope_chain_schedule();
arrange_z(&set, &reversed(set.len()));
let start = z_shape(&set);
publish_scope_chain(&mut world, &scopes);
schedule.run(&mut world);
let landed = z_shape(&set);
let pairs_landed = balloon_offsets(&landed, 2);
let (late_set, late_scopes) = spawn_scopes(&mut world, 1, w!("zorder-chain-lifecycle/late"));
set.extend_from_slice(&late_set);
let owners_before_splice = owner_shape(&set);
publish_scope_chain(&mut world, &[scopes[0], late_scopes[0], scopes[1]]);
schedule.run(&mut world);
let spliced = z_shape(&set);
let owners_spliced = owner_shape(&set);
let pairs_spliced = balloon_offsets(&spliced, 3);
let members_after = publish_scope_chain(&mut world, &scopes);
schedule.run(&mut world);
let chain_set: Vec<HWND> = set[..4].to_vec();
let withdrawn = z_shape(&chain_set);
let owners_withdrawn = owner_shape(&set);
let ledger_withdrawn = ledger_shape(&mut world, &members_after);
let pairs_withdrawn = balloon_offsets(&withdrawn, 2);
teardown(&set);
assert!(
all_visible,
"始めの 4 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert_eq!(
start,
vec![3, 2, 1, 0],
"始点が宣言の逆順に揃っていない: {start:?}"
);
assert_eq!(
landed,
vec![0, 1, 2, 3],
"差し込みの前に鎖が宣言順へ収まっていない(この比較の前提): {landed:?}"
);
assert_eq!(
owners_before_splice,
vec![Some(1), Some(2), Some(3), None, None, None],
"差し込みの前の所有関係が前提どおりでない(新しいスコープはまだ鎖にも居ない)"
);
assert_eq!(
owners_spliced,
vec![Some(1), Some(4), Some(3), None, Some(5), Some(2)],
"差し込みで所有関係が `b0←s0←b2←s2←b1←s1` の一直線にならない"
);
assert_eq!(
spliced,
vec![0, 1, 4, 5, 2, 3],
"後から現れたスコープが鎖の途中へ入っていない(要件 7.1): {spliced:?}"
);
assert_eq!(
owners_withdrawn,
vec![Some(1), Some(2), Some(3), None, Some(5), None],
"取り下げで元の鎖へ戻っていない(`s0` の owner が戻らない/ペア edge を壊した)"
);
assert_eq!(
withdrawn,
vec![0, 1, 2, 3],
"取り下げの後に残る鎖が宣言順へ戻っていない(要件 7.1): {withdrawn:?}"
);
assert_eq!(
ledger_withdrawn,
vec![(1, 2)],
"取り下げの後の帳簿が元の横断 edge 1 本だけになっていない: {ledger_withdrawn:?}"
);
assert_eq!(
pairs_landed,
vec![Some(1); 2],
"差し込みの前にバルーンがキャラ窓の直上に居ない: {landed:?}"
);
assert_eq!(
pairs_spliced,
vec![Some(1); 3],
"差し込んだ状態でバルーンがキャラ窓の直上でなくなった(要件 6.3): {spliced:?}"
);
assert_eq!(
pairs_withdrawn,
vec![Some(1); 2],
"取り下げの後にバルーンがキャラ窓の直上でなくなった(要件 6.3): {withdrawn:?}"
);
}
#[test]
fn destroying_a_departing_scope_leaves_the_other_scopes_alive() {
const SCOPES: usize = 3;
let mut world = World::new();
let (set, scopes) = spawn_scopes(&mut world, SCOPES, w!("zorder-chain-lifecycle/destroy"));
let all_visible = set.iter().all(|h| is_visible(*h));
let mut schedule = scope_chain_schedule();
arrange_z(&set, &reversed(set.len()));
let start = z_shape(&set);
publish_scope_chain(&mut world, &scopes);
schedule.run(&mut world);
let landed = z_shape(&set);
let owners_before = owner_shape(&set);
let pairs_landed = balloon_offsets(&landed, SCOPES);
world.despawn(scopes[SCOPES - 1].balloon);
world.despawn(scopes[SCOPES - 1].character);
schedule.run(&mut world);
let owners_after_detach = owner_shape(&set);
let departing: Vec<HWND> = set[(SCOPES - 1) * 2..].to_vec();
let all_windows_before_destroy = set.iter().all(|h| is_window(*h));
let destroyed_ok = departing
.iter()
.all(|hwnd| unsafe { DestroyWindow(*hwnd) }.is_ok());
let departing_gone = departing.iter().all(|h| !is_window(*h));
let survivors: Vec<HWND> = set[..(SCOPES - 1) * 2].to_vec();
let survivors_alive = survivors.iter().all(|h| is_window(*h));
let survivors_visible = survivors.iter().all(|h| is_visible(*h));
let survivor_shape = z_shape(&survivors);
let survivor_owners = owner_shape(&survivors);
let pairs_survivors = balloon_offsets(&survivor_shape, SCOPES - 1);
teardown(&set);
assert!(
all_visible,
"6 枚が表示状態になっていない(不可視の窓では要件 9.3 の実測経路が働かない)"
);
assert_eq!(
start,
reversed(set.len()),
"始点が宣言の逆順に揃っていない: {start:?}"
);
assert_eq!(
landed,
declared(set.len()),
"破棄の前に鎖が宣言順へ収まっていない(この比較の前提): {landed:?}"
);
assert_eq!(
owners_before,
vec![Some(1), Some(2), Some(3), Some(4), Some(5), None],
"破棄の前に 3 スコープが一直線の鎖になっていない(この比較の前提)"
);
assert_eq!(
pairs_landed,
vec![Some(1); SCOPES],
"破棄の前にバルーンがキャラ窓の直上に居ない: {landed:?}"
);
assert_eq!(
owners_after_detach,
vec![Some(1), Some(2), Some(3), None, Some(5), None],
"去るスコープへの繋ぎが破棄より先に外れていない=この後の破棄は道連れを起こす"
);
assert!(
all_windows_before_destroy,
"破棄の前に 6 枚すべてが窓ではない(この比較の前提)"
);
assert!(
destroyed_ok,
"去るスコープの窓を壊す指令そのものが失敗した(刺激が届いていない)"
);
assert!(
departing_gone,
"去るスコープの窓が破棄されていない(刺激が届いておらず、以下の比較は空虚である)"
);
assert!(
survivors_alive,
"鎖の窓を壊したら他スコープの窓まで破棄された(要件 7.2)"
);
assert!(
survivors_visible,
"生き残った窓が表示状態を失った(要件 11.5・見える変化は重なり順に限る)"
);
assert_eq!(
survivor_shape,
declared(survivors.len()),
"生き残った窓の相対順が指定どおりでない(要件 7.2): {survivor_shape:?}"
);
assert_eq!(
survivor_owners,
vec![Some(1), Some(2), Some(3), None],
"生き残った鎖の所有関係が一直線でない(切離しが余分な繋ぎまで外した)"
);
assert_eq!(
pairs_survivors,
vec![Some(1); SCOPES - 1],
"破棄の後にバルーンがキャラ窓の直上でなくなった(要件 6.3): {survivor_shape:?}"
);
}