wintf 0.0.3

Windows Tategaki Framework - Rust UI library with Japanese vertical text support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
//! 設計前実測(owner の鎖)——`research.md` §11.6 の 2・3 を実測で閉じるための調査檻。
//!
//! spec `areka-P0-scope-zorder-pinning` の改訂第 2 版(要件 14)は、前後関係の維持を
//! 「所有の鎖」——分岐の無い一直線の owner 連結——で行うことを要件で固定した。設計を
//! 書く前に、次の 2 点を**推論ではなく実測で**閉じる。
//!
//! 1. **表示中の窓の owner 張り替えが z 順へ即時反映されるか**(§11.6-2)。反映が
//!    後押し待ちなら、張り替え直後の 1 回だけの後押しで足りるか。
//! 2. **横断 edge の副作用**(§11.6-3)——最小化・非表示・破棄が鎖を伝うか。
//!    要件 11.5(重なり順以外の見える性質を変えない)と要件 7.2(破棄を巻き込まない)が
//!    構造的に満たせるかの分かれ目。
//!
//! # 窓の作り
//!
//! 本番のゴースト窓と同じ `WS_POPUP` + `WS_EX_TOOLWINDOW`(`placement/spawn.rs:572-576`)
//! で作り、`ShowWindow(SW_SHOWNOACTIVATE)` で**実際に表示状態**にする——「既に表示中の窓の
//! 張り替え」が問いだからである。寸法は 0x0 なので画面には 1 ピクセルも出ないが、
//! `WS_VISIBLE` は立ち、OS から見れば可視のトップレベル窓である。
//! `WS_EX_TOOLWINDOW` は本番と同じであり、この一点によって owner の付け外しが
//! **タスクバーへの出方を変えようがない**(道具窓は元からタスクバーに出ない)。
//!
//! # 測り方
//!
//! 重なりは `GetTopWindow` から `GW_HWNDNEXT` で降りる走査で読み、**自分が作った窓だけ**を
//! 拾って添字の列にする(`zorder_chain_order_tests.rs` の助手と同形)。隣接ではなく順序で
//! 見るので、不可視の隣(既定 IME 窓など)や他プロセスの窓が何枚挟まっても結果は動かない。
//!
//! # 決定論のための規律(要件 10.3・実測で確定)
//!
//! 実測で潰した非決定の元は 2 つある。
//!
//! ⑴ **助走に絶対帯指定(`HWND_TOP`/`HWND_BOTTOM`)を使わない**。cargo 3 プロセス同時の
//!    regime で、絶対帯指定を助走に用いた檻だけが 18 走行中 3 回落ちた。助走は
//!    [`arrange_z`](自分の窓どうしの相対指定)で組む。絶対帯指定を残してあるのは、
//!    **それ自体が測定対象である** [`owner_chain_probe_minimal_nudge_variants`] だけ。
//! ⑵ **挿入位置に他プロセスの窓を渡さない**。`GW_HWNDPREV`(いま 1 つ手前にいる窓)は
//!    自分の窓とは限らず、読み取りと書き込みの間に消えると `SetWindowPos` が黙って失敗する。
//!    full-suite の並走走行で実際に再現した。後押しは [`nudge_chain`]——**自分の窓 2 枚だけ**を
//!    参照する形——に統一してある。
//!
//! ⑵ は檻の都合ではなく**本番の設計判断**でもある(design.md の DD-3)。
//!
//! # ⚠ 実測 7・9 の結論は 2026-08-30 に撤回・測り直した(`research.md` §13.2)
//!
//! 当初この檻は、後押しの形として「**鎖の先頭を 2 番目の直後へ差し直す**」(⚠ **撤回済み**)を
//! 採り、それで鎖が収まると記録していた。**それは汚染された測定である**——受け皿を敷いていなかったため、
//! スレッド既定の不可視 IME 窓が**鎖の先頭に所有されて**おり、本来何も所有していないはずの
//! 先頭が「所有する窓」になっていた。Windows が鎖を並べ直すのは**所有する窓が動いたとき**
//! だけなので、受け皿を敷くと同じ形は **24 通りの始点すべてで 0/24**(1 枚も動かない)。
//!
//! よって実測 7・9 は [`ensure_ime_anchor`] で受け皿を敷いた形へ書き替え、主張も現在の形
//! (**鎖の根を錨の直後へ**・§13.3)へ改めてある。実測 9 は**撤回そのものを記録する**——
//! 同じ始点から、撤回済みの形では 1 枚も動かないことと、採用形なら収まることを対で測る。
//! 両テストは [`head_owns_nothing`] で「先頭が何も所有していない」ことを自己検査するので、
//! **受け皿を外すと必ず赤になる**。
//!
//! 他の実測(1・1b・2・3・4・5・6・8)は所有側を動かす形か絶対帯指定を測っており、記録済みの
//! 逐語値は受け皿の有無に依らない。よってそちらは値を保つためにあえて触っていない。
//!
//! # 主張しないこと
//!
//! **鎖と、鎖の外の窓との前後関係は主張しない。** 鎖は 1 つの塊として動き、後押しの際に
//! 鎖の外の窓を追い越すことがある(周囲の窓の状況に依る——並走走行で両方の結果が出た)。
//! 要件が縛るのは ⑴ 鎖の中の相対順(要件 1.1/1.2)と ⑵ **鎖の外どうし**の相対順
//! (要件 6.1/6.2)であって、その 2 群の間ではない。檻もそこまでしか固定しない。

use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::System::Threading::GetCurrentThreadId;
use windows::Win32::UI::WindowsAndMessaging::{
    CreateWindowExW, DestroyWindow, EnumThreadWindows, GW_HWNDNEXT, GW_OWNER, GetTopWindow,
    GetWindow, HWND_BOTTOM, HWND_TOP, IsIconic, IsWindow, IsWindowVisible, SW_HIDE, SW_MINIMIZE,
    SW_RESTORE, SW_SHOWNOACTIVATE, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SetWindowPos,
    ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WS_EX_TOOLWINDOW, WS_POPUP,
};
use windows::core::{BOOL, PCWSTR, w};

use super::{clear_window_owner, set_window_owner};

// ---------------------------------------------------------------------------
// 実窓(0x0・可視・トップレベル・本番と同じ拡張スタイル)
// ---------------------------------------------------------------------------

pub(super) fn create_probe_window(title: PCWSTR) -> HWND {
    use windows::Win32::System::LibraryLoader::GetModuleHandleW;
    // SAFETY: Win32 境界。自プロセス所有の 0x0 トップレベル窓を生成し、活性化を奪わずに
    // 表示状態へ移す。
    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 probe window");
        let _ = ShowWindow(hwnd, SW_SHOWNOACTIVATE);
        hwnd
    }
}

thread_local! {
    /// このスレッドで受け皿をもう作ったか。
    static IME_ANCHOR_MADE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}

/// **スレッド既定の不可視 IME 窓の受け皿**を、鎖の窓より先に 1 枚だけ作る。
///
/// Windows はスレッドにつき 1 つ `class="IME"` の不可視 0x0 窓を作り、それを**そのスレッドで
/// 最初に作られた窓に所有させる**。手当てをしないと「この檻が最初に作った窓=鎖の先頭」が
/// 所有する窓になり、**本番には無い性質**(先頭も所有する窓である)の上で測ってしまう。
/// §13 が掘り当てたのはまさにこの汚染であり、実測 7・9 の当初の結論はこれに乗っていた。
///
/// 受け皿は壊さない——Windows はスレッド終了時にそのスレッドの窓をすべて破棄するので、
/// テスト 1 本につき 0x0 の窓が 1 枚残り、終われば消える。壊すと所有が次の窓へ移りかねない。
///
/// **受け皿を敷くのは §13 で測り直した実測 7・9 と、§13 の掃き出しだけである。** 他の実測
/// (1・1b・2・3・4・5・6)は所有側を動かす形か絶対帯指定を測っており、記録済みの逐語値は
/// 受け皿の有無に依らないので、値を保つためにあえて触らない。
pub(super) fn ensure_ime_anchor() {
    IME_ANCHOR_MADE.with(|made| {
        if made.get() {
            return;
        }
        made.set(true);
        let _ = create_probe_window(w!("owner-chain-probe/ime-anchor"));
    });
}

/// その窓が、同じスレッドの窓を 1 つでも所有しているか。
fn owns_any_thread_window(hwnd: HWND) -> bool {
    /// 列挙の受け皿(探す窓と、見つかったかどうか)。
    struct Probe {
        target: HWND,
        found: bool,
    }

    /// 列挙のコールバック。`lparam` は直下で渡した [`Probe`] を指す。
    unsafe extern "system" fn visit(candidate: HWND, lparam: LPARAM) -> BOOL {
        // SAFETY: `lparam` には直下の `EnumThreadWindows` 呼び出しが `&mut Probe` を
        // 渡している。列挙はその呼び出しの中で完結するので、参照は生存している。
        let probe = unsafe { &mut *(lparam.0 as *mut Probe) };
        // SAFETY: Win32 境界。読み取りのみ。
        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,
    };
    // SAFETY: Win32 境界。自スレッドの窓を列挙し、各窓の所有者を読むだけである。
    unsafe {
        let _ = EnumThreadWindows(
            GetCurrentThreadId(),
            Some(visit),
            LPARAM(&raw mut probe as isize),
        );
    }
    probe.found
}

/// 鎖の先頭が何も所有していないか(=本番と同じ姿で測っているか)。
///
/// **生の隣接では測らない**——2 つの窓の間に何も挟まらないことはこちらが保証できるもの
/// ではない(他プロセスの窓がいつでも割り込む)。所有関係なら自分で作ったものだけを見る。
pub(super) fn head_owns_nothing(chain: &[HWND]) -> bool {
    chain
        .first()
        .is_some_and(|head| !owns_any_thread_window(*head))
}

pub(super) fn destroy_all(windows: &[HWND]) {
    for hwnd in windows {
        // SAFETY: Win32 境界。自プロセスが生成した窓を破棄する。
        unsafe {
            let _ = DestroyWindow(*hwnd);
        }
    }
}

fn is_visible(hwnd: HWND) -> bool {
    // SAFETY: Win32 境界。読み取りのみ。
    unsafe { IsWindowVisible(hwnd) }.as_bool()
}

fn is_alive(hwnd: HWND) -> bool {
    // SAFETY: Win32 境界。読み取りのみ。
    unsafe { IsWindow(Some(hwnd)) }.as_bool()
}

fn is_minimized(hwnd: HWND) -> bool {
    // SAFETY: Win32 境界。読み取りのみ。
    unsafe { IsIconic(hwnd) }.as_bool()
}

pub(super) fn owner_of(hwnd: HWND) -> Option<HWND> {
    // SAFETY: Win32 境界。読み取りのみ。
    unsafe { GetWindow(hwnd, GW_OWNER) }.ok()
}

// ---------------------------------------------------------------------------
// Z の読み取り
// ---------------------------------------------------------------------------

/// 走査をやり直す上限。観測できないまま無限に回らないためだけに置く。
const Z_SCAN_ATTEMPTS: usize = 8;

/// 与えた窓集合だけを Z の上から下へ並べて返す。
///
/// # なぜ 1 度の走査では足りないのか(2026-08-30 の実測)
///
/// 走査はデスクトップ全体の**生きた**連結リストを 1 歩ずつ辿る。歩いている最中に手前の
/// 他プロセスの窓が破棄されると `GetWindow(GW_HWNDNEXT)` が `Err` を返して走査がそこで
/// 打ち切られ、集合の窓へ辿り着く前に切れれば結果は**空**になる。3 プロセス同時 × 120
/// 走行で `owner_chain_probe_splice_in_and_out` がこの形で赤くなった(`left: []`)。
/// task 2.3 を含まない HEAD でも同じ形が出ており、道具の側の欠陥である
/// (`research.md` §13.7 の 7 件目)。よって全部拾えるまで有界回数だけやり直す。
/// 使い切ったときはいちばん多く拾えた回を返す=従来と同じ意味であり、主張は弱めない。
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
}

/// 走査の 1 回分(打ち切られたら短い列を返す)。
fn scan_z_once(windows: &[HWND]) -> Vec<HWND> {
    let mut result = Vec::new();
    // SAFETY: Win32 境界。デスクトップ配下の最前面窓を得る読み取り専用 API。
    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;
        }
        // SAFETY: Win32 境界。窓ハンドルに対する読み取り専用の走査。
        cursor = unsafe { GetWindow(hwnd, GW_HWNDNEXT) }.ok();
    }
    result
}

/// 添字の列から、指定した添字だけを順序を保って抜き出す。
///
/// **鎖の外の窓の絶対位置は主張しない**ための助手(module doc「主張しないこと」を参照)。
pub(super) fn only(shape: &[usize], keep: &[usize]) -> Vec<usize> {
    shape.iter().copied().filter(|i| keep.contains(i)).collect()
}

/// 組内の並びを添字の列(手前から順)で返す。
pub(super) fn z_shape(set: &[HWND]) -> Vec<usize> {
    relative_z_order(set)
        .iter()
        .filter_map(|hwnd| set.iter().position(|w| w == hwnd))
        .collect()
}

/// 素の Z 指令(助走・後押し専用)。
pub(super) fn place_after(hwnd: HWND, after: HWND) {
    // SAFETY: Win32 境界。自プロセスの窓の Z のみを動かす(活性化・移動・寸法変更なし)。
    unsafe {
        let _ = SetWindowPos(
            hwnd,
            Some(after),
            0,
            0,
            0,
            0,
            SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE,
        );
    }
}

fn place_at(hwnd: HWND, insert_after: HWND) {
    place_after(hwnd, insert_after);
}

/// 本設計が採る後押し——**鎖の根を錨(1 つ手前の窓)の直後へ差し直す**(`research.md` §13.3)。
///
/// > ⚠ **2026-08-30 訂正**: ここは当初「鎖の**先頭**を 2 番目の直後へ差し直す」だった。
/// > §13.2 でその形は**撤回**されている——Windows が鎖を並べ直すのは**所有する窓が動いた
/// > とき**だけであり、鎖の先頭は誰も所有していないので、先頭を動かしても鎖は並ばない
/// > (24 通りの始点すべてで 0/24)。当時それが効いて見えたのは、受け皿が無く
/// > **不可視の IME 窓が鎖の先頭に所有されていた**からである。撤回済みの形は
/// > [`retired_head_nudge`](実測 9 が「収まらないこと」を主張するために保存)。
///
/// 参照するのはどちらも自分の窓である。`GW_HWNDPREV`(=「いま自分の 1 つ手前にいる窓」)を
/// 挿入位置に使う形は**その窓が他プロセスのものでありうる**——読み取りと書き込みの間に
/// 消えると `SetWindowPos` が黙って失敗し、鎖が収まらない(full-suite の並走走行で実際に
/// 再現した)。よって自分の窓だけを参照するこちらを採る。
fn nudge_chain(chain: &[HWND]) {
    if chain.len() >= 2 {
        place_after(chain[chain.len() - 1], chain[chain.len() - 2]);
    }
}

/// **撤回済みの初版の後押し**——鎖の先頭を 2 番目の直後へ差し直す(記録として保存)。
///
/// ⚠ この形は**収まらない**(§13.2 で撤回)。実測 9 が「収まらないこと」を主張するために
/// だけ残してある。本番はこれを使わない。
fn retired_head_nudge(chain: &[HWND]) {
    if chain.len() >= 2 {
        place_after(chain[0], chain[1]);
    }
}

/// 組を `order`(手前から順の添字)へ揃える助走。
pub(super) fn arrange_z(set: &[HWND], order: &[usize]) {
    for pair in order.windows(2) {
        place_after(set[pair[1]], set[pair[0]]);
    }
}

/// 鎖を張る(`chain` は**手前から奥**の順。`chain[i]` は `chain[i+1]` に所有される)。
pub(super) fn link_chain(chain: &[HWND]) {
    for pair in chain.windows(2) {
        set_window_owner(pair[0], pair[1]).expect("set_window_owner should succeed");
    }
}

/// owner を持つ窓だけを外す。
///
/// **owner を持たない窓へ `clear_window_owner` を当ててはならない**——実測 5 が示すとおり
/// `SetWindowLongPtrW(GWLP_HWNDPARENT, 0)` は元の owner が無いとき偽の失敗を返す。
pub(super) fn unlink_all(windows: &[HWND]) {
    for hwnd in windows {
        if owner_of(*hwnd).is_some() {
            clear_window_owner(*hwnd).expect("clear_window_owner should succeed");
        }
    }
}

// ===========================================================================
// 実測 1(§11.6-2): 表示中の窓の owner 張り替えは z 順へ即時反映されるか
// ===========================================================================

#[test]
fn owner_chain_probe_relink_on_visible_windows() {
    let set: Vec<HWND> = (0..4)
        .map(|_| create_probe_window(w!("owner-chain-probe/relink")))
        .collect();

    // 助走——宣言の逆順(手前から 3,2,1,0)。鎖が効けば 0,1,2,3 へ動くはず。
    arrange_z(&set, &[3, 2, 1, 0]);
    let before = z_shape(&set);

    // 鎖を張る(手前から 0,1,2,3 = 0 が 1 に所有され…3 が根)。
    link_chain(&set);
    let owners: Vec<Option<usize>> = set
        .iter()
        .map(|h| owner_of(*h).and_then(|o| set.iter().position(|w| *w == o)))
        .collect();
    let after_link = z_shape(&set);

    // 後押し① 根を最背面へ落とす(1 回だけ)。
    place_at(set[3], HWND_BOTTOM);
    let after_nudge_root_bottom = z_shape(&set);

    // 後押し② 先頭を最前面へ持ち上げる(1 回だけ)。
    place_at(set[0], HWND_TOP);
    let after_nudge_head_top = z_shape(&set);

    // 攪乱——利用者の活性化に相当する形で最背面の窓を最前面へ持ち上げる。
    place_at(set[3], HWND_TOP);
    let after_disturb = z_shape(&set);

    eprintln!("[probe-1] before(reversed)        = {before:?}");
    eprintln!("[probe-1] owners(index)           = {owners:?}");
    eprintln!("[probe-1] after_link(no nudge)    = {after_link:?}");
    eprintln!("[probe-1] after_nudge_root_bottom = {after_nudge_root_bottom:?}");
    eprintln!("[probe-1] after_nudge_head_top    = {after_nudge_head_top:?}");
    eprintln!("[probe-1] after_disturb_root_top  = {after_disturb:?}");

    unlink_all(&set);
    let after_unlink = z_shape(&set);
    eprintln!("[probe-1] after_unlink            = {after_unlink:?}");

    destroy_all(&set);

    assert_eq!(before, vec![3, 2, 1, 0], "助走が宣言の逆順に揃っていること");
    assert_eq!(
        owners,
        vec![Some(1), Some(2), Some(3), None],
        "鎖は一直線(星形でも輪でもない)"
    );
    assert_eq!(
        after_link, before,
        "【実測】表示中の窓への owner 張り替えは、それだけでは重なりを 1 ミリも動かさない"
    );
    assert_eq!(
        after_nudge_root_bottom,
        vec![0, 1, 2, 3],
        "【実測】Z を伴う後押し 1 回で鎖全体が宣言順へ収まる"
    );
    assert_eq!(
        after_nudge_head_top,
        vec![0, 1, 2, 3],
        "収まった後は追加の後押しでも崩れない"
    );
    assert_eq!(
        after_disturb,
        vec![0, 1, 2, 3],
        "【実測・要件 14.3】最も奥の窓を最前面へ持ち上げても、OS が鎖の順を保つ"
    );
    assert_eq!(
        after_unlink,
        vec![0, 1, 2, 3],
        "【実測・要件 6】鎖を外しても並べ替えは起きない——束縛が消えるだけ"
    );
}

// ===========================================================================
// 実測 1b: 既に鎖が張られている状態から**別の順へ張り替える**
// ===========================================================================

#[test]
fn owner_chain_probe_rechain_to_a_different_order() {
    let set: Vec<HWND> = (0..4)
        .map(|_| create_probe_window(w!("owner-chain-probe/rechain")))
        .collect();

    link_chain(&set); // 手前から 0,1,2,3
    place_at(set[3], HWND_BOTTOM);
    let first = z_shape(&set);

    // 逆順の鎖へ張り替える(先に全部外してから張り直す=途中で星形も輪も作らない)。
    unlink_all(&set);
    let after_unlink = z_shape(&set);
    let reversed: Vec<HWND> = set.iter().rev().copied().collect();
    link_chain(&reversed); // 手前から 3,2,1,0
    let after_relink_no_nudge = z_shape(&set);
    place_at(set[0], HWND_BOTTOM); // 新しい根(添字 0)を最背面へ
    let after_relink_nudge = z_shape(&set);

    eprintln!("[probe-1b] first_chain            = {first:?}");
    eprintln!("[probe-1b] after_unlink           = {after_unlink:?}");
    eprintln!("[probe-1b] rechain_no_nudge       = {after_relink_no_nudge:?}");
    eprintln!("[probe-1b] rechain_after_nudge    = {after_relink_nudge:?}");

    unlink_all(&set);
    destroy_all(&set);

    assert_eq!(first, vec![0, 1, 2, 3], "最初の鎖が成立していること");
    assert_eq!(after_unlink, first, "外した瞬間には並びは動かない");
    assert_eq!(
        after_relink_no_nudge, first,
        "張り替えだけでは動かない(実測 1 と同じ)"
    );
    assert_eq!(
        after_relink_nudge,
        vec![3, 2, 1, 0],
        "【実測】全部外してから逆順で張り直し、後押し 1 回で新しい順へ収まる"
    );
}

// ===========================================================================
// 実測 2(§11.6-3): 副作用——最小化・非表示・破棄は鎖を伝うか
// ===========================================================================

#[test]
fn owner_chain_probe_minimize_and_hide_cascade() {
    // 鎖: front <- a <- b <- c :back (a は b に、b は c に所有される)
    let a = create_probe_window(w!("owner-chain-probe/casc-a"));
    let b = create_probe_window(w!("owner-chain-probe/casc-b"));
    let c = create_probe_window(w!("owner-chain-probe/casc-c"));
    let set = [a, b, c];
    link_chain(&set);

    let base = (is_visible(a), is_visible(b), is_visible(c));

    // ⑴ 根(最も奥・最上位の owner)を最小化する。
    // SAFETY: Win32 境界。自プロセスの窓の表示状態を変える。
    unsafe {
        let _ = ShowWindow(c, SW_MINIMIZE);
    }
    let after_min_root = (
        is_visible(a),
        is_visible(b),
        is_visible(c),
        is_minimized(c),
        is_minimized(a),
    );
    // SAFETY: Win32 境界。
    unsafe {
        let _ = ShowWindow(c, SW_RESTORE);
    }
    let after_restore_root = (is_visible(a), is_visible(b), is_visible(c));

    // ⑵ 中間の窓を最小化する。
    // SAFETY: Win32 境界。
    unsafe {
        let _ = ShowWindow(b, SW_MINIMIZE);
    }
    let after_min_mid = (is_visible(a), is_visible(b), is_visible(c));
    // SAFETY: Win32 境界。
    unsafe {
        let _ = ShowWindow(b, SW_RESTORE);
    }

    // ⑶ 根を非表示にする。
    // SAFETY: Win32 境界。
    unsafe {
        let _ = ShowWindow(c, SW_HIDE);
    }
    let after_hide_root = (is_visible(a), is_visible(b), is_visible(c));
    // SAFETY: Win32 境界。
    unsafe {
        let _ = ShowWindow(c, SW_SHOWNOACTIVATE);
    }

    eprintln!("[probe-2] base(a,b,c visible)          = {base:?}");
    eprintln!("[probe-2] after_min_root(a,b,c,ic,ia)  = {after_min_root:?}");
    eprintln!("[probe-2] after_restore_root(a,b,c)    = {after_restore_root:?}");
    eprintln!("[probe-2] after_min_mid(a,b,c)         = {after_min_mid:?}");
    eprintln!("[probe-2] after_hide_root(a,b,c)       = {after_hide_root:?}");

    unlink_all(&set);
    destroy_all(&set);

    assert_eq!(base, (true, true, true), "3 枚とも可視で始まること");
    assert_eq!(
        after_min_root,
        (false, false, true, true, false),
        "【実測・要件 11.5 の要注意点】所有者を最小化すると鎖の下流の窓がすべて不可視になる(隠れるのであって最小化されるのではない)"
    );
    assert_eq!(
        after_restore_root,
        (true, true, true),
        "【実測】復元で元に戻る=連動は可逆"
    );
    assert_eq!(
        after_min_mid,
        (false, true, true),
        "【実測】連動は下流(手前側)へだけ伝う。上流の所有者は影響を受けない"
    );
    assert_eq!(
        after_hide_root,
        (true, true, false),
        "【実測】`SW_HIDE` は連動しない——伝うのは最小化だけ"
    );
}

#[test]
fn owner_chain_probe_destroy_cascade_and_neutralization() {
    // ⑴ 素のまま根を破棄する。
    let a = create_probe_window(w!("owner-chain-probe/dest-a"));
    let b = create_probe_window(w!("owner-chain-probe/dest-b"));
    let c = create_probe_window(w!("owner-chain-probe/dest-c"));
    link_chain(&[a, b, c]);
    // SAFETY: Win32 境界。
    unsafe {
        let _ = DestroyWindow(c);
    }
    let after_destroy_root = (is_alive(a), is_alive(b), is_alive(c));
    destroy_all(&[a, b]);

    // ⑵ 中間の窓を破棄する。
    let d = create_probe_window(w!("owner-chain-probe/dest-d"));
    let e = create_probe_window(w!("owner-chain-probe/dest-e"));
    let f = create_probe_window(w!("owner-chain-probe/dest-f"));
    link_chain(&[d, e, f]);
    // SAFETY: Win32 境界。
    unsafe {
        let _ = DestroyWindow(e);
    }
    let after_destroy_mid = (is_alive(d), is_alive(e), is_alive(f));
    let d_owner_after = owner_of(d).map(|o| o == f);
    destroy_all(&[d, f]);

    // ⑶ 先に鎖から外してから破棄する(ペア切離の雛形=`zorder_pair_maintain.rs:286`)。
    let g = create_probe_window(w!("owner-chain-probe/dest-g"));
    let h = create_probe_window(w!("owner-chain-probe/dest-h"));
    let i = create_probe_window(w!("owner-chain-probe/dest-i"));
    link_chain(&[g, h, i]);
    clear_window_owner(g).expect("clear g");
    clear_window_owner(h).expect("clear h");
    // SAFETY: Win32 境界。
    unsafe {
        let _ = DestroyWindow(i);
    }
    let after_unlinked_destroy = (is_alive(g), is_alive(h), is_alive(i));
    destroy_all(&[g, h]);

    eprintln!("[probe-3] destroy_root(a,b,c alive)   = {after_destroy_root:?}");
    eprintln!("[probe-3] destroy_mid(d,e,f alive)    = {after_destroy_mid:?}");
    eprintln!("[probe-3] d_owner_is_f_after_mid_gone = {d_owner_after:?}");
    eprintln!("[probe-3] unlinked_destroy(g,h,i)     = {after_unlinked_destroy:?}");

    assert_eq!(
        after_destroy_root,
        (false, false, false),
        "【実測】鎖の根を破棄すると下流の窓がすべて道連れになる"
    );
    assert_eq!(
        after_destroy_mid,
        (false, false, true),
        "【実測】途中の窓の破棄は下流だけを巻き込み、上流の所有者は残る"
    );
    assert_eq!(
        after_unlinked_destroy,
        (true, true, false),
        "【実測・要件 7.2】先に鎖から外してから破棄すれば道連れは完全に消える"
    );
}

// ===========================================================================
// 実測 4(§11.6-4): スプライス——鎖の途中へ窓を挿す/途中の窓を抜く
// ===========================================================================

#[test]
fn owner_chain_probe_splice_in_and_out() {
    // 既存の鎖: front <- a <- b <- c :back
    let a = create_probe_window(w!("owner-chain-probe/spl-a"));
    let b = create_probe_window(w!("owner-chain-probe/spl-b"));
    let c = create_probe_window(w!("owner-chain-probe/spl-c"));
    let x = create_probe_window(w!("owner-chain-probe/spl-x")); // 後から現れる窓
    let all = [a, b, c, x];

    // 助走は **自分の窓どうしの相対配置だけ**で組む(絶対帯指定はデスクトップ全体の状態に
    // 依存し、並走走行で非決定になる)。手前から a,b,c,x。
    arrange_z(&all, &[0, 1, 2, 3]);
    link_chain(&[a, b, c]);
    nudge_chain(&[a, b, c]);
    let established = z_shape(&all);

    // 挿入——b と c の間へ x を差す。既存 edge を 1 本だけ切り、2 本張る。
    clear_window_owner(b).expect("clear b before splice");
    let mid_state = z_shape(&all);
    set_window_owner(b, x).expect("b <- x");
    set_window_owner(x, c).expect("x <- c");
    let after_splice_no_nudge = z_shape(&all);
    nudge_chain(&[a, b, c]);
    let after_splice_nudge = z_shape(&all);

    // 抜去——x を鎖から外し、b を直接 c へ繋ぎ直す。
    clear_window_owner(b).expect("clear b");
    clear_window_owner(x).expect("clear x");
    set_window_owner(b, c).expect("b <- c");
    nudge_chain(&[a, b, c]);
    let after_unsplice = z_shape(&all);

    eprintln!("[probe-4] established(a,b,c,x)    = {established:?}");
    eprintln!("[probe-4] mid_state(edge 1 本切)  = {mid_state:?}");
    eprintln!("[probe-4] splice_no_nudge         = {after_splice_no_nudge:?}");
    eprintln!("[probe-4] splice_after_nudge      = {after_splice_nudge:?}");
    eprintln!("[probe-4] after_unsplice          = {after_unsplice:?}");
    // 抜けた窓 x(添字 3)が最後にどの絶対位置へ落ち着くかは、こちらが決めていない
    // ——本 spec は鎖の外の窓を動かさないからである(要件 6.1)。よって主張するのは
    // **残った鎖 a,b,c の相対順だけ**にする。x の絶対位置まで固定すると、こちらが
    // 保証していないものを檻に書くことになり、実際に並走走行で赤くなる。
    let chain_order_after_unsplice: Vec<usize> =
        after_unsplice.iter().copied().filter(|i| *i < 3).collect();

    unlink_all(&all);
    destroy_all(&all);

    // 鎖の外の窓 x(添字 3)の絶対位置は主張しない——鎖は塊として動き、鎖の外の窓を
    // 追い越すことがある。主張するのは鎖 a,b,c の相対順だけ。
    assert_eq!(
        only(&established, &[0, 1, 2]),
        vec![0, 1, 2],
        "助走: 鎖 a,b,c が手前から宣言順で並んでいること"
    );
    assert_eq!(
        only(&mid_state, &[0, 1, 2]),
        vec![0, 1, 2],
        "【実測・要件 8.2】edge を 1 本切った途中状態でも鎖の順は崩れない"
    );
    assert_eq!(
        only(&after_splice_no_nudge, &[0, 1, 2]),
        vec![0, 1, 2],
        "張り直しだけでは動かない"
    );
    assert_eq!(
        after_splice_nudge,
        vec![0, 1, 3, 2],
        "【実測・要件 7.1】後押し 1 回で a,b,x,c = 差し込んだ鎖の順へ収まる"
    );
    assert_eq!(
        chain_order_after_unsplice,
        vec![0, 1, 2],
        "【実測・要件 7.2】x を抜いても残りの鎖 a,b,c は宣言順を保つ"
    );
}

// ===========================================================================
// 実測 5: `clear_window_owner` は owner を持たない窓に対して偽の失敗を返す
// ===========================================================================

#[test]
fn owner_chain_probe_clear_owner_on_unowned_window_reports_failure() {
    let lone = create_probe_window(w!("owner-chain-probe/lone"));
    let owner = create_probe_window(w!("owner-chain-probe/lone-owner"));

    // ⑴ owner を持たない窓を外そうとする。
    let unowned_result = clear_window_owner(lone).map_err(|e| e.code());

    // ⑵ 対照——owner を持つ窓を外すのは成功する。
    set_window_owner(lone, owner).expect("set_window_owner");
    let owned_result = clear_window_owner(lone).map_err(|e| e.code());

    // ⑶ 同じ窓を二度外そうとすると⑴と同じ失敗になる。
    let second_result = clear_window_owner(lone).map_err(|e| e.code());

    eprintln!("[probe-5] clear_on_unowned  = {unowned_result:?}");
    eprintln!("[probe-5] clear_on_owned    = {owned_result:?}");
    eprintln!("[probe-5] clear_twice       = {second_result:?}");

    destroy_all(&[lone, owner]);

    assert!(
        owned_result.is_ok(),
        "owner を持つ窓の切離しは成功しなければならない"
    );
    assert!(
        unowned_result.is_err(),
        "owner を持たない窓の切離しは失敗として返る(設計はこれを踏まない)"
    );
    assert_eq!(
        unowned_result, second_result,
        "二度目の切離しは一度目の未所有と同じ結果"
    );
}

// ===========================================================================
// 実測 6: 後押しの最小形——どの窓を、どこへ 1 回動かせば鎖が効くか
// ===========================================================================

#[test]
fn owner_chain_probe_minimal_nudge_variants() {
    // ⑴ 鎖の根を最背面へ。
    let s1: Vec<HWND> = (0..4)
        .map(|_| create_probe_window(w!("owner-chain-probe/nudge-root-bottom")))
        .collect();
    arrange_z(&s1, &[3, 2, 1, 0]);
    link_chain(&s1);
    place_at(s1[3], HWND_BOTTOM);
    let root_bottom = z_shape(&s1);

    // ⑵ 鎖の先頭を最前面へ。
    let s2: Vec<HWND> = (0..4)
        .map(|_| create_probe_window(w!("owner-chain-probe/nudge-head-top")))
        .collect();
    arrange_z(&s2, &[3, 2, 1, 0]);
    link_chain(&s2);
    place_at(s2[0], HWND_TOP);
    let head_top = z_shape(&s2);

    // ⑶ 中間の窓だけを動かす。
    let s3: Vec<HWND> = (0..4)
        .map(|_| create_probe_window(w!("owner-chain-probe/nudge-mid")))
        .collect();
    arrange_z(&s3, &[3, 2, 1, 0]);
    link_chain(&s3);
    place_at(s3[2], HWND_TOP);
    let mid_top = z_shape(&s3);

    // ⑷ 後押しをまったく出さない(対照)。
    let s4: Vec<HWND> = (0..4)
        .map(|_| create_probe_window(w!("owner-chain-probe/nudge-none")))
        .collect();
    arrange_z(&s4, &[3, 2, 1, 0]);
    link_chain(&s4);
    let no_nudge = z_shape(&s4);

    eprintln!("[probe-6] root->BOTTOM = {root_bottom:?}");
    eprintln!("[probe-6] head->TOP    = {head_top:?}");
    eprintln!("[probe-6] mid->TOP     = {mid_top:?}");
    eprintln!("[probe-6] no nudge     = {no_nudge:?}  (対照)");

    for s in [&s1, &s2, &s3, &s4] {
        unlink_all(s);
        destroy_all(s);
    }

    assert_eq!(root_bottom, vec![0, 1, 2, 3], "根への後押しで足りる");
    assert_eq!(head_top, vec![0, 1, 2, 3], "先頭への後押しでも足りる");
    assert_eq!(mid_top, vec![0, 1, 2, 3], "途中の窓への後押しでも足りる");
    assert_eq!(
        no_nudge,
        vec![3, 2, 1, 0],
        "【対照】後押しが無ければ何も起きない——上の 3 本は空虚ではない"
    );
}

// ===========================================================================
// 実測 7: 後押しが「グループ外の窓との関係」をどう動かすか(要件 6.2/11.1)
//
// 位置も寸法も Z も変えない後押し(`SWP_NOZORDER`)で足りるなら、グループ外の窓との
// 関係は 1 ミリも動かない。足りないなら、どの形が最も動かさないかを測る。
// ===========================================================================

/// 位置・寸法・Z のいずれも指定しない後押し(純粋な「触るだけ」)。
fn touch_only(hwnd: HWND) {
    use windows::Win32::UI::WindowsAndMessaging::SWP_NOZORDER;
    // SAFETY: Win32 境界。位置・寸法・Z のいずれも変更しない指令。
    unsafe {
        let _ = SetWindowPos(
            hwnd,
            None,
            0,
            0,
            0,
            0,
            SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOZORDER,
        );
    }
}

#[test]
fn owner_chain_probe_nudge_effect_on_outsiders() {
    // **受け皿を先に敷く**——敷かないと鎖の先頭が不可視の IME 窓を所有してしまい、
    // 「先頭も所有する窓である」という本番には無い性質の上で測ることになる(§13.2)。
    ensure_ime_anchor();

    // 組: 0,1,2 が鎖・3 が部外者。
    let chain: Vec<HWND> = (0..3)
        .map(|_| create_probe_window(w!("owner-chain-probe/out-chain")))
        .collect();
    let outsider = create_probe_window(w!("owner-chain-probe/out-outsider"));
    let mut all = chain.clone();
    all.push(outsider);

    // 助走——**自分の窓どうしの相対配置だけ**で組む。手前から 部外者(3), 2, 1, 0。
    arrange_z(&all, &[3, 2, 1, 0]);
    let start = z_shape(&all);

    link_chain(&chain);
    let head_bare = head_owns_nothing(&chain);

    // ⑴ 触るだけの後押し(Z を指定しない)。
    touch_only(chain[0]);
    let after_touch_head = z_shape(&all);
    touch_only(chain[2]);
    let after_touch_root = z_shape(&all);

    // ⑵ Z を伴う後押し——**所有側(鎖の根)**を錨の直後へ差し直す(§13.3 の採用形)。
    nudge_chain(&chain);
    let after_nudge = z_shape(&all);

    eprintln!("[probe-7] start(out=3 が最前面)   = {start:?}");
    eprintln!("[probe-7] head_owns_nothing       = {head_bare}");
    eprintln!("[probe-7] after_touch_head        = {after_touch_head:?}");
    eprintln!("[probe-7] after_touch_root        = {after_touch_root:?}");
    eprintln!("[probe-7] after_nudge(root->anchor) = {after_nudge:?}");

    unlink_all(&chain);
    destroy_all(&all);

    assert_eq!(start, vec![3, 2, 1, 0], "助走: 部外者が最前面・鎖は逆順");
    assert!(
        head_bare,
        "鎖の先頭が窓を所有している=受け皿が効かず、本番より易しい配置で測っている(§13.2)"
    );
    assert_eq!(
        after_touch_head, start,
        "【実測】`SWP_NOZORDER` の後押しでは再整列が起きない——Z を伴う指令でなければならない"
    );
    assert_eq!(after_touch_root, start, "根を触っても同じ");
    assert_eq!(
        only(&after_nudge, &[0, 1, 2]),
        vec![0, 1, 2],
        "【実測・§13.3】Z を伴う後押しでも**所有側を動かせば**鎖は宣言順へ収まる"
    );
}

// ===========================================================================
// 実測 9: **自分の窓だけ**を参照する後押し(実測 8 の弱点を潰す)
//
// 実測 8 の「その場への差し直し」は挿入位置に `GW_HWNDPREV`=**他プロセスの窓**を渡しうる。
// その窓が読み取りと書き込みの間に消えると `SetWindowPos` が黙って失敗し、鎖が収まらない
// (full-suite の並走走行で実際に再現した)。よって自分の窓だけを参照する形を採る。
//
// ⚠ **2026-08-30 に結論を撤回・測り直した(§13.2)。** 当初ここは代案として
// 「鎖の**先頭**を 2 番目の直後へ差し直す」を採り、収まると記録していた。それは
// **受け皿を敷いていなかったため**——不可視の IME 窓が鎖の先頭に所有されており、先頭が
// 「所有する窓」になっていた——の汚染された測定であった。受け皿を敷くと同じ形は
// **1 枚も動かさない**。本テストはその**撤回そのものを記録**する形へ書き替えてある:
// 撤回済みの形では収まらないこと(`retired_head_nudge`)と、採用形(`nudge_chain`=
// 所有側である根を動かす)なら同じ始点から収まることを、同じ 1 本で対にして測る。
// ===========================================================================

#[test]
fn owner_chain_probe_nudge_referencing_only_our_own_windows() {
    // **受け皿を先に敷く**——これが無いと初版の形が偶然効いてしまう(§13.2)。
    ensure_ime_anchor();

    let chain: Vec<HWND> = (0..3)
        .map(|_| create_probe_window(w!("owner-chain-probe/own-chain")))
        .collect();
    let front_outsider = create_probe_window(w!("owner-chain-probe/own-front"));
    let back_outsider = create_probe_window(w!("owner-chain-probe/own-back"));
    let mut all = chain.clone();
    all.push(front_outsider);
    all.push(back_outsider);

    // 手前から: front_outsider(3), 鎖の逆順 2,1,0, back_outsider(4)。
    arrange_z(&all, &[3, 2, 1, 0, 4]);
    let start = z_shape(&all);

    link_chain(&chain);
    let head_bare = head_owns_nothing(&chain);

    // ⑴ **撤回済みの初版の形**——先頭(何も所有していない窓)を 2 番目の直後へ。
    retired_head_nudge(&chain);
    let after_retired = z_shape(&all);

    // ⑵ **採用形**——所有側である根を錨の直後へ。同じ始点から測る。
    nudge_chain(&chain);
    let after_adopted = z_shape(&all);

    eprintln!("[probe-9] start             = {start:?}");
    eprintln!("[probe-9] head_owns_nothing = {head_bare}");
    eprintln!("[probe-9] after_retired(head->second) = {after_retired:?}");
    eprintln!("[probe-9] after_adopted(root->anchor) = {after_adopted:?}");

    unlink_all(&chain);
    destroy_all(&all);

    assert_eq!(
        start,
        vec![3, 2, 1, 0, 4],
        "助走: 部外者で前後を挟み鎖は逆順"
    );
    assert!(
        head_bare,
        "鎖の先頭が窓を所有している=受け皿が効かず、本番より易しい配置で測っている(§13.2)"
    );
    assert_eq!(
        after_retired, start,
        "【実測・§13.2 の撤回そのもの】撤回済みの形(先頭を 2 番目の直後へ)は 1 枚も動かさない。当初ここが収まると記録していたのは、受け皿が無く IME 窓が先頭に所有されていたためである"
    );
    assert_eq!(
        only(&after_adopted, &[0, 1, 2]),
        vec![0, 1, 2],
        "【実測・§13.3】自分の窓だけを参照する後押しでも、**所有側を動かせば**鎖は宣言順へ収まる"
    );
    assert_eq!(
        only(&after_adopted, &[3, 4]),
        vec![3, 4],
        "【実測・要件 6.1/6.2】鎖の外どうしの相対順は変わらない"
    );
}