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
//! 所有の鎖——**計画の受け口・帳簿・差分の純判断・後押しの選定・記録の唯一の出口**
//! (要件 4.1/7.1/7.2/8.2/10.1/10.4/11.1/14.4)。
//!
//! design.md「`zorder_chain`(新設・受け口と純関数)」が正本である。ここに在るのは
//! **判断だけ**であり、Win32 の呼び出しも World の走査も 1 つも無い(それは後続の適用系
//! `zorder_chain_apply` の担当である)。分けてあるのは、鎖の組み替えの導出を実機も
//! 実ディスプレイも無しに全分岐固定できる形にするためである(要件 10.1)。
//!
//! # 望む状態と現況の 2 つしか持たない
//!
//! 状態は [`ZOrderChainPlan`](望む鎖・areka が公開する)と [`CrossOwnerLink`] の集合
//! (現況=本 spec が実際に OS へ書いた繋ぎ)の 2 つだけである。**OS の z 順そのものは
//! 状態として持たない**——持てば周期的な観測が要り、要件 14.2 が禁じた「繰り返しの観測と
//! 是正」へ逆戻りする。維持は「所有される窓は所有者より手前」という OS の不変条件へ
//! 委ねる(要件 14.1)。
//!
//! # areka の型は 1 つも載せない
//!
//! `wintf → areka` の import は禁止(既存規律)なので、鎖の型は**こちらで定義し areka が
//! 組む**。よって欄に載せられるのは Entity と HWND と正準表記の文字列だけであり、スコープや
//! 窓種別の知識は areka 側に閉じる。既存の `KeepDirectlyAbove` と同じ分界である。
//!
//! # 記録の出口をここへ集約する理由(本 task の裁定)
//!
//! `tracing` の既定の出力先は**呼び出し元の module path** である。行を組む純関数
//! ([`zorder_chain_diag`](super::zorder_chain_diag))の側からマクロを撃つと、出力先が
//! 行組立の層の名前になり、記録を出す場所が増えるたびに grep の対象が割れていく。
//! よって鎖系 7 語の**マクロ呼出はこの 1 ファイルに集約**し、出力先を
//! `wintf::ecs::window::zorder_chain` の 1 本に固定する(design.md の同節・初版の申し送り 2.1)。
//!
//! 例外は保全語彙 2 語(`[zorder-group] applied`/`rejected`)である。あちらは design.md
//! 「保全する既存語彙(要件 9.5)と、その新しい住処」が移設先ファイルを
//! `zorder_chain_diag.rs` と**名指しで**規定しており、記録そのものもそちらに置いてある。
//! 出力先は `…::zorder_chain_diag` になるが、実機サインオフの `RUST_LOG` 指定
//! `wintf::ecs::window::zorder_chain=debug` は**前方一致**でこれを点灯させ、
//! `signoff-scan.ps1` は出力先の文字列を 1 つも読まない(判定はタグの字面で行う)。
//! よって語彙の保全(要件 9.5)は割れない。2 つの住処が実際に分かれていることは
//! 兄弟テストが捕捉した行の出力先で固定している。
//!
//! # `dead_code` 許可は 1 つも残っていない
//!
//! 適用系([`zorder_chain_apply`](super::zorder_chain_apply))が着地したことで、差分の
//! 純判断・後押しの選定・記録の出口には本番の呼び手が付いた。段階的実装のために
//! モジュール全体を覆っていた許可は撤去してある。
use *;
use ;
use HWND;
use ;
use ;
// ============================================================================
// 鎖の計画——areka が組み、wintf が適用する
// ============================================================================
/// 手前側の窓が奥側の窓に所有される 1 本の関係。
///
/// 向きを取り違えると鎖が逆さまになるため、欄の名前で向きを固定する——
/// `owned` が**手前**、`owner` が**奥**である(OS の不変条件「所有される窓は所有者より
/// 手前」がそのまま並びになる)。
/// 全窓の鎖 1 本ぶんの計画(areka が構築し、wintf が適用する)。
///
/// `members` はグループの連結(登記順・先に登記されたほど手前)+未指定スコープの
/// 後方配置(スコープ ID 昇順・要件 15)を、**実在する窓だけへ射影した**列である。
/// areka が公開する「望む鎖」。wintf 側の唯一の受け口。
/// 本 spec が張った横断 edge の帳簿(**被所有側**の Entity に付く)。
///
/// 被所有側に付けるのは、`clear_window_owner` が被所有側だけを引数に取るからである
/// (撤去の主語と帳簿の住処が同じになる)。`owner_hwnd` は**張った時点で実際に書き込んだ
/// 値**であり、撤去の前に `GetWindow(GW_OWNER)` の現況と突き合わせるために控える——
/// 食い違うときに撤去すると、ペア機構が張り替えた繋ぎを誤って外し、バルーン直上
/// (要件 6.3)を壊す。
// SAFETY: `CrossOwnerLink` は `HWND`(`*mut c_void` の newtype)を 2 つ保持するため、
// 自動では Send/Sync が導出されず、この手動 impl は `Component`(ECS は Send+Sync を
// 要求)にするために必須である。健全性: 保持するのは窓の不透明な識別子の**値の写し**で
// あり、所有権も破棄責務も持たない。この HWND を Win32 へ渡すのは鎖の適用系であり、
// UI スレッド固定された system である。既存の `OwnerLink`/`ZOrder::InsertAfter(HWND)`
// と同根の扱いである。
unsafe
unsafe
// ============================================================================
// 差分の純判断——必ず「全ての撤去が先・全ての付与が後」
// ============================================================================
/// 鎖へ出すべき操作 1 つ。
pub
/// 望む edge 列と現況の帳簿から、出すべき操作列を導く(純関数)。
///
/// 返り値は必ず **すべての [`ChainOp::Detach`] が先・すべての [`ChainOp::Attach`] が後**
/// である(§12.4 の実測手順「切る 1 本 → 張る 2 本」)。混ぜると、外す前に張った窓が
/// 一時的に 2 つの所有者を主張する形になり、途中状態で鎖が分岐する。
///
/// # ここで出る撤去の理由は 2 つだけ
///
/// - [`DetachReason::Teardown`]: 望む鎖にその窓の繋ぎがもう無い(解除・後方配置の撤去)
/// - [`DetachReason::Rechain`]: 同じ窓の相手が別の窓へ変わる(スプライス)
///
/// 残る 2 つは**この純関数では決まらない**——[`DetachReason::Departing`] は窓が去る事実
/// (帳簿より前の段で判る)、[`DetachReason::Diverged`] は撤去の直前に読んだ OS の現況と
/// 帳簿の食い違い(Win32 を読まないと判らない)であり、どちらも適用系が決める。
///
/// # 望む列に同じ窓が 2 度現れたら先頭に近い方だけを採る
///
/// 前提(`compose_chain` の不変条件)が満たされていればこの形は来ない。それでも
/// **構造で潰してある**のは、ここが「同じ窓へ 2 つの所有者を主張する」=要件 14.4 が
/// 禁じた星形・分岐が生まれうる唯一の場所だからである。手前側を残すのは、鎖が手前から
/// 奥へ並ぶ列であり、先に現れた繋ぎの方が鎖の頭に近いためである。
pub
// ============================================================================
// 後押しの選定——自分の窓 2 枚しか参照しない(動かすのは鎖の根)
// ============================================================================
/// 後押しの指令を組む(純関数・§12.2 実測 9/§13 実測 10)。
///
/// 表示中の窓の owner を張り替えても、それだけでは重なりが動かない(§12.2 実測 6)。
/// 収めるには Z を伴う指令を 1 回だけ出す必要がある。その形を
/// **鎖の根(`members` の末尾)を、その 1 つ手前の窓(`members[len-2]`・以下「錨」)の
/// 直後へ差し直す**に固定する。
///
/// # なぜ動かすのは根なのか(§13 実測 10)
///
/// Windows が鎖全体を並べ直すのは、**所有する窓が動いたとき**である——所有される窓は
/// 所有者より手前という不変条件を保つため、所有側を動かすと被所有側が引き連れられ、
/// それが鎖の奥から手前へ再帰的に伝わる。鎖の中で「所有していて、かつ誰にも所有されて
/// いない」のは根だけであり、逆に**先頭は誰も所有していない**。
///
/// 初版が採っていた「先頭を 2 番目の直後へ」は、先頭が何も所有していないため
/// **その 1 枚が動くだけ**で鎖は並び直らない。しかも指令の意味は「先頭を 2 番目の**奥**へ」
/// であり、望む並びとは逆である。24 通りの始点すべてで実測した結果は
/// **0/24**(=1 度も収まらない)であった。収まって見えていたのは、スレッド既定の
/// 不可視 IME 窓がたまたま先頭に所有されていた配置に限る(§13.1)。
///
/// # 挿入位置は 2 択(現況を 1 度だけ読む)
///
/// `SetWindowPos` は「要求した位置が現在位置と同じ」なら重なりを 1 ミリも動かさない。
/// よって錨の直後が既に根であるとき、素直な形は**完全な空振り**になる。そこで
/// **その 1 度きりの判定にだけ**、錨の生の 1 つ奥(`anchor_next`)を使う:
///
/// - `anchor_next != Some(root)` → 挿入位置は**錨**(望む関係そのものを主張する形)
/// - `anchor_next == Some(root)` → 挿入位置は**先頭**(`members[0]`)
///
/// 2 つの要求が同時に空振りすることはない——根の生の 1 つ手前は高々 1 枚であり、
/// 鎖が 3 枚以上なら錨と先頭は別の窓だからである。よって
/// **どちらか一方は必ず本物の位置変更になる**。これは「Windows が隣接時に省略する」という
/// 未文書の性質に賭ける形ではなく、「要求位置が現在位置と違えば Z は動く」という
/// `SetWindowPos` の基本の性質だけに乗る形である。
///
/// 窓が 2 枚のときは錨と先頭が同じ窓になり、2 択は 1 つに畳まれる。そのとき空振りする
/// のは「根が先頭の直後に居る」=**既に望む並びである**場合だけなので、収めるものが無い。
///
/// # 現況を読むこと自体は要件 14.2 に触れない
///
/// 禁じられているのは**周期的な観測と是正**である。ここで読むのは、書く直前に 1 度だけ、
/// 出す指令の形を選ぶためであり、既存の撤去経路(`detach_one` が外す前に
/// `GWLP_HWNDPARENT` を読む)と同じ規律である。読んだ値を控えもしないし、次の巡へ
/// 持ち越しもしない。
///
/// # 他の形を選んではならない
///
/// - `SWP_NOZORDER`(触るだけ)は再整列を起こさない(実測 7)
/// - `GW_HWNDPREV` で得た「いま自分の 1 つ手前にいる窓」を挿入位置に渡す形は、その窓が
/// **他プロセスのもの**でありうる。読み取りと書き込みの間にそれが消えると
/// `SetWindowPos` が黙って失敗し、鎖が収まらない(`research.md` §12.9 の 2 件目で実測)
/// - `HWND_TOP`/`HWND_BOTTOM` の絶対帯指定は、グループの絶対位置を無用に動かす
///
/// いずれの禁じ手も「指令が名指しする窓が `members` の外から来る」形で現れる。兄弟テストは
/// 指令に現れる窓ハンドルを列挙して `members` の部分集合であることを主張しており、
/// 差し替えた瞬間に赤くなる。
///
/// # 位置・寸法を運ぶ欄がそもそも無い
///
/// 指令は位置も寸法も持たない [`WindowPos`] から組む。よって
/// `SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE` は「フラグを立て忘れない」ではなく
/// **組み立て方から従う**——これが要件 11.1 の構造的な根拠である(既存ペア機構の
/// `pair_fix_command` と同じ規律)。
///
/// # 積まずに返す
///
/// 返すのは値であって、[`SetWindowPosCommand::enqueue`] の遅延キューへは積まない。
/// 理由は 2 つ——⑴ 要件 9.2 が「組み替えの**直後に実測**した重なり」を求めており、
/// 遅延バッチでは同じ巡で測れない。⑵ 積む側は内部で起床の印を立てるので、通せば
/// 「書いたあとに次の巡を促す」形になり、要件 14.2 が退役させた反復是正が裏口から戻る。
///
/// `members.len() < 2` のときは後押しを出さない(張るべき繋ぎも 1 本も無い)。
pub
// ============================================================================
// 記録の出口——鎖系 7 語のマクロ呼出はここだけ
// ============================================================================
// 以下 7 本は行組立を 1 つも持たない——本文は
// [`zorder_chain_diag`](super::zorder_chain_diag) の純関数の戻り値をそのまま載せる。
// 組立を二重に持つと、片方だけが書式を変えたときに実機の手順が静かに嘘になる。
/// 繋いだ事実を記録する(debug・要件 9.1)。
pub
/// 外した事実を記録する(debug・要件 4.1/7.2/9.1)。
pub
/// 収まった事実を記録する(debug・要件 9.2)。
///
/// 鎖全体につき 1 行である。宣言(`declared=`)と後押しの直後の実測(`measured=`)を
/// 同じ 1 行に載せるのは、分けると「指令は出したが効かなかった」の判定が 2 行の突合に
/// なり、過去に同型の誤診を生んでいるためである。
pub
/// 宣言された要素の窓が不在だった事実を記録する(debug・要件 1.4/8.4)。
///
/// 鎖系 7 語のうち、**これだけは areka から呼ばれる**——不在は「宣言と在庫の食い違い」
/// であり、それを知っているのは台帳と在庫を持つ側だからである。よって保全語彙 2 語
/// ([`log_group_applied`](super::zorder_chain_diag::log_group_applied) 等)と同じく
/// crate の外へ開いてある。出力先は本モジュールのままなので、grep の対象は割れない。
/// 呼び手は指令消化の相(areka の `emo2_boot/frame/zorder_drain.rs`)であり、材料は
/// [`ChainPlan::absent`] が `(group_id, element)` の対で運ぶ。同じ内容が続く間は
/// 1 度だけ出す(連呼の抑止は呼び手側の控えが持つ)。
/// 見送りを理由つきで記録する(debug・要件 8.3)。
///
/// 理由の無い見送りを作れないよう、引数は [`ChainSkipReason`] の 1 つに限ってある。
pub
/// 張り失敗を記録する(error・要件 8.2)。
///
/// 失敗した繋ぎ 1 本だけを飛ばして残りは張る、というのが裁定である。よってこの行は
/// 「どの区間のどの対が張れなかったか」を単独で読めなければならない。
pub
/// 外し失敗を記録する(error・要件 8.2)。
pub