@cssints/apg-specs 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,831 @@
1
+ // Resizable panels: a group of N panes and N-1 separators (bean cssints-pyit).
2
+ //
3
+ // Not an APG pattern: it is a group of APG window splitters, so the vocabulary
4
+ // and the keyboard contract come from windowsplitter.qnt, the layout from
5
+ // react-resizable-panels. Researched for the decisions below:
6
+ // - react-resizable-panels v4 (bvaughn): panels carry min/max in % of the
7
+ // group, `collapsible` + `collapsedSize`, a layout is a list of percentages
8
+ // that always sums to 100, `onLayoutChange` fires on every pointer move and
9
+ // `onLayoutChanged` on release (persist there), drag is pointer capture,
10
+ // double-click resets to the default layout, Home/End and arrows resize,
11
+ // Enter toggles collapse.
12
+ // - Radix/Ark splitter: the same model (sizes %, min/max, collapsible, arrows,
13
+ // Enter), one separator between two neighbours.
14
+ // - APG window splitter: the separator is the focusable widget; aria-valuenow
15
+ // is the size of the pane it resizes (aria-controls), aria-valuemin / -max
16
+ // its bounds, orientation names the bar, arrows move by a step, Home/End are
17
+ // optional jumps, Enter collapses and restores, F6 cycles panes.
18
+ //
19
+ // MODELLED: three panes in percentages on a 10% grid, 100 in total, MIN 10 and
20
+ // MAX 80 per pane, pane 0 collapsible at collapsedSize 0. One separator sits
21
+ // between each pair, its aria-valuenow is the size of the pane on its left.
22
+ // Focus is per separator (the group's tab stops; no F6: with three panes the
23
+ // cycle buys nothing a Tab does not, so the primitive leaves it out).
24
+ //
25
+ // A MOVE (arrow, Home/End, drag) changes only the separator's two neighbours:
26
+ // pane i grows or shrinks, pane i+1 takes the opposite amount, and the result is
27
+ // clamped by BOTH panes' bounds (no cascade past the neighbour). Collapsing pane
28
+ // 0 gives its space to the nearest pane that can take it and cascades outward
29
+ // (pane 1 up to MAX, then pane 2); restoring takes it back in the same order, so
30
+ // a restore always returns exactly the remembered size (prev0 <= 80 and the
31
+ // panes right of a collapsed pane hold 100, so the space is always there).
32
+ // A separator next to a collapsed pane is immovable: its arrows and its drag are
33
+ // no-ops, and only Enter (the restore) acts. Collapsed size 0 is the collapsed
34
+ // state; a pane cannot be dragged below MIN while expanded.
35
+ //
36
+ // DRAG is snapshot + delta, as in canvas/pointer.qnt: pointerdown remembers the
37
+ // layout (k0..k2) and the separator (dr), each move adds NOTCH to the delta (dd,
38
+ // bounded to +/-100) and the layout is re-solved from the snapshot, never
39
+ // accumulated, so dragging back past a clamp recovers exactly. pointerup ends
40
+ // the drag (the newest pointerdown takes over and re-snapshots); a host write
41
+ // cancels it, and the keyboard is a no-op while it runs. A drag never changes a
42
+ // pane outside its pair (DragMovesOnlyNeighbours).
43
+ //
44
+ // REQUEST VS COMMIT is the windowsplitter contract (map ticket rt1p), applied to
45
+ // the whole layout: every move requests the target layout (rq0..rq2) and bumps
46
+ // `requests` when it differs from the current sizes; uncontrolled (or a
47
+ // controlled host that accepts, `accepting`) commits it, a controlled host that
48
+ // holds ignores it ("rejected"). The host writes the layout with hostEqual /
49
+ // hostWide / hostNarrow / hostCollapsed (the only commit while controlled) and
50
+ // settles `outcome` back to "none". base0..base2 is the layout a request found.
51
+ //
52
+ // NOT MODELLED: pane min/max other than 10/80, collapsedSize other than 0, more
53
+ // than three panes (the primitive takes N; this model is the three-pane instance
54
+ // its tests run), collapsing a pane other than pane 0, the collapsed threshold
55
+ // react-resizable-panels uses (a pane collapses on Enter or at the host's
56
+ // request, never by dragging through MIN), persistence (host storage), nested
57
+ // groups, and F6.
58
+ //
59
+ // GENERATOR GAPS HIT: no parameterized actions and no collections, so a drag
60
+ // delta is a bounded int advanced by dragNext/dragPrev and the three sizes are
61
+ // three vars; every action assigns every var.
62
+ module patterns_resizable_panels {
63
+ import aria.* from "../../shared/aria"
64
+
65
+ var o: str // orientation: "vertical" (columns) | "horizontal" (rows)
66
+ var s0: int // committed size of pane 0, in percent (0 = collapsed)
67
+ var s1: int // pane 1
68
+ var s2: int // pane 2
69
+ var prev0: int // pane 0's size remembered at its last collapse
70
+ var focus: str // "none" | "sep0" | "sep1"
71
+ var justRestored: bool // the last event was an Enter that restored pane 0
72
+ var rq0: int // target of the last request: pane 0
73
+ var rq1: int // pane 1
74
+ var rq2: int // pane 2
75
+ var b0: int // sizes when the last request was received
76
+ var b1: int
77
+ var b2: int
78
+ var k0: int // pointerdown snapshot of a drag
79
+ var k1: int
80
+ var k2: int
81
+ var dd: int // drag delta since pointerdown, percent
82
+ var dr: int // separator being dragged (-1 = none)
83
+ var outcome: str // "none" | "accepted" | "rejected"
84
+ var controlled: bool // the host owns the layout (`sizes` prop present)
85
+ var accepting: bool // controlled host that answers every request with its intent
86
+ var requests: int // onSizesChange calls
87
+
88
+ def MIN: int = 10
89
+ def MAX: int = 80
90
+ def ZERO: int = 0
91
+ def FULL: int = 100
92
+ def STEP: int = 10
93
+ def NOTCH: int = 10
94
+ def DMAX: int = 100
95
+ def FAR: int = 100
96
+ def INIT0: int = 20
97
+ def INIT1: int = 40
98
+ def INIT2: int = 40
99
+
100
+ def clampI(v: int, lo: int, hi: int): int = if (v < lo) lo else if (v > hi) hi else v
101
+ def minI(a: int, b: int): int = if (a < b) a else b
102
+ def maxI(a: int, b: int): int = if (a > b) a else b
103
+
104
+ // --- the layout ---------------------------------------------------------
105
+ def isCollapsed(i: int): bool = and(i == 0, s0 == ZERO)
106
+ def onSep(i: int): bool = focus == (if (i == 0) "sep0" else "sep1")
107
+ def sepA(i: int): int = if (i == 0) s0 else s1
108
+ def sepB(i: int): int = if (i == 0) s1 else s2
109
+ // the new size of the left pane of a pair (a, b) moved by d, clamped by both
110
+ def solved(a: int, b: int, d: int): int =
111
+ clampI(a + d, maxI(MIN, a + b - MAX), minI(MAX, a + b - MIN))
112
+
113
+ // the focused separator, or -1
114
+ def fIdx: int = if (focus == "sep0") 0 else if (focus == "sep1") 1 else 0 - 1
115
+ def fA: int = sepA(fIdx)
116
+ def fB: int = sepB(fIdx)
117
+ def fMovable: bool = and(fIdx >= 0, and(not(isCollapsed(fIdx)), not(isCollapsed(fIdx + 1))))
118
+ // the pointer owns the interaction while a drag runs: keys are no-ops then
119
+ def dragging: bool = dr >= 0
120
+ def arrowA(axis: str, d: int): int =
121
+ if (and(fMovable, and(o == axis, not(dragging)))) solved(fA, fB, d) else fA
122
+ def jumpA(d: int): int = if (and(fMovable, not(dragging))) solved(fA, fB, d) else fA
123
+ // the requested layout after the focused separator's left pane becomes na
124
+ def triple0(na: int): int = if (fIdx == 0) na else s0
125
+ def triple1(na: int): int = if (fIdx == 0) fA + fB - na else if (fIdx == 1) na else s1
126
+ def triple2(na: int): int = if (fIdx == 1) fA + fB - na else s2
127
+
128
+ // --- collapse / restore (pane 0 only) -----------------------------------
129
+ def enterOn: bool = and(focus == "sep0", not(dragging))
130
+ def collapsedNow: bool = s0 == ZERO
131
+ def restGive1: int = minI(prev0, s1 - MIN)
132
+ def enterA: int = if (enterOn) (if (collapsedNow) prev0 else ZERO) else s0
133
+ def enterB: int =
134
+ if (enterOn) (if (collapsedNow) s1 - restGive1 else minI(s0 + s1, MAX)) else s1
135
+ def enterC: int = if (enterOn) FULL - enterA - enterB else s2
136
+
137
+ // --- drag (snapshot + delta) --------------------------------------------
138
+ def dragOk: bool = and(dr >= 0, and(not(isCollapsed(dr)), not(isCollapsed(dr + 1))))
139
+ def dragA: int = if (dr == 0) k0 else k1
140
+ def dragB: int = if (dr == 0) k1 else k2
141
+ def dragSolved(d: int): int = if (dragOk) solved(dragA, dragB, d) else dragA
142
+ def dragT0(d: int): int = if (dragOk) (if (dr == 0) dragSolved(d) else k0) else s0
143
+ def dragT1(d: int): int =
144
+ if (dragOk) (if (dr == 0) dragA + dragB - dragSolved(d) else dragSolved(d)) else s1
145
+ def dragT2(d: int): int = if (dragOk) (if (dr == 1) dragA + dragB - dragSolved(d) else k2) else s2
146
+ def ddNext(d: int): int = clampI(dd + d, 0 - DMAX, DMAX)
147
+
148
+ // --- request / commit ---------------------------------------------------
149
+ def holds: bool = and(controlled, not(accepting))
150
+ def verdict: str = if (holds) "rejected" else "accepted"
151
+ def landed(h: bool, v: int, want: int): int = if (h) v else want
152
+ def differs(a0: int, a1: int, a2: int): bool = or(s0 != a0, or(s1 != a1, s2 != a2))
153
+ def moved(a0: int, a1: int, a2: int): int = if (differs(a0, a1, a2)) 1 else 0
154
+ def enterChanged: bool = differs(enterA, enterB, enterC)
155
+ def rejectedEnter: bool = and(holds, enterChanged)
156
+
157
+ def left0: int = triple0(arrowA("vertical", 0 - STEP))
158
+ def left1: int = triple1(arrowA("vertical", 0 - STEP))
159
+ def left2: int = triple2(arrowA("vertical", 0 - STEP))
160
+ def leftChanged: bool = differs(left0, left1, left2)
161
+
162
+ def right0: int = triple0(arrowA("vertical", STEP))
163
+ def right1: int = triple1(arrowA("vertical", STEP))
164
+ def right2: int = triple2(arrowA("vertical", STEP))
165
+ def rightChanged: bool = differs(right0, right1, right2)
166
+
167
+ def up0: int = triple0(arrowA("horizontal", 0 - STEP))
168
+ def up1: int = triple1(arrowA("horizontal", 0 - STEP))
169
+ def up2: int = triple2(arrowA("horizontal", 0 - STEP))
170
+ def upChanged: bool = differs(up0, up1, up2)
171
+
172
+ def down0: int = triple0(arrowA("horizontal", STEP))
173
+ def down1: int = triple1(arrowA("horizontal", STEP))
174
+ def down2: int = triple2(arrowA("horizontal", STEP))
175
+ def downChanged: bool = differs(down0, down1, down2)
176
+
177
+ def home0: int = triple0(jumpA(0 - FAR))
178
+ def home1: int = triple1(jumpA(0 - FAR))
179
+ def home2: int = triple2(jumpA(0 - FAR))
180
+ def homeChanged: bool = differs(home0, home1, home2)
181
+
182
+ def end0: int = triple0(jumpA(FAR))
183
+ def end1: int = triple1(jumpA(FAR))
184
+ def end2: int = triple2(jumpA(FAR))
185
+ def endChanged: bool = differs(end0, end1, end2)
186
+
187
+ def dragNextD: int = ddNext(NOTCH)
188
+ def dragNext0: int = dragT0(dragNextD)
189
+ def dragNext1: int = dragT1(dragNextD)
190
+ def dragNext2: int = dragT2(dragNextD)
191
+ def dragNextChanged: bool = differs(dragNext0, dragNext1, dragNext2)
192
+
193
+ def dragPrevD: int = ddNext(0 - NOTCH)
194
+ def dragPrev0: int = dragT0(dragPrevD)
195
+ def dragPrev1: int = dragT1(dragPrevD)
196
+ def dragPrev2: int = dragT2(dragPrevD)
197
+ def dragPrevChanged: bool = differs(dragPrev0, dragPrev1, dragPrev2)
198
+
199
+ action init = all {
200
+ o' = "vertical",
201
+ s0' = INIT0,
202
+ s1' = INIT1,
203
+ s2' = INIT2,
204
+ prev0' = INIT0,
205
+ focus' = "none",
206
+ justRestored' = false,
207
+ rq0' = INIT0,
208
+ rq1' = INIT1,
209
+ rq2' = INIT2,
210
+ b0' = INIT0,
211
+ b1' = INIT1,
212
+ b2' = INIT2,
213
+ k0' = INIT0,
214
+ k1' = INIT1,
215
+ k2' = INIT2,
216
+ dd' = 0,
217
+ dr' = 0 - 1,
218
+ outcome' = "none",
219
+ controlled' = false,
220
+ accepting' = false,
221
+ requests' = 0
222
+ }
223
+
224
+ action focusSep0 = all {
225
+ o' = o,
226
+ s0' = s0,
227
+ s1' = s1,
228
+ s2' = s2,
229
+ prev0' = prev0,
230
+ focus' = "sep0",
231
+ justRestored' = false,
232
+ rq0' = rq0,
233
+ rq1' = rq1,
234
+ rq2' = rq2,
235
+ b0' = b0,
236
+ b1' = b1,
237
+ b2' = b2,
238
+ k0' = k0,
239
+ k1' = k1,
240
+ k2' = k2,
241
+ dd' = dd,
242
+ dr' = dr,
243
+ outcome' = outcome,
244
+ controlled' = controlled,
245
+ accepting' = accepting,
246
+ requests' = requests
247
+ }
248
+
249
+ action focusSep1 = all {
250
+ o' = o,
251
+ s0' = s0,
252
+ s1' = s1,
253
+ s2' = s2,
254
+ prev0' = prev0,
255
+ focus' = "sep1",
256
+ justRestored' = false,
257
+ rq0' = rq0,
258
+ rq1' = rq1,
259
+ rq2' = rq2,
260
+ b0' = b0,
261
+ b1' = b1,
262
+ b2' = b2,
263
+ k0' = k0,
264
+ k1' = k1,
265
+ k2' = k2,
266
+ dd' = dd,
267
+ dr' = dr,
268
+ outcome' = outcome,
269
+ controlled' = controlled,
270
+ accepting' = accepting,
271
+ requests' = requests
272
+ }
273
+
274
+ action blur = all {
275
+ o' = o,
276
+ s0' = s0,
277
+ s1' = s1,
278
+ s2' = s2,
279
+ prev0' = prev0,
280
+ focus' = "none",
281
+ justRestored' = false,
282
+ rq0' = rq0,
283
+ rq1' = rq1,
284
+ rq2' = rq2,
285
+ b0' = b0,
286
+ b1' = b1,
287
+ b2' = b2,
288
+ k0' = k0,
289
+ k1' = k1,
290
+ k2' = k2,
291
+ dd' = dd,
292
+ dr' = dr,
293
+ outcome' = outcome,
294
+ controlled' = controlled,
295
+ accepting' = accepting,
296
+ requests' = requests
297
+ }
298
+
299
+ action left = all {
300
+ o' = o,
301
+ s0' = landed(holds, s0, left0),
302
+ s1' = landed(holds, s1, left1),
303
+ s2' = landed(holds, s2, left2),
304
+ prev0' = prev0,
305
+ focus' = focus,
306
+ justRestored' = false,
307
+ rq0' = if (leftChanged) left0 else rq0,
308
+ rq1' = if (leftChanged) left1 else rq1,
309
+ rq2' = if (leftChanged) left2 else rq2,
310
+ b0' = if (leftChanged) s0 else b0,
311
+ b1' = if (leftChanged) s1 else b1,
312
+ b2' = if (leftChanged) s2 else b2,
313
+ k0' = k0,
314
+ k1' = k1,
315
+ k2' = k2,
316
+ dd' = dd,
317
+ dr' = dr,
318
+ outcome' = if (leftChanged) verdict else outcome,
319
+ controlled' = controlled,
320
+ accepting' = accepting,
321
+ requests' = requests + moved(left0, left1, left2)
322
+ }
323
+
324
+ action right = all {
325
+ o' = o,
326
+ s0' = landed(holds, s0, right0),
327
+ s1' = landed(holds, s1, right1),
328
+ s2' = landed(holds, s2, right2),
329
+ prev0' = prev0,
330
+ focus' = focus,
331
+ justRestored' = false,
332
+ rq0' = if (rightChanged) right0 else rq0,
333
+ rq1' = if (rightChanged) right1 else rq1,
334
+ rq2' = if (rightChanged) right2 else rq2,
335
+ b0' = if (rightChanged) s0 else b0,
336
+ b1' = if (rightChanged) s1 else b1,
337
+ b2' = if (rightChanged) s2 else b2,
338
+ k0' = k0,
339
+ k1' = k1,
340
+ k2' = k2,
341
+ dd' = dd,
342
+ dr' = dr,
343
+ outcome' = if (rightChanged) verdict else outcome,
344
+ controlled' = controlled,
345
+ accepting' = accepting,
346
+ requests' = requests + moved(right0, right1, right2)
347
+ }
348
+
349
+ action up = all {
350
+ o' = o,
351
+ s0' = landed(holds, s0, up0),
352
+ s1' = landed(holds, s1, up1),
353
+ s2' = landed(holds, s2, up2),
354
+ prev0' = prev0,
355
+ focus' = focus,
356
+ justRestored' = false,
357
+ rq0' = if (upChanged) up0 else rq0,
358
+ rq1' = if (upChanged) up1 else rq1,
359
+ rq2' = if (upChanged) up2 else rq2,
360
+ b0' = if (upChanged) s0 else b0,
361
+ b1' = if (upChanged) s1 else b1,
362
+ b2' = if (upChanged) s2 else b2,
363
+ k0' = k0,
364
+ k1' = k1,
365
+ k2' = k2,
366
+ dd' = dd,
367
+ dr' = dr,
368
+ outcome' = if (upChanged) verdict else outcome,
369
+ controlled' = controlled,
370
+ accepting' = accepting,
371
+ requests' = requests + moved(up0, up1, up2)
372
+ }
373
+
374
+ action down = all {
375
+ o' = o,
376
+ s0' = landed(holds, s0, down0),
377
+ s1' = landed(holds, s1, down1),
378
+ s2' = landed(holds, s2, down2),
379
+ prev0' = prev0,
380
+ focus' = focus,
381
+ justRestored' = false,
382
+ rq0' = if (downChanged) down0 else rq0,
383
+ rq1' = if (downChanged) down1 else rq1,
384
+ rq2' = if (downChanged) down2 else rq2,
385
+ b0' = if (downChanged) s0 else b0,
386
+ b1' = if (downChanged) s1 else b1,
387
+ b2' = if (downChanged) s2 else b2,
388
+ k0' = k0,
389
+ k1' = k1,
390
+ k2' = k2,
391
+ dd' = dd,
392
+ dr' = dr,
393
+ outcome' = if (downChanged) verdict else outcome,
394
+ controlled' = controlled,
395
+ accepting' = accepting,
396
+ requests' = requests + moved(down0, down1, down2)
397
+ }
398
+
399
+ action home = all {
400
+ o' = o,
401
+ s0' = landed(holds, s0, home0),
402
+ s1' = landed(holds, s1, home1),
403
+ s2' = landed(holds, s2, home2),
404
+ prev0' = prev0,
405
+ focus' = focus,
406
+ justRestored' = false,
407
+ rq0' = if (homeChanged) home0 else rq0,
408
+ rq1' = if (homeChanged) home1 else rq1,
409
+ rq2' = if (homeChanged) home2 else rq2,
410
+ b0' = if (homeChanged) s0 else b0,
411
+ b1' = if (homeChanged) s1 else b1,
412
+ b2' = if (homeChanged) s2 else b2,
413
+ k0' = k0,
414
+ k1' = k1,
415
+ k2' = k2,
416
+ dd' = dd,
417
+ dr' = dr,
418
+ outcome' = if (homeChanged) verdict else outcome,
419
+ controlled' = controlled,
420
+ accepting' = accepting,
421
+ requests' = requests + moved(home0, home1, home2)
422
+ }
423
+
424
+ action end = all {
425
+ o' = o,
426
+ s0' = landed(holds, s0, end0),
427
+ s1' = landed(holds, s1, end1),
428
+ s2' = landed(holds, s2, end2),
429
+ prev0' = prev0,
430
+ focus' = focus,
431
+ justRestored' = false,
432
+ rq0' = if (endChanged) end0 else rq0,
433
+ rq1' = if (endChanged) end1 else rq1,
434
+ rq2' = if (endChanged) end2 else rq2,
435
+ b0' = if (endChanged) s0 else b0,
436
+ b1' = if (endChanged) s1 else b1,
437
+ b2' = if (endChanged) s2 else b2,
438
+ k0' = k0,
439
+ k1' = k1,
440
+ k2' = k2,
441
+ dd' = dd,
442
+ dr' = dr,
443
+ outcome' = if (endChanged) verdict else outcome,
444
+ controlled' = controlled,
445
+ accepting' = accepting,
446
+ requests' = requests + moved(end0, end1, end2)
447
+ }
448
+
449
+ action enter = all {
450
+ o' = o,
451
+ s0' = landed(holds, s0, enterA),
452
+ s1' = landed(holds, s1, enterB),
453
+ s2' = landed(holds, s2, enterC),
454
+ prev0' = if (rejectedEnter) prev0 else (if (and(enterOn, not(collapsedNow))) s0 else prev0),
455
+ focus' = focus,
456
+ justRestored' = if (rejectedEnter) justRestored else (and(enterOn, collapsedNow)),
457
+ rq0' = if (enterChanged) enterA else rq0,
458
+ rq1' = if (enterChanged) enterB else rq1,
459
+ rq2' = if (enterChanged) enterC else rq2,
460
+ b0' = if (enterChanged) s0 else b0,
461
+ b1' = if (enterChanged) s1 else b1,
462
+ b2' = if (enterChanged) s2 else b2,
463
+ k0' = k0,
464
+ k1' = k1,
465
+ k2' = k2,
466
+ dd' = dd,
467
+ dr' = dr,
468
+ outcome' = if (enterChanged) verdict else outcome,
469
+ controlled' = controlled,
470
+ accepting' = accepting,
471
+ requests' = requests + moved(enterA, enterB, enterC)
472
+ }
473
+
474
+ action orientVertical = all {
475
+ o' = "vertical",
476
+ s0' = s0,
477
+ s1' = s1,
478
+ s2' = s2,
479
+ prev0' = prev0,
480
+ focus' = focus,
481
+ justRestored' = false,
482
+ rq0' = rq0,
483
+ rq1' = rq1,
484
+ rq2' = rq2,
485
+ b0' = b0,
486
+ b1' = b1,
487
+ b2' = b2,
488
+ k0' = k0,
489
+ k1' = k1,
490
+ k2' = k2,
491
+ dd' = dd,
492
+ dr' = dr,
493
+ outcome' = outcome,
494
+ controlled' = controlled,
495
+ accepting' = accepting,
496
+ requests' = requests
497
+ }
498
+
499
+ action orientHorizontal = all {
500
+ o' = "horizontal",
501
+ s0' = s0,
502
+ s1' = s1,
503
+ s2' = s2,
504
+ prev0' = prev0,
505
+ focus' = focus,
506
+ justRestored' = false,
507
+ rq0' = rq0,
508
+ rq1' = rq1,
509
+ rq2' = rq2,
510
+ b0' = b0,
511
+ b1' = b1,
512
+ b2' = b2,
513
+ k0' = k0,
514
+ k1' = k1,
515
+ k2' = k2,
516
+ dd' = dd,
517
+ dr' = dr,
518
+ outcome' = outcome,
519
+ controlled' = controlled,
520
+ accepting' = accepting,
521
+ requests' = requests
522
+ }
523
+
524
+ action dragDown0 = all {
525
+ o' = o,
526
+ s0' = s0,
527
+ s1' = s1,
528
+ s2' = s2,
529
+ prev0' = prev0,
530
+ focus' = "sep0",
531
+ justRestored' = false,
532
+ rq0' = rq0,
533
+ rq1' = rq1,
534
+ rq2' = rq2,
535
+ b0' = b0,
536
+ b1' = b1,
537
+ b2' = b2,
538
+ k0' = if (isCollapsed(0)) k0 else s0,
539
+ k1' = if (isCollapsed(0)) k1 else s1,
540
+ k2' = if (isCollapsed(0)) k2 else s2,
541
+ dd' = if (isCollapsed(0)) dd else 0,
542
+ dr' = if (isCollapsed(0)) dr else 0,
543
+ outcome' = outcome,
544
+ controlled' = controlled,
545
+ accepting' = accepting,
546
+ requests' = requests
547
+ }
548
+
549
+ action dragDown1 = all {
550
+ o' = o,
551
+ s0' = s0,
552
+ s1' = s1,
553
+ s2' = s2,
554
+ prev0' = prev0,
555
+ focus' = "sep1",
556
+ justRestored' = false,
557
+ rq0' = rq0,
558
+ rq1' = rq1,
559
+ rq2' = rq2,
560
+ b0' = b0,
561
+ b1' = b1,
562
+ b2' = b2,
563
+ k0' = if (isCollapsed(1)) k0 else s0,
564
+ k1' = if (isCollapsed(1)) k1 else s1,
565
+ k2' = if (isCollapsed(1)) k2 else s2,
566
+ dd' = if (isCollapsed(1)) dd else 0,
567
+ dr' = if (isCollapsed(1)) dr else 1,
568
+ outcome' = outcome,
569
+ controlled' = controlled,
570
+ accepting' = accepting,
571
+ requests' = requests
572
+ }
573
+
574
+ action dragNext = all {
575
+ o' = o,
576
+ s0' = landed(holds, s0, dragNext0),
577
+ s1' = landed(holds, s1, dragNext1),
578
+ s2' = landed(holds, s2, dragNext2),
579
+ prev0' = prev0,
580
+ focus' = focus,
581
+ justRestored' = false,
582
+ rq0' = if (dragNextChanged) dragNext0 else rq0,
583
+ rq1' = if (dragNextChanged) dragNext1 else rq1,
584
+ rq2' = if (dragNextChanged) dragNext2 else rq2,
585
+ b0' = if (dragNextChanged) s0 else b0,
586
+ b1' = if (dragNextChanged) s1 else b1,
587
+ b2' = if (dragNextChanged) s2 else b2,
588
+ k0' = k0,
589
+ k1' = k1,
590
+ k2' = k2,
591
+ dd' = dragNextD,
592
+ dr' = dr,
593
+ outcome' = if (dragNextChanged) verdict else outcome,
594
+ controlled' = controlled,
595
+ accepting' = accepting,
596
+ requests' = requests + moved(dragNext0, dragNext1, dragNext2)
597
+ }
598
+
599
+ action dragPrev = all {
600
+ o' = o,
601
+ s0' = landed(holds, s0, dragPrev0),
602
+ s1' = landed(holds, s1, dragPrev1),
603
+ s2' = landed(holds, s2, dragPrev2),
604
+ prev0' = prev0,
605
+ focus' = focus,
606
+ justRestored' = false,
607
+ rq0' = if (dragPrevChanged) dragPrev0 else rq0,
608
+ rq1' = if (dragPrevChanged) dragPrev1 else rq1,
609
+ rq2' = if (dragPrevChanged) dragPrev2 else rq2,
610
+ b0' = if (dragPrevChanged) s0 else b0,
611
+ b1' = if (dragPrevChanged) s1 else b1,
612
+ b2' = if (dragPrevChanged) s2 else b2,
613
+ k0' = k0,
614
+ k1' = k1,
615
+ k2' = k2,
616
+ dd' = dragPrevD,
617
+ dr' = dr,
618
+ outcome' = if (dragPrevChanged) verdict else outcome,
619
+ controlled' = controlled,
620
+ accepting' = accepting,
621
+ requests' = requests + moved(dragPrev0, dragPrev1, dragPrev2)
622
+ }
623
+
624
+ action dragUp = all {
625
+ o' = o,
626
+ s0' = s0,
627
+ s1' = s1,
628
+ s2' = s2,
629
+ prev0' = prev0,
630
+ focus' = focus,
631
+ justRestored' = false,
632
+ rq0' = rq0,
633
+ rq1' = rq1,
634
+ rq2' = rq2,
635
+ b0' = b0,
636
+ b1' = b1,
637
+ b2' = b2,
638
+ k0' = k0,
639
+ k1' = k1,
640
+ k2' = k2,
641
+ dd' = 0,
642
+ dr' = 0 - 1,
643
+ outcome' = outcome,
644
+ controlled' = controlled,
645
+ accepting' = accepting,
646
+ requests' = requests
647
+ }
648
+
649
+ action hostEqual = all {
650
+ o' = o,
651
+ s0' = if (controlled) 30 else s0,
652
+ s1' = if (controlled) 30 else s1,
653
+ s2' = if (controlled) 40 else s2,
654
+ prev0' = if (controlled) (if (30 == ZERO) (if (s0 == ZERO) prev0 else s0) else 30) else prev0,
655
+ focus' = focus,
656
+ justRestored' = if (controlled) false else justRestored,
657
+ rq0' = rq0,
658
+ rq1' = rq1,
659
+ rq2' = rq2,
660
+ b0' = b0,
661
+ b1' = b1,
662
+ b2' = b2,
663
+ k0' = k0,
664
+ k1' = k1,
665
+ k2' = k2,
666
+ dd' = if (controlled) 0 else dd,
667
+ dr' = if (controlled) 0 - 1 else dr,
668
+ outcome' = if (controlled) "none" else outcome,
669
+ controlled' = controlled,
670
+ accepting' = accepting,
671
+ requests' = requests
672
+ }
673
+
674
+ action hostWide = all {
675
+ o' = o,
676
+ s0' = if (controlled) 50 else s0,
677
+ s1' = if (controlled) 30 else s1,
678
+ s2' = if (controlled) 20 else s2,
679
+ prev0' = if (controlled) (if (50 == ZERO) (if (s0 == ZERO) prev0 else s0) else 50) else prev0,
680
+ focus' = focus,
681
+ justRestored' = if (controlled) false else justRestored,
682
+ rq0' = rq0,
683
+ rq1' = rq1,
684
+ rq2' = rq2,
685
+ b0' = b0,
686
+ b1' = b1,
687
+ b2' = b2,
688
+ k0' = k0,
689
+ k1' = k1,
690
+ k2' = k2,
691
+ dd' = if (controlled) 0 else dd,
692
+ dr' = if (controlled) 0 - 1 else dr,
693
+ outcome' = if (controlled) "none" else outcome,
694
+ controlled' = controlled,
695
+ accepting' = accepting,
696
+ requests' = requests
697
+ }
698
+
699
+ action hostNarrow = all {
700
+ o' = o,
701
+ s0' = if (controlled) 10 else s0,
702
+ s1' = if (controlled) 40 else s1,
703
+ s2' = if (controlled) 50 else s2,
704
+ prev0' = if (controlled) (if (10 == ZERO) (if (s0 == ZERO) prev0 else s0) else 10) else prev0,
705
+ focus' = focus,
706
+ justRestored' = if (controlled) false else justRestored,
707
+ rq0' = rq0,
708
+ rq1' = rq1,
709
+ rq2' = rq2,
710
+ b0' = b0,
711
+ b1' = b1,
712
+ b2' = b2,
713
+ k0' = k0,
714
+ k1' = k1,
715
+ k2' = k2,
716
+ dd' = if (controlled) 0 else dd,
717
+ dr' = if (controlled) 0 - 1 else dr,
718
+ outcome' = if (controlled) "none" else outcome,
719
+ controlled' = controlled,
720
+ accepting' = accepting,
721
+ requests' = requests
722
+ }
723
+
724
+ action hostCollapsed = all {
725
+ o' = o,
726
+ s0' = if (controlled) ZERO else s0,
727
+ s1' = if (controlled) 50 else s1,
728
+ s2' = if (controlled) 50 else s2,
729
+ prev0' = if (controlled) (if (ZERO == ZERO) (if (s0 == ZERO) prev0 else s0) else ZERO) else prev0,
730
+ focus' = focus,
731
+ justRestored' = if (controlled) false else justRestored,
732
+ rq0' = rq0,
733
+ rq1' = rq1,
734
+ rq2' = rq2,
735
+ b0' = b0,
736
+ b1' = b1,
737
+ b2' = b2,
738
+ k0' = k0,
739
+ k1' = k1,
740
+ k2' = k2,
741
+ dd' = if (controlled) 0 else dd,
742
+ dr' = if (controlled) 0 - 1 else dr,
743
+ outcome' = if (controlled) "none" else outcome,
744
+ controlled' = controlled,
745
+ accepting' = accepting,
746
+ requests' = requests
747
+ }
748
+
749
+ action control = all {
750
+ o' = o,
751
+ s0' = s0,
752
+ s1' = s1,
753
+ s2' = s2,
754
+ prev0' = prev0,
755
+ focus' = focus,
756
+ justRestored' = false,
757
+ rq0' = rq0,
758
+ rq1' = rq1,
759
+ rq2' = rq2,
760
+ b0' = b0,
761
+ b1' = b1,
762
+ b2' = b2,
763
+ k0' = k0,
764
+ k1' = k1,
765
+ k2' = k2,
766
+ dd' = dd,
767
+ dr' = dr,
768
+ outcome' = "none",
769
+ controlled' = true,
770
+ accepting' = accepting,
771
+ requests' = requests
772
+ }
773
+
774
+ action acceptRequests = all {
775
+ o' = o,
776
+ s0' = s0,
777
+ s1' = s1,
778
+ s2' = s2,
779
+ prev0' = prev0,
780
+ focus' = focus,
781
+ justRestored' = false,
782
+ rq0' = rq0,
783
+ rq1' = rq1,
784
+ rq2' = rq2,
785
+ b0' = b0,
786
+ b1' = b1,
787
+ b2' = b2,
788
+ k0' = k0,
789
+ k1' = k1,
790
+ k2' = k2,
791
+ dd' = dd,
792
+ dr' = dr,
793
+ outcome' = "none",
794
+ controlled' = controlled,
795
+ accepting' = true,
796
+ requests' = requests
797
+ }
798
+
799
+ action step = any {
800
+ init, focusSep0, focusSep1, blur, left, right, up, down, home, end, enter, orientVertical, orientHorizontal, dragDown0, dragDown1, dragNext, dragPrev, dragUp, hostEqual, hostWide, hostNarrow, hostCollapsed, control, acceptRequests
801
+ }
802
+
803
+ // the three panes are a layout: percentages always add up to 100
804
+ val PaneSizesSumTo100: bool = s0 + s1 + s2 == FULL
805
+ // every pane is inside its own bounds; only pane 0 can sit at collapsedSize
806
+ val PaneInBounds: bool =
807
+ and(s0 >= ZERO, and(s0 <= MAX, and(s1 >= MIN, and(s1 <= MAX, and(s2 >= MIN, s2 <= MAX)))))
808
+ // the remembered size is always a size a pane can have
809
+ val PrevInBounds: bool = and(prev0 >= MIN, prev0 <= MAX)
810
+ // restoring returns pane 0 to exactly the remembered size
811
+ val RestoreReturnsPrev: bool = or(not(justRestored), s0 == prev0)
812
+ // focus is one of the modelled separators
813
+ val FocusKnown: bool = or(focus == "none", or(focus == "sep0", focus == "sep1"))
814
+ // orientation is one of the two APG orientations
815
+ val OrientationKnown: bool = ariaOrientationKnown(o)
816
+ // callbacks only ever count up from zero
817
+ val RequestsNonNegative: bool = requests >= 0
818
+ // a holding host never sees an accepted request: nothing commits
819
+ val HoldingLayoutStable: bool = or(not(holds), not(outcome == "accepted"))
820
+ // a rejected request leaves the layout as it found it
821
+ val RejectedRequestUntouched: bool =
822
+ or(not(outcome == "rejected"), and(s0 == b0, and(s1 == b1, s2 == b2)))
823
+ // an accepted request committed exactly its target
824
+ val AcceptedRequestCommits: bool =
825
+ or(not(outcome == "accepted"), and(s0 == rq0, and(s1 == rq1, s2 == rq2)))
826
+ // a drag only ever moves the two panes around the dragged separator
827
+ val DragMovesOnlyNeighbours: bool =
828
+ and(or(not(dr == 0), s2 == k2), or(not(dr == 1), s0 == k0))
829
+ // the drag delta stays bounded (the layout is clamped, the delta is not the state)
830
+ val DragDeltaBounded: bool = and(dd >= 0 - DMAX, dd <= DMAX)
831
+ }