use bevy_ecs::prelude::*;
use bevy_ecs::schedule::{Schedule, SingleThreadedExecutor};
use windows::Win32::Foundation::{HINSTANCE, HWND};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DestroyWindow, HWND_TOP, HWND_TOPMOST, SET_WINDOW_POS_FLAGS, SW_SHOWNA,
SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SetWindowPos, ShowWindow,
WINDOW_EX_STYLE, WINDOW_STYLE, WS_CHILD, WS_EX_TOOLWINDOW, WS_POPUP, WS_VISIBLE,
};
use windows::core::{PCWSTR, w};
use super::{IssuedPairFix, apply_zorder_pair_maintenance, pair_fix_command, plan_pair_fix};
use crate::api::{get_window_above, get_window_below};
use crate::ecs::test_support::capture_under_filter;
use crate::ecs::window::zorder_pair::{
ExpectedOrder, InsertSpec, PairFixDecision, PairObservation, PairTrigger, SkipReason,
measure_window_below,
};
use crate::ecs::window::{
KeepDirectlyAbove, ReassertZOrder, SetWindowPosCommand, WindowHandle, ZOrderPairStrategy,
};
const CAPTURE_DIRECTIVES: &str =
"info,wintf::ecs::window::zorder_pair=debug,wintf::ecs::window::command=debug";
const FIX_TAG: &str = "[zorder-pair] fix";
const SKIP_TAG: &str = "[zorder-pair] skip";
const VERIFY_FAILED_TAG: &str = "[zorder-pair] verify-failed";
const SET_WINDOW_POS_TAG: &str = "[guarded_set_window_pos] Calling SetWindowPos";
const ORPHAN_REQUEST_MARK: &str = "ペア宣言の無い窓に再断行の要求";
const UNBACKED_VERIFY_MARK: &str = "検証待ちの要求に、出した指令の持ち越しがありません";
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 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 hwnd_hex(hwnd: HWND) -> String {
format!("0x{:X}", hwnd.0 as usize)
}
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 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 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 Trio {
parent: HWND,
character: HWND,
intruder: HWND,
balloon: HWND,
}
impl Trio {
fn create(tag: PCWSTR) -> Self {
let parent = create_test_parent(tag);
let character = create_test_child(parent, w!("character"));
let intruder = create_test_child(parent, w!("intruder"));
let balloon = create_test_child(parent, w!("balloon"));
insert_after(intruder, balloon);
insert_after(character, intruder);
let trio = Self {
parent,
character,
intruder,
balloon,
};
assert_eq!(
get_window_below(trio.balloon).expect("GetWindow は成功するはず"),
Some(trio.intruder),
"前提: バルーンの 1 つ背後は割り込み窓(=キャラの隣に居ない)"
);
trio
}
fn destroy(self) {
destroy_test_window(self.parent);
}
}
#[test]
fn reassert_request_is_applied_verified_and_then_disappears() {
let trio = Trio::create(w!("wintf-zorder-maintain-lifecycle"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, trio.character);
let balloon = spawn_window(&mut world, trio.balloon);
declare_pair(&mut world, balloon, character);
request_reassert(&mut world, balloon);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
assert_eq!(
world
.entity(balloon)
.get::<ReassertZOrder>()
.map(|r| r.pending_verify),
Some(Some(ExpectedOrder {
above: trio.balloon,
below: trio.character,
})),
"適用した巡では要求が検証待ちの段階へ進み、期待隣接を持つはず"
);
assert!(
world.entity(balloon).contains::<IssuedPairFix>(),
"次巡の記録に載せる「出した指令」が持ち越されているはず"
);
SetWindowPosCommand::flush();
schedule.run(&mut world);
schedule.run(&mut world);
});
assert_line_count(&out, SET_WINDOW_POS_TAG, 1, "発行された指令は 1 本だけ");
assert_line_count(&out, FIX_TAG, 1, "是正の記録は検証の巡に 1 本だけ");
assert_line_count(&out, VERIFY_FAILED_TAG, 0, "期待どおりなので不一致は無い");
assert_line_count(&out, SKIP_TAG, 0, "見送っていない");
let fix = lines_with(&out, FIX_TAG)[0];
assert!(
fix.contains(&format!("insert_after={}", hwnd_hex(trio.intruder))),
"出した指令の挿入位置はキャラの 1 つ手前だった窓のはず: {fix}"
);
assert!(
fix.contains(&format!(
"measured_next_after_fix={}",
hwnd_hex(trio.character)
)),
"適用後の実測はキャラ窓のはず: {fix}"
);
assert_eq!(
get_window_below(trio.balloon).expect("GetWindow は成功するはず"),
Some(trio.character),
"OS 側でもバルーンの 1 つ背後がキャラ窓になっているはず"
);
assert_eq!(
get_window_above(trio.character).expect("GetWindow は成功するはず"),
Some(trio.balloon),
"逆向きの実測でも隣接しているはず"
);
assert!(
!world.entity(balloon).contains::<ReassertZOrder>(),
"検証を終えた要求は消えているはず"
);
assert!(
!world.entity(balloon).contains::<IssuedPairFix>(),
"指令の持ち越しも要求とともに消えるはず"
);
trio.destroy();
}
#[test]
fn already_adjacent_pair_issues_no_command_at_all() {
let parent = create_test_parent(w!("wintf-zorder-maintain-adjacent"));
let character_hwnd = create_test_child(parent, w!("character"));
let balloon_hwnd = create_test_child(parent, w!("balloon"));
insert_after(character_hwnd, balloon_hwnd);
assert_eq!(
get_window_below(balloon_hwnd).expect("GetWindow は成功するはず"),
Some(character_hwnd),
"前提: バルーンの 1 つ背後はキャラ窓(=既に隣接)"
);
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 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,
SET_WINDOW_POS_TAG,
1,
"是正の指令は出ず、対照の 1 本だけが出るはず",
);
assert_line_count(&out, SKIP_TAG, 1, "見送りは理由つきで 1 本だけ");
assert_line_count(&out, FIX_TAG, 0, "是正していないので是正の記録は出ない");
assert_line_count(&out, VERIFY_FAILED_TAG, 0, "検証段階へ進んでいない");
let skip = lines_with(&out, SKIP_TAG)[0];
assert!(
skip.contains("reason=AlreadyAdjacent"),
"見送りの理由は「既に隣接している」であるはず: {skip}"
);
assert!(
!world.entity(balloon).contains::<ReassertZOrder>(),
"一回限りの要求は見送りとともに消えるはず(毎巡の再評価を残さない)"
);
assert!(
!world.entity(balloon).contains::<IssuedPairFix>(),
"指令を出していないので持ち越しも作らない"
);
assert_eq!(
get_window_below(balloon_hwnd).expect("GetWindow は成功するはず"),
Some(character_hwnd),
"重なりは動いていないはず"
);
destroy_test_window(parent);
}
#[test]
fn without_a_request_the_maintenance_does_nothing() {
let trio = Trio::create(w!("wintf-zorder-maintain-no-trigger"));
let control_parent = create_test_parent(w!("wintf-zorder-maintain-no-trigger-control"));
let control_character = create_test_child(control_parent, w!("character"));
let control_balloon = create_test_child(control_parent, w!("balloon"));
insert_after(control_character, control_balloon);
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, trio.character);
let balloon = spawn_window(&mut world, trio.balloon);
declare_pair(&mut world, balloon, character);
let other_character = spawn_window(&mut world, control_character);
let other_balloon = spawn_window(&mut world, control_balloon);
declare_pair(&mut world, other_balloon, other_character);
request_reassert(&mut world, other_balloon);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
schedule.run(&mut world);
schedule.run(&mut world);
SetWindowPosCommand::flush();
issue_control_command(trio.balloon);
});
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"要求が無ければ指令は出ず、対照の 1 本だけが出るはず",
);
assert_line_count(
&out,
SKIP_TAG,
1,
"記録が出るのは要求を持つ対照のペアだけ(要求の無いペアは判断そのものを行わない)",
);
let skip = lines_with(&out, SKIP_TAG)[0];
assert!(
skip.contains(&format!("entity={other_balloon:?}")),
"見送りの記録は対照のペアを指すはず: {skip}"
);
assert_line_count(&out, FIX_TAG, 0, "是正しない");
assert_eq!(
get_window_below(trio.balloon).expect("GetWindow は成功するはず"),
Some(trio.intruder),
"重なりは動いていないはず"
);
destroy_test_window(control_parent);
trio.destroy();
}
#[test]
fn verification_mismatch_is_recorded_once_without_retry() {
let trio = Trio::create(w!("wintf-zorder-maintain-mismatch"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, trio.character);
let balloon = spawn_window(&mut world, trio.balloon);
declare_pair(&mut world, balloon, character);
request_reassert(&mut world, balloon);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
schedule.run(&mut world);
schedule.run(&mut world);
schedule.run(&mut world);
SetWindowPosCommand::flush();
});
assert_line_count(
&out,
VERIFY_FAILED_TAG,
1,
"検証不一致の記録は 1 本だけ(再試行しない)",
);
assert_line_count(&out, FIX_TAG, 0, "一致していないので是正の記録は出ない");
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"発行された指令は最初の 1 本だけ(不一致が新たな指令を生まない)",
);
let failed = lines_with(&out, VERIFY_FAILED_TAG)[0];
assert!(
failed.contains(&format!("expected_above={}", hwnd_hex(trio.balloon))),
"期待した手前側はバルーン窓のはず: {failed}"
);
assert!(
failed.contains(&format!("expected_below={}", hwnd_hex(trio.character))),
"期待した背後側はキャラ窓のはず: {failed}"
);
assert!(
failed.contains(&format!("measured={}", hwnd_hex(trio.intruder))),
"実測は割り込み窓のはず: {failed}"
);
assert!(
!world.entity(balloon).contains::<ReassertZOrder>(),
"不一致でも要求はそこで消える(再試行の輪を作らない)"
);
trio.destroy();
}
#[test]
fn peer_lost_before_verification_skips_instead_of_failing() {
let trio = Trio::create(w!("wintf-zorder-maintain-peer-lost"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, trio.character);
let balloon = spawn_window(&mut world, trio.balloon);
declare_pair(&mut world, balloon, character);
request_reassert(&mut world, balloon);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
SetWindowPosCommand::flush();
world.entity_mut(character).despawn();
schedule.run(&mut world);
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_line_count(
&out,
VERIFY_FAILED_TAG,
0,
"相手の消滅は正常系であり error にしない(要件 1.5)",
);
assert_line_count(&out, FIX_TAG, 0, "検証を行っていないので是正の記録も出ない");
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"残る窓へは書き込まない(最初の是正の 1 本だけ)",
);
assert!(!world.entity(balloon).contains::<ReassertZOrder>());
assert!(!world.entity(balloon).contains::<IssuedPairFix>());
trio.destroy();
}
#[test]
fn peer_losing_only_its_handle_before_verification_skips_with_handle_missing() {
let trio = Trio::create(w!("wintf-zorder-maintain-peer-handle-lost"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = spawn_window(&mut world, trio.character);
let balloon = spawn_window(&mut world, trio.balloon);
declare_pair(&mut world, balloon, character);
request_reassert(&mut world, balloon);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
SetWindowPosCommand::flush();
world.entity_mut(character).remove::<WindowHandle>();
schedule.run(&mut world);
schedule.run(&mut world);
SetWindowPosCommand::flush();
});
assert_line_count(&out, SKIP_TAG, 1, "見送りは理由つきで 1 本だけ");
let skip = lines_with(&out, SKIP_TAG)[0];
assert!(
skip.contains("reason=HandleMissing"),
"理由は「ハンドルが無い」であるはず(相手の実体は生きている): {skip}"
);
assert_line_count(
&out,
VERIFY_FAILED_TAG,
0,
"照合を行っていないので検証不一致にはしない",
);
assert_line_count(&out, FIX_TAG, 0, "照合を行っていないので是正の記録も出ない");
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"出るのは 1 巡目の是正だけ(この 1 本が捕捉の生存を示す対照でもある)",
);
assert!(!world.entity(balloon).contains::<ReassertZOrder>());
assert!(!world.entity(balloon).contains::<IssuedPairFix>());
trio.destroy();
}
#[test]
fn request_for_a_missing_peer_issues_no_command() {
let parent = create_test_parent(w!("wintf-zorder-maintain-peer-gone"));
let balloon_hwnd = create_test_child(parent, w!("balloon"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = world.spawn_empty().id();
let balloon = spawn_window(&mut world, balloon_hwnd);
declare_pair(&mut world, balloon, character);
request_reassert(&mut world, balloon);
world.entity_mut(character).despawn();
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, SKIP_TAG, 1, "見送りは理由つきで 1 本だけ");
assert!(lines_with(&out, SKIP_TAG)[0].contains("reason=PeerMissing"));
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"是正の指令は出ず、対照の 1 本だけが出るはず",
);
assert!(!world.entity(balloon).contains::<ReassertZOrder>());
destroy_test_window(parent);
}
#[test]
fn request_without_handles_issues_no_command() {
let parent = create_test_parent(w!("wintf-zorder-maintain-handleless"));
let balloon_hwnd = create_test_child(parent, w!("balloon"));
let mut world = World::new();
world.insert_resource(ZOrderPairStrategy::default());
let character = world.spawn_empty().id();
let balloon = spawn_window(&mut world, balloon_hwnd);
declare_pair(&mut world, balloon, character);
request_reassert(&mut world, balloon);
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, SKIP_TAG, 1, "見送りは理由つきで 1 本だけ");
assert!(lines_with(&out, SKIP_TAG)[0].contains("reason=HandleMissing"));
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"是正の指令は出ず、対照の 1 本だけが出るはず",
);
assert!(!world.entity(balloon).contains::<ReassertZOrder>());
destroy_test_window(parent);
}
#[test]
fn request_on_a_window_without_declaration_is_dropped_with_a_record() {
let parent = create_test_parent(w!("wintf-zorder-maintain-orphan"));
let character_hwnd = create_test_child(parent, w!("character"));
let balloon_hwnd = create_test_child(parent, w!("balloon"));
insert_after(character_hwnd, balloon_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);
request_reassert(&mut world, balloon);
let stray = spawn_window(&mut world, fake_hwnd(0x00C1));
request_reassert(&mut world, stray);
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
schedule.run(&mut world);
SetWindowPosCommand::flush();
issue_control_command(balloon_hwnd);
});
assert_line_count(
&out,
ORPHAN_REQUEST_MARK,
1,
"宣言の無い要求は 1 度だけ記録されて消えるはず",
);
assert_line_count(&out, SKIP_TAG, 1, "対照のペアは理由つきで 1 度見送られる");
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"どちらも指令を出さず、対照の 1 本だけが出るはず",
);
assert!(
lines_with(&out, ORPHAN_REQUEST_MARK)[0].contains(&format!("entity={stray:?}")),
"記録は宣言の無い窓を指すはず"
);
assert!(
!world.entity(stray).contains::<ReassertZOrder>(),
"落とした要求は残さない(毎巡読み飛ばす状態を作らない)"
);
destroy_test_window(parent);
}
#[test]
fn pending_verification_without_an_issued_command_is_dropped_with_a_record() {
let parent = create_test_parent(w!("wintf-zorder-maintain-unbacked"));
let character_hwnd = create_test_child(parent, w!("character"));
let balloon_hwnd = create_test_child(parent, w!("balloon"));
insert_after(character_hwnd, balloon_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);
request_reassert(&mut world, balloon);
let other_character = spawn_window(&mut world, fake_hwnd(0x00D1));
let other_balloon = spawn_window(&mut world, fake_hwnd(0x00D2));
declare_pair(&mut world, other_balloon, other_character);
world.entity_mut(other_balloon).insert(ReassertZOrder {
pending_verify: Some(ExpectedOrder {
above: fake_hwnd(0x00D2),
below: fake_hwnd(0x00D1),
}),
});
let mut schedule = maintain_schedule();
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
schedule.run(&mut world);
schedule.run(&mut world);
SetWindowPosCommand::flush();
issue_control_command(balloon_hwnd);
});
assert_line_count(
&out,
UNBACKED_VERIFY_MARK,
1,
"持ち越しの無い検証待ちは 1 度だけ記録されて消えるはず",
);
assert_line_count(&out, FIX_TAG, 0, "出してもいない指令を是正として記録しない");
assert_line_count(&out, VERIFY_FAILED_TAG, 0, "偽の検証不一致も記録しない");
assert_line_count(&out, SKIP_TAG, 1, "対照のペアは理由つきで 1 度見送られる");
assert_line_count(
&out,
SET_WINDOW_POS_TAG,
1,
"どちらも指令を出さず、対照の 1 本だけが出るはず",
);
assert!(
!world.entity(other_balloon).contains::<ReassertZOrder>(),
"落とした要求は残さない"
);
destroy_test_window(parent);
}
#[test]
fn measure_window_below_maps_failure_to_absence_and_records_it() {
let out = capture_under_filter(CAPTURE_DIRECTIVES, || {
assert_eq!(measure_window_below(HWND::default()), None);
});
assert!(
!out.trim().is_empty(),
"走査の失敗は記録に残るはず(黙って None へ潰さない)"
);
}
#[test]
fn pair_fix_command_moves_only_the_zorder() {
let target = fake_hwnd(0x0A01);
let after = fake_hwnd(0x0A02);
let cmd = pair_fix_command(target, InsertSpec::After(after));
assert_eq!(cmd.hwnd, target);
assert_eq!(
cmd.flags,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
"位置・寸法・活性化を伴わない 3 つのフラグちょうどであるはず(重なりの抑止は付かない)"
);
assert_eq!(cmd.hwnd_insert_after, Some(after));
assert_eq!((cmd.x, cmd.y, cmd.width, cmd.height), (0, 0, 0, 0));
let top = pair_fix_command(target, InsertSpec::TopEdge);
assert_eq!(top.flags, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
assert_eq!(top.hwnd_insert_after, Some(HWND_TOP));
assert_ne!(top.hwnd_insert_after, Some(HWND_TOPMOST));
assert_eq!(
top.flags & SWP_NOZORDER,
SET_WINDOW_POS_FLAGS(0),
"重なりを動かす指令なので SWP_NOZORDER は立たない"
);
}
#[test]
fn plan_pair_fix_picks_the_moved_window_per_arm() {
let above = fake_hwnd(0x0B01);
let below = fake_hwnd(0x0B02);
let front_of_below = fake_hwnd(0x0B03);
let obs = PairObservation {
trigger: PairTrigger::Reassert,
strategy: ZOrderPairStrategy::default(),
above_alive: true,
below_alive: true,
above_hwnd: Some(above),
below_hwnd: Some(below),
measured_below_of_above: None,
measured_above_of_below: Some(front_of_below),
measured_above_of_below_is_always_on_top: false,
};
let plan = plan_pair_fix(
&obs,
PairFixDecision::PlaceAboveOverBelow {
insert_after: InsertSpec::After(front_of_below),
},
)
.expect("手前へ寄せる腕は計画になるはず");
assert_eq!(plan.target, above, "動かすのは手前側の窓");
assert_eq!(plan.insert_after, InsertSpec::After(front_of_below));
assert_eq!(plan.expected, ExpectedOrder { above, below });
let plan = plan_pair_fix(
&obs,
PairFixDecision::PlaceBelowUnderAbove {
insert_after: above,
},
)
.expect("背後へ寄せる腕は計画になるはず");
assert_eq!(plan.target, below, "動かすのは背後側の窓");
assert_eq!(plan.insert_after, InsertSpec::After(above));
assert_eq!(
plan.expected,
ExpectedOrder { above, below },
"どちらの腕でも収束する隣接は同じ"
);
assert!(
plan_pair_fix(&obs, PairFixDecision::Skip(SkipReason::AlreadyAdjacent)).is_none(),
"見送りからは計画が作られない(指令を出さない)"
);
let handleless = PairObservation {
above_hwnd: None,
..obs.clone()
};
assert!(
plan_pair_fix(
&handleless,
PairFixDecision::PlaceAboveOverBelow {
insert_after: InsertSpec::TopEdge
}
)
.is_none(),
"ハンドルが揃わない観測からは計画が作られない"
);
}