@cssints/apg-specs 0.0.0-stage → 0.5.2
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.
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/apg/accordion.bridge.d.mts +35 -0
- package/dist/apg/accordion.bridge.mjs +326 -0
- package/dist/apg/accordion.model.json +8397 -0
- package/dist/apg/alert.bridge.d.mts +22 -0
- package/dist/apg/alert.bridge.mjs +86 -0
- package/dist/apg/alert.model.json +871 -0
- package/dist/apg/alertdialog.bridge.d.mts +21 -0
- package/dist/apg/alertdialog.bridge.mjs +221 -0
- package/dist/apg/alertdialog.model.json +5232 -0
- package/dist/apg/breadcrumb.bridge.d.mts +26 -0
- package/dist/apg/breadcrumb.bridge.mjs +120 -0
- package/dist/apg/breadcrumb.model.json +574 -0
- package/dist/apg/button.bridge.d.mts +35 -0
- package/dist/apg/button.bridge.mjs +208 -0
- package/dist/apg/button.model.json +5814 -0
- package/dist/apg/carousel.bridge.d.mts +44 -0
- package/dist/apg/carousel.bridge.mjs +262 -0
- package/dist/apg/carousel.model.json +8963 -0
- package/dist/apg/checkbox-group.bridge.d.mts +25 -0
- package/dist/apg/checkbox-group.bridge.mjs +276 -0
- package/dist/apg/checkbox-group.model.json +3863 -0
- package/dist/apg/checkbox.bridge.d.mts +34 -0
- package/dist/apg/checkbox.bridge.mjs +253 -0
- package/dist/apg/checkbox.model.json +3158 -0
- package/dist/apg/combobox-autocomplete.bridge.d.mts +14 -0
- package/dist/apg/combobox-autocomplete.bridge.mjs +326 -0
- package/dist/apg/combobox-autocomplete.model.json +11586 -0
- package/dist/apg/combobox-dialog.bridge.d.mts +17 -0
- package/dist/apg/combobox-dialog.bridge.mjs +250 -0
- package/dist/apg/combobox-dialog.model.json +8981 -0
- package/dist/apg/combobox-grid.bridge.d.mts +17 -0
- package/dist/apg/combobox-grid.bridge.mjs +227 -0
- package/dist/apg/combobox-grid.model.json +8554 -0
- package/dist/apg/combobox-select-only.bridge.d.mts +24 -0
- package/dist/apg/combobox-select-only.bridge.mjs +304 -0
- package/dist/apg/combobox-select-only.model.json +7846 -0
- package/dist/apg/combobox-tree.bridge.d.mts +17 -0
- package/dist/apg/combobox-tree.bridge.mjs +211 -0
- package/dist/apg/combobox-tree.model.json +9345 -0
- package/dist/apg/dialog-modal.bridge.d.mts +35 -0
- package/dist/apg/dialog-modal.bridge.mjs +186 -0
- package/dist/apg/dialog-modal.model.json +7580 -0
- package/dist/apg/disclosure.bridge.d.mts +25 -0
- package/dist/apg/disclosure.bridge.mjs +202 -0
- package/dist/apg/disclosure.model.json +4703 -0
- package/dist/apg/feed.bridge.d.mts +31 -0
- package/dist/apg/feed.bridge.mjs +246 -0
- package/dist/apg/feed.model.json +4523 -0
- package/dist/apg/grid-selection.bridge.d.mts +13 -0
- package/dist/apg/grid-selection.bridge.mjs +160 -0
- package/dist/apg/grid-selection.model.json +6974 -0
- package/dist/apg/grid-sort.bridge.d.mts +12 -0
- package/dist/apg/grid-sort.bridge.mjs +138 -0
- package/dist/apg/grid-sort.model.json +3799 -0
- package/dist/apg/grid.bridge.d.mts +54 -0
- package/dist/apg/grid.bridge.mjs +441 -0
- package/dist/apg/grid.model.json +3802 -0
- package/dist/apg/link.bridge.d.mts +24 -0
- package/dist/apg/link.bridge.mjs +126 -0
- package/dist/apg/link.model.json +694 -0
- package/dist/apg/listbox-multi.bridge.d.mts +10 -0
- package/dist/apg/listbox-multi.bridge.mjs +261 -0
- package/dist/apg/listbox-multi.model.json +10893 -0
- package/dist/apg/listbox-rearrangeable.bridge.d.mts +20 -0
- package/dist/apg/listbox-rearrangeable.bridge.mjs +237 -0
- package/dist/apg/listbox-rearrangeable.model.json +17592 -0
- package/dist/apg/listbox-shared.d.mts +58 -0
- package/dist/apg/listbox-shared.mjs +171 -0
- package/dist/apg/listbox-single.bridge.d.mts +10 -0
- package/dist/apg/listbox-single.bridge.mjs +196 -0
- package/dist/apg/listbox-single.model.json +8024 -0
- package/dist/apg/listbox-typeahead.bridge.d.mts +10 -0
- package/dist/apg/listbox-typeahead.bridge.mjs +153 -0
- package/dist/apg/listbox-typeahead.model.json +5615 -0
- package/dist/apg/menu-button.bridge.d.mts +29 -0
- package/dist/apg/menu-button.bridge.mjs +259 -0
- package/dist/apg/menu-button.model.json +5706 -0
- package/dist/apg/menu-checkable.bridge.d.mts +25 -0
- package/dist/apg/menu-checkable.bridge.mjs +226 -0
- package/dist/apg/menu-checkable.model.json +4277 -0
- package/dist/apg/menu-menubar.bridge.d.mts +27 -0
- package/dist/apg/menu-menubar.bridge.mjs +273 -0
- package/dist/apg/menu-menubar.model.json +9540 -0
- package/dist/apg/meter.bridge.d.mts +28 -0
- package/dist/apg/meter.bridge.mjs +159 -0
- package/dist/apg/meter.model.json +1185 -0
- package/dist/apg/player.bridge.d.mts +24 -0
- package/dist/apg/player.bridge.mjs +342 -0
- package/dist/apg/player.model.json +11013 -0
- package/dist/apg/radio.bridge.d.mts +35 -0
- package/dist/apg/radio.bridge.mjs +371 -0
- package/dist/apg/radio.model.json +5011 -0
- package/dist/apg/slider-multithumb.bridge.d.mts +34 -0
- package/dist/apg/slider-multithumb.bridge.mjs +365 -0
- package/dist/apg/slider-multithumb.model.json +5774 -0
- package/dist/apg/slider.bridge.d.mts +54 -0
- package/dist/apg/slider.bridge.mjs +359 -0
- package/dist/apg/slider.model.json +4881 -0
- package/dist/apg/spinbutton.bridge.d.mts +41 -0
- package/dist/apg/spinbutton.bridge.mjs +327 -0
- package/dist/apg/spinbutton.model.json +7571 -0
- package/dist/apg/switch.bridge.d.mts +30 -0
- package/dist/apg/switch.bridge.mjs +243 -0
- package/dist/apg/switch.model.json +3159 -0
- package/dist/apg/tabs.bridge.d.mts +41 -0
- package/dist/apg/tabs.bridge.mjs +294 -0
- package/dist/apg/tabs.model.json +7659 -0
- package/dist/apg/timeline.bridge.d.mts +36 -0
- package/dist/apg/timeline.bridge.mjs +265 -0
- package/dist/apg/timeline.model.json +6189 -0
- package/dist/apg/toolbar.bridge.d.mts +25 -0
- package/dist/apg/toolbar.bridge.mjs +307 -0
- package/dist/apg/toolbar.model.json +6616 -0
- package/dist/apg/tooltip.bridge.d.mts +36 -0
- package/dist/apg/tooltip.bridge.mjs +208 -0
- package/dist/apg/tooltip.model.json +8295 -0
- package/dist/apg/treegrid.bridge.d.mts +28 -0
- package/dist/apg/treegrid.bridge.mjs +311 -0
- package/dist/apg/treegrid.model.json +17358 -0
- package/dist/apg/treeview.bridge.d.mts +26 -0
- package/dist/apg/treeview.bridge.mjs +396 -0
- package/dist/apg/treeview.model.json +13745 -0
- package/dist/apg/windowsplitter.bridge.d.mts +36 -0
- package/dist/apg/windowsplitter.bridge.mjs +242 -0
- package/dist/apg/windowsplitter.model.json +6253 -0
- package/dist/canvas/camera.model.json +6568 -0
- package/dist/canvas/canvas.bridge.d.mts +190 -0
- package/dist/canvas/canvas.bridge.mjs +729 -0
- package/dist/canvas/pointer.model.json +17095 -0
- package/dist/scenario.d.mts +233 -0
- package/dist/scenario.mjs +313 -0
- package/package.json +57 -4
- package/scenarios/apg/README.md +221 -0
- package/scenarios/apg/accordion.base-ui.json +1005 -0
- package/scenarios/apg/accordion.react-aria.json +929 -0
- package/scenarios/apg/alert.base-ui.json +81 -0
- package/scenarios/apg/alert.react-aria.json +98 -0
- package/scenarios/apg/alertdialog.base-ui.json +331 -0
- package/scenarios/apg/alertdialog.react-aria.json +225 -0
- package/scenarios/apg/breadcrumb.react-aria.json +312 -0
- package/scenarios/apg/button.ariakit.json +136 -0
- package/scenarios/apg/button.base-ui.json +747 -0
- package/scenarios/apg/button.react-aria.json +443 -0
- package/scenarios/apg/carousel.apg.json +909 -0
- package/scenarios/apg/checkbox.ariakit.json +299 -0
- package/scenarios/apg/checkbox.base-ui.json +1126 -0
- package/scenarios/apg/checkbox.react-aria.json +518 -0
- package/scenarios/apg/combobox.ariakit.json +945 -0
- package/scenarios/apg/combobox.base-ui.json +2444 -0
- package/scenarios/apg/combobox.react-aria.json +845 -0
- package/scenarios/apg/dialog-modal.ariakit.json +1102 -0
- package/scenarios/apg/dialog-modal.base-ui.json +775 -0
- package/scenarios/apg/dialog-modal.react-aria.json +638 -0
- package/scenarios/apg/disclosure.apg.json +349 -0
- package/scenarios/apg/disclosure.ariakit.json +163 -0
- package/scenarios/apg/disclosure.base-ui.json +451 -0
- package/scenarios/apg/disclosure.react-aria.json +643 -0
- package/scenarios/apg/feed.apg.json +442 -0
- package/scenarios/apg/grid.ariakit.json +197 -0
- package/scenarios/apg/grid.base-ui.json +720 -0
- package/scenarios/apg/grid.react-aria.json +969 -0
- package/scenarios/apg/landmarks.ariakit.json +93 -0
- package/scenarios/apg/landmarks.react-aria.json +330 -0
- package/scenarios/apg/link.ariakit.json +83 -0
- package/scenarios/apg/link.base-ui.json +88 -0
- package/scenarios/apg/link.react-aria.json +186 -0
- package/scenarios/apg/listbox.ariakit.json +78 -0
- package/scenarios/apg/listbox.base-ui.json +163 -0
- package/scenarios/apg/listbox.react-aria.json +1118 -0
- package/scenarios/apg/menu-button.ariakit.json +924 -0
- package/scenarios/apg/menu-button.base-ui.json +1155 -0
- package/scenarios/apg/menu-button.react-aria.json +1080 -0
- package/scenarios/apg/menubar.ariakit.json +758 -0
- package/scenarios/apg/menubar.base-ui.json +673 -0
- package/scenarios/apg/meter.base-ui.json +243 -0
- package/scenarios/apg/meter.react-aria.json +201 -0
- package/scenarios/apg/radio.ariakit.json +459 -0
- package/scenarios/apg/radio.base-ui.json +581 -0
- package/scenarios/apg/radio.react-aria.json +576 -0
- package/scenarios/apg/slider-multithumb.base-ui.json +316 -0
- package/scenarios/apg/slider-multithumb.react-aria.json +363 -0
- package/scenarios/apg/slider.base-ui.json +544 -0
- package/scenarios/apg/slider.react-aria.json +456 -0
- package/scenarios/apg/spinbutton.base-ui.json +1688 -0
- package/scenarios/apg/spinbutton.react-aria.json +426 -0
- package/scenarios/apg/switch.base-ui.json +456 -0
- package/scenarios/apg/switch.react-aria.json +303 -0
- package/scenarios/apg/table.ariakit.json +65 -0
- package/scenarios/apg/table.react-aria.json +221 -0
- package/scenarios/apg/tabs.ariakit.json +329 -0
- package/scenarios/apg/tabs.base-ui.json +875 -0
- package/scenarios/apg/tabs.react-aria.json +644 -0
- package/scenarios/apg/toolbar.ariakit.json +595 -0
- package/scenarios/apg/toolbar.base-ui.json +1189 -0
- package/scenarios/apg/toolbar.react-aria.json +383 -0
- package/scenarios/apg/tooltip.ariakit.json +441 -0
- package/scenarios/apg/tooltip.base-ui.json +411 -0
- package/scenarios/apg/tooltip.react-aria.json +454 -0
- package/scenarios/apg/treegrid.apg.json +2558 -0
- package/scenarios/apg/treegrid.react-aria.json +5094 -0
- package/scenarios/apg/treeview.apg.json +928 -0
- package/scenarios/apg/windowsplitter.apg.json +557 -0
- package/scenarios/canvas/README.md +69 -0
- package/scenarios/canvas/camera.contract.json +237 -0
- package/scenarios/canvas/camera.excalidraw.json +57 -0
- package/scenarios/canvas/marquee.excalidraw.json +122 -0
- package/scenarios/canvas/pan.excalidraw.json +46 -0
- package/scenarios/canvas/pointer.contract.json +528 -0
- package/scenarios/parts.json +291 -0
- package/shared/aria.qnt +186 -0
- package/shared/details-group.qnt +50 -0
- package/shared/focusgroup.qnt +152 -0
- package/shared/invokers.qnt +81 -0
- package/shared/popover.qnt +71 -0
- package/specs/apg/STATIC.md +103 -0
- package/specs/apg/accordion.qnt +578 -0
- package/specs/apg/alert.qnt +131 -0
- package/specs/apg/alertdialog.qnt +455 -0
- package/specs/apg/breadcrumb.qnt +88 -0
- package/specs/apg/button.qnt +445 -0
- package/specs/apg/carousel.qnt +732 -0
- package/specs/apg/checkbox-group.qnt +388 -0
- package/specs/apg/checkbox.qnt +394 -0
- package/specs/apg/combobox-autocomplete.qnt +764 -0
- package/specs/apg/combobox-dialog.qnt +627 -0
- package/specs/apg/combobox-grid.qnt +632 -0
- package/specs/apg/combobox-select-only.qnt +626 -0
- package/specs/apg/combobox-tree.qnt +633 -0
- package/specs/apg/dialog-modal.qnt +499 -0
- package/specs/apg/disclosure.qnt +309 -0
- package/specs/apg/feed.qnt +485 -0
- package/specs/apg/grid-selection.qnt +512 -0
- package/specs/apg/grid-sort.qnt +330 -0
- package/specs/apg/grid.qnt +360 -0
- package/specs/apg/link.qnt +119 -0
- package/specs/apg/listbox-multi.qnt +799 -0
- package/specs/apg/listbox-rearrangeable.qnt +801 -0
- package/specs/apg/listbox-single.qnt +511 -0
- package/specs/apg/listbox-typeahead.qnt +396 -0
- package/specs/apg/menu-button.qnt +363 -0
- package/specs/apg/menu-checkable.qnt +304 -0
- package/specs/apg/menu-menubar.qnt +531 -0
- package/specs/apg/meter.qnt +129 -0
- package/specs/apg/player.qnt +1081 -0
- package/specs/apg/radio.qnt +394 -0
- package/specs/apg/slider-multithumb.qnt +441 -0
- package/specs/apg/slider.qnt +396 -0
- package/specs/apg/spinbutton.qnt +494 -0
- package/specs/apg/switch.qnt +389 -0
- package/specs/apg/tabs.qnt +412 -0
- package/specs/apg/timeline.qnt +612 -0
- package/specs/apg/toolbar.qnt +364 -0
- package/specs/apg/tooltip.qnt +553 -0
- package/specs/apg/treegrid.qnt +983 -0
- package/specs/apg/treeview.qnt +1004 -0
- package/specs/apg/windowsplitter.qnt +487 -0
- package/specs/canvas/camera.qnt +407 -0
- package/specs/canvas/pointer.qnt +841 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
// Span timeline / waterfall — NOT an APG pattern.
|
|
2
|
+
//
|
|
3
|
+
// The o11y dashboard lays spans out on a shared time axis: nesting/indentation,
|
|
4
|
+
// a duration ruler, selection, per-parent collapse and a zoom window over the
|
|
5
|
+
// visible tick domain. WAI-ARIA APG has no waterfall pattern, so this is a new
|
|
6
|
+
// behaviour-machine modelled on the treeview spec's encoding:
|
|
7
|
+
//
|
|
8
|
+
// * the span tree is a fixed constant, ids are pre-order = document order;
|
|
9
|
+
// * `expanded` is a bitmask over node ids, `selected` is a node id (0 = none),
|
|
10
|
+
// `focus` is the roving-tabindex node and `hasFocus` says whether DOM focus
|
|
11
|
+
// is inside the timeline. Key events are no-ops while `hasFocus` is false.
|
|
12
|
+
// * the zoom window is `viewStart`/`viewEnd`, a sub-interval of the fixed
|
|
13
|
+
// tick domain 0..100. zoomIn shrinks it around its centre (no-op at the
|
|
14
|
+
// minimum width), zoomOut grows it around its centre clamped to the domain
|
|
15
|
+
// (no-op at full width), pan shifts it by a quarter of its width clamped
|
|
16
|
+
// in-domain, zoomReset restores the full domain.
|
|
17
|
+
//
|
|
18
|
+
// Request vs commit (map ticket rt1p). Selection is the value. Every event a
|
|
19
|
+
// user sees as a selection — `click`, `pressEnter` and the programmatic
|
|
20
|
+
// `select` — is a *request* carrying an intent (`requested`); it fires
|
|
21
|
+
// onValueChange (`requests` counts the calls) when the intent differs from the
|
|
22
|
+
// current `selected`, and then:
|
|
23
|
+
// uncontrolled the request is accepted: selection commits the intent
|
|
24
|
+
// controlled, holds the host ignores it ("rejected"): selection is untouched
|
|
25
|
+
// controlled, accepts the host answers with the same value (`accepting`): the
|
|
26
|
+
// selection commits through the controlled value, not the event
|
|
27
|
+
// `valueProp` is the only thing that changes selection while `controlled`;
|
|
28
|
+
// `setValue` is the host writing it. `control` / `acceptRequests` are
|
|
29
|
+
// configuration events (init is literal-only). Focus/expansion and the zoom
|
|
30
|
+
// window are not the controlled value: they keep moving on a rejected request,
|
|
31
|
+
// like treeview's roving focus. Invariants: ControlledFollowsProp,
|
|
32
|
+
// RejectedRequestUntouched, AcceptedRequestCommits.
|
|
33
|
+
//
|
|
34
|
+
// Span tree (fixed; start/end are fractions of the parent span):
|
|
35
|
+
//
|
|
36
|
+
// 1 root level 1, parent 0..1
|
|
37
|
+
// 2 child level 2, parent 0..0.6
|
|
38
|
+
// 3 leaf level 3, parent 0..0.3
|
|
39
|
+
// 4 leaf level 3, parent 0.3..0.6
|
|
40
|
+
// 5 leaf level 2, parent 0.6..1
|
|
41
|
+
//
|
|
42
|
+
// Encoding (the generator only supports str/int/bool vars): `expanded` is a
|
|
43
|
+
// bitmask over nodes 1..5 (bit n-1, weight 2^(n-1)); `selected` is the selected
|
|
44
|
+
// node id (0 = none); `focus` is the roving node (always 1..5, even while the
|
|
45
|
+
// timeline is not focused). This mirrors treeview.qnt but uses a selected id
|
|
46
|
+
// instead of a selection bitmask — a waterfall selects a single span.
|
|
47
|
+
//
|
|
48
|
+
// Deliberate omissions (ponytail): fractional/overlapping spans and lane
|
|
49
|
+
// packing (layout is the consumer's), drag/range selection, virtualisation,
|
|
50
|
+
// type-ahead, multi-select, and aria-activedescendant (the rows use roving
|
|
51
|
+
// tabindex). The visibility/navigation defs are shared with treeview's shape.
|
|
52
|
+
module apg_timeline {
|
|
53
|
+
var expanded: int // bitmask: parent nodes whose children are shown
|
|
54
|
+
var selected: int // selected node id, 0 = none
|
|
55
|
+
var focus: int // roving-tabindex node, 1..5
|
|
56
|
+
var hasFocus: bool // DOM focus is inside the timeline
|
|
57
|
+
var viewStart: int // zoom window start tick, 0..100
|
|
58
|
+
var viewEnd: int // zoom window end tick, 0..100
|
|
59
|
+
var controlled: bool // the host owns the selection (`value` prop present)
|
|
60
|
+
var accepting: bool // controlled host that answers every request with its intent
|
|
61
|
+
var valueProp: int // the controlled value prop (meaningful while controlled)
|
|
62
|
+
var requested: int // intent of the last request
|
|
63
|
+
var requests: int // number of requests that fired onValueChange
|
|
64
|
+
var outcome: str // "none" | "accepted" | "rejected"
|
|
65
|
+
var base: int // selection when the last request was received
|
|
66
|
+
|
|
67
|
+
// ---- the constant span tree ---------------------------------------------
|
|
68
|
+
|
|
69
|
+
val lastNode: int = 5
|
|
70
|
+
val allMask: int = 31
|
|
71
|
+
val domainMin: int = 0
|
|
72
|
+
val domainMax: int = 100
|
|
73
|
+
val minWidth: int = 10
|
|
74
|
+
|
|
75
|
+
def parentOf(n: int): int =
|
|
76
|
+
if (n == 2) 1
|
|
77
|
+
else if (n == 3) 2
|
|
78
|
+
else if (n == 4) 2
|
|
79
|
+
else if (n == 5) 1
|
|
80
|
+
else 0
|
|
81
|
+
|
|
82
|
+
def firstChild(n: int): int =
|
|
83
|
+
if (n == 1) 2
|
|
84
|
+
else if (n == 2) 3
|
|
85
|
+
else 0
|
|
86
|
+
|
|
87
|
+
def isParent(n: int): bool = firstChild(n) != 0
|
|
88
|
+
|
|
89
|
+
// ---- bitmask helpers ----------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def weight(n: int): int =
|
|
92
|
+
if (n == 1) 1
|
|
93
|
+
else if (n == 2) 2
|
|
94
|
+
else if (n == 3) 4
|
|
95
|
+
else if (n == 4) 8
|
|
96
|
+
else if (n == 5) 16
|
|
97
|
+
else 0
|
|
98
|
+
|
|
99
|
+
def bit(mask: int, n: int): bool =
|
|
100
|
+
if (and(n >= 1, n <= lastNode)) ((mask / weight(n)) % 2) == 1 else false
|
|
101
|
+
|
|
102
|
+
def setBit(mask: int, n: int, on: bool): int =
|
|
103
|
+
if (bit(mask, n) == on) mask
|
|
104
|
+
else if (on) mask + weight(n)
|
|
105
|
+
else mask - weight(n)
|
|
106
|
+
|
|
107
|
+
def toggleBit(mask: int, n: int): int = setBit(mask, n, not(bit(mask, n)))
|
|
108
|
+
|
|
109
|
+
// ---- visibility and navigation ------------------------------------------
|
|
110
|
+
|
|
111
|
+
// the node's parent is shown-open (roots, and the pseudo-parent 0, always are)
|
|
112
|
+
def parentOpen(n: int, e: int): bool = or(parentOf(n) == 0, bit(e, parentOf(n)))
|
|
113
|
+
|
|
114
|
+
// focusable: every ancestor is expanded (depth <= 3 => two ancestor checks)
|
|
115
|
+
def visible(n: int, e: int): bool =
|
|
116
|
+
and(n >= 1, n <= lastNode, parentOpen(n, e), parentOpen(parentOf(n), e))
|
|
117
|
+
|
|
118
|
+
def visibleAfter(n: int, k: int, e: int): bool =
|
|
119
|
+
and(n + k <= lastNode, visible(n + k, e))
|
|
120
|
+
def visibleBefore(n: int, k: int, e: int): bool =
|
|
121
|
+
and(n - k >= 1, visible(n - k, e))
|
|
122
|
+
|
|
123
|
+
// Down: the next visible node in document order; the last node stays put
|
|
124
|
+
def nextVisible(n: int, e: int): int =
|
|
125
|
+
if (visibleAfter(n, 1, e)) n + 1
|
|
126
|
+
else if (visibleAfter(n, 2, e)) n + 2
|
|
127
|
+
else if (visibleAfter(n, 3, e)) n + 3
|
|
128
|
+
else if (visibleAfter(n, 4, e)) n + 4
|
|
129
|
+
else n
|
|
130
|
+
|
|
131
|
+
// Up: the previous visible node; the first node stays put
|
|
132
|
+
def prevVisible(n: int, e: int): int =
|
|
133
|
+
if (visibleBefore(n, 1, e)) n - 1
|
|
134
|
+
else if (visibleBefore(n, 2, e)) n - 2
|
|
135
|
+
else if (visibleBefore(n, 3, e)) n - 3
|
|
136
|
+
else if (visibleBefore(n, 4, e)) n - 4
|
|
137
|
+
else n
|
|
138
|
+
|
|
139
|
+
// End: the last node focusable without opening anything (node 1 is a root)
|
|
140
|
+
def lastVisible(e: int): int =
|
|
141
|
+
if (visible(5, e)) 5
|
|
142
|
+
else if (visible(4, e)) 4
|
|
143
|
+
else if (visible(3, e)) 3
|
|
144
|
+
else if (visible(2, e)) 2
|
|
145
|
+
else 1
|
|
146
|
+
|
|
147
|
+
// Right on a closed parent opens it (focus stays); on an open parent focus
|
|
148
|
+
// moves to its first child; on a leaf nothing happens
|
|
149
|
+
def rightExpands(f: int, e: int): bool = and(isParent(f), not(bit(e, f)))
|
|
150
|
+
def rightTarget(f: int, e: int): int =
|
|
151
|
+
if (and(isParent(f), bit(e, f))) firstChild(f) else f
|
|
152
|
+
|
|
153
|
+
// Left on an open parent closes it (focus stays); otherwise focus moves to
|
|
154
|
+
// the parent; on a root nothing happens
|
|
155
|
+
def leftCollapses(f: int, e: int): bool = and(isParent(f), bit(e, f))
|
|
156
|
+
def leftTarget(f: int, e: int): int =
|
|
157
|
+
if (leftCollapses(f, e)) f
|
|
158
|
+
else if (parentOf(f) == 0) f
|
|
159
|
+
else parentOf(f)
|
|
160
|
+
|
|
161
|
+
// ---- zoom window --------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def w(s: int, e: int): int = e - s
|
|
164
|
+
|
|
165
|
+
// shrink by a quarter, never below the minimum width
|
|
166
|
+
def shrink(x: int): int =
|
|
167
|
+
if (x <= minWidth) x
|
|
168
|
+
else if (x - (x / 4) < minWidth) minWidth
|
|
169
|
+
else x - (x / 4)
|
|
170
|
+
|
|
171
|
+
// grow by a quarter, never past the domain
|
|
172
|
+
def grow(x: int): int =
|
|
173
|
+
if (x >= domainMax) x
|
|
174
|
+
else if (x + (x / 4) > domainMax) domainMax
|
|
175
|
+
else x + (x / 4)
|
|
176
|
+
|
|
177
|
+
// clamp the window start so [start, start+width] stays inside the domain
|
|
178
|
+
def clampStart(s: int, wi: int): int =
|
|
179
|
+
if (s < domainMin) domainMin
|
|
180
|
+
else if (s + wi > domainMax) domainMax - wi
|
|
181
|
+
else s
|
|
182
|
+
|
|
183
|
+
def zoomInStart(s: int, e: int): int =
|
|
184
|
+
clampStart(s + ((w(s, e) - shrink(w(s, e))) / 2), shrink(w(s, e)))
|
|
185
|
+
def zoomInEnd(s: int, e: int): int = zoomInStart(s, e) + shrink(w(s, e))
|
|
186
|
+
|
|
187
|
+
def zoomOutStart(s: int, e: int): int =
|
|
188
|
+
clampStart(s - ((grow(w(s, e)) - w(s, e)) / 2), grow(w(s, e)))
|
|
189
|
+
def zoomOutEnd(s: int, e: int): int = zoomOutStart(s, e) + grow(w(s, e))
|
|
190
|
+
|
|
191
|
+
def panRightStart(s: int, e: int): int =
|
|
192
|
+
clampStart(s + (w(s, e) / 4), w(s, e))
|
|
193
|
+
def panRightEnd(s: int, e: int): int = panRightStart(s, e) + w(s, e)
|
|
194
|
+
|
|
195
|
+
def panLeftStart(s: int, e: int): int =
|
|
196
|
+
clampStart(s - (w(s, e) / 4), w(s, e))
|
|
197
|
+
def panLeftEnd(s: int, e: int): int = panLeftStart(s, e) + w(s, e)
|
|
198
|
+
|
|
199
|
+
// ---- request / commit (map ticket rt1p) ---------------------------------
|
|
200
|
+
|
|
201
|
+
// a change of intent fires onValueChange; an inert event (same intent) does not
|
|
202
|
+
def moved(before: int, after: int): int = if (before == after) 0 else 1
|
|
203
|
+
// the node a pointer click would select (an invisible target is inert)
|
|
204
|
+
def clickWant(node: int, e: int, sel: int): int =
|
|
205
|
+
if (visible(node, e)) node else sel
|
|
206
|
+
// the node Enter would select (a key outside the timeline is inert)
|
|
207
|
+
def enterWant(hf: bool, f: int, sel: int): int = if (hf) f else sel
|
|
208
|
+
// controlled host that keeps the old value: the request is rejected
|
|
209
|
+
def holds: bool = and(controlled, not(accepting))
|
|
210
|
+
// controlled host that writes the intent back through the value prop
|
|
211
|
+
def follows: bool = and(controlled, accepting)
|
|
212
|
+
def verdict: str = if (holds) "rejected" else "accepted"
|
|
213
|
+
// the selection after a request for `want`
|
|
214
|
+
def landed(h: bool, current: int, want: int): int = if (h) current else want
|
|
215
|
+
// the value prop after a request for `want`
|
|
216
|
+
def propAfter(f: bool, p: int, want: int): int = if (f) want else p
|
|
217
|
+
|
|
218
|
+
// ---- actions ------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
action init = all {
|
|
221
|
+
expanded' = 0,
|
|
222
|
+
selected' = 0,
|
|
223
|
+
focus' = 1,
|
|
224
|
+
hasFocus' = false,
|
|
225
|
+
viewStart' = domainMin,
|
|
226
|
+
viewEnd' = domainMax,
|
|
227
|
+
controlled' = false,
|
|
228
|
+
accepting' = false,
|
|
229
|
+
valueProp' = 0,
|
|
230
|
+
requested' = 0,
|
|
231
|
+
requests' = 0,
|
|
232
|
+
outcome' = "none",
|
|
233
|
+
base' = 0
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Tab into the timeline: the roving node receives focus
|
|
237
|
+
action focusTimeline = all {
|
|
238
|
+
expanded' = expanded,
|
|
239
|
+
selected' = selected,
|
|
240
|
+
focus' = focus,
|
|
241
|
+
hasFocus' = true,
|
|
242
|
+
viewStart' = viewStart,
|
|
243
|
+
viewEnd' = viewEnd,
|
|
244
|
+
controlled' = controlled,
|
|
245
|
+
accepting' = accepting,
|
|
246
|
+
valueProp' = valueProp,
|
|
247
|
+
requested' = requested,
|
|
248
|
+
requests' = requests,
|
|
249
|
+
outcome' = outcome,
|
|
250
|
+
base' = base
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
action blurTimeline = all {
|
|
254
|
+
expanded' = expanded,
|
|
255
|
+
selected' = selected,
|
|
256
|
+
focus' = focus,
|
|
257
|
+
hasFocus' = false,
|
|
258
|
+
viewStart' = viewStart,
|
|
259
|
+
viewEnd' = viewEnd,
|
|
260
|
+
controlled' = controlled,
|
|
261
|
+
accepting' = accepting,
|
|
262
|
+
valueProp' = valueProp,
|
|
263
|
+
requested' = requested,
|
|
264
|
+
requests' = requests,
|
|
265
|
+
outcome' = outcome,
|
|
266
|
+
base' = base
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// pointer/programmatic focus of one row: hidden rows are unreachable
|
|
270
|
+
action focusNode = {
|
|
271
|
+
nondet node = Set(1, 2, 3, 4, 5).oneOf()
|
|
272
|
+
all {
|
|
273
|
+
expanded' = expanded,
|
|
274
|
+
selected' = selected,
|
|
275
|
+
focus' = if (visible(node, expanded)) node else focus,
|
|
276
|
+
hasFocus' = if (visible(node, expanded)) true else hasFocus,
|
|
277
|
+
viewStart' = viewStart,
|
|
278
|
+
viewEnd' = viewEnd,
|
|
279
|
+
controlled' = controlled,
|
|
280
|
+
accepting' = accepting,
|
|
281
|
+
valueProp' = valueProp,
|
|
282
|
+
requested' = requested,
|
|
283
|
+
requests' = requests,
|
|
284
|
+
outcome' = outcome,
|
|
285
|
+
base' = base
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
action pressDown = all {
|
|
290
|
+
expanded' = expanded,
|
|
291
|
+
selected' = selected,
|
|
292
|
+
focus' = if (hasFocus) nextVisible(focus, expanded) else focus,
|
|
293
|
+
hasFocus' = hasFocus,
|
|
294
|
+
viewStart' = viewStart,
|
|
295
|
+
viewEnd' = viewEnd,
|
|
296
|
+
controlled' = controlled,
|
|
297
|
+
accepting' = accepting,
|
|
298
|
+
valueProp' = valueProp,
|
|
299
|
+
requested' = requested,
|
|
300
|
+
requests' = requests,
|
|
301
|
+
outcome' = outcome,
|
|
302
|
+
base' = base
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
action pressUp = all {
|
|
306
|
+
expanded' = expanded,
|
|
307
|
+
selected' = selected,
|
|
308
|
+
focus' = if (hasFocus) prevVisible(focus, expanded) else focus,
|
|
309
|
+
hasFocus' = hasFocus,
|
|
310
|
+
viewStart' = viewStart,
|
|
311
|
+
viewEnd' = viewEnd,
|
|
312
|
+
controlled' = controlled,
|
|
313
|
+
accepting' = accepting,
|
|
314
|
+
valueProp' = valueProp,
|
|
315
|
+
requested' = requested,
|
|
316
|
+
requests' = requests,
|
|
317
|
+
outcome' = outcome,
|
|
318
|
+
base' = base
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
action pressRight = all {
|
|
322
|
+
expanded' = if (and(hasFocus, rightExpands(focus, expanded))) setBit(expanded, focus, true) else expanded,
|
|
323
|
+
selected' = selected,
|
|
324
|
+
focus' = if (hasFocus) rightTarget(focus, expanded) else focus,
|
|
325
|
+
hasFocus' = hasFocus,
|
|
326
|
+
viewStart' = viewStart,
|
|
327
|
+
viewEnd' = viewEnd,
|
|
328
|
+
controlled' = controlled,
|
|
329
|
+
accepting' = accepting,
|
|
330
|
+
valueProp' = valueProp,
|
|
331
|
+
requested' = requested,
|
|
332
|
+
requests' = requests,
|
|
333
|
+
outcome' = outcome,
|
|
334
|
+
base' = base
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
action pressLeft = all {
|
|
338
|
+
expanded' = if (and(hasFocus, leftCollapses(focus, expanded))) setBit(expanded, focus, false) else expanded,
|
|
339
|
+
selected' = selected,
|
|
340
|
+
focus' = if (hasFocus) leftTarget(focus, expanded) else focus,
|
|
341
|
+
hasFocus' = hasFocus,
|
|
342
|
+
viewStart' = viewStart,
|
|
343
|
+
viewEnd' = viewEnd,
|
|
344
|
+
controlled' = controlled,
|
|
345
|
+
accepting' = accepting,
|
|
346
|
+
valueProp' = valueProp,
|
|
347
|
+
requested' = requested,
|
|
348
|
+
requests' = requests,
|
|
349
|
+
outcome' = outcome,
|
|
350
|
+
base' = base
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
action pressHome = all {
|
|
354
|
+
expanded' = expanded,
|
|
355
|
+
selected' = selected,
|
|
356
|
+
focus' = if (hasFocus) 1 else focus,
|
|
357
|
+
hasFocus' = hasFocus,
|
|
358
|
+
viewStart' = viewStart,
|
|
359
|
+
viewEnd' = viewEnd,
|
|
360
|
+
controlled' = controlled,
|
|
361
|
+
accepting' = accepting,
|
|
362
|
+
valueProp' = valueProp,
|
|
363
|
+
requested' = requested,
|
|
364
|
+
requests' = requests,
|
|
365
|
+
outcome' = outcome,
|
|
366
|
+
base' = base
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
action pressEnd = all {
|
|
370
|
+
expanded' = expanded,
|
|
371
|
+
selected' = selected,
|
|
372
|
+
focus' = if (hasFocus) lastVisible(expanded) else focus,
|
|
373
|
+
hasFocus' = hasFocus,
|
|
374
|
+
viewStart' = viewStart,
|
|
375
|
+
viewEnd' = viewEnd,
|
|
376
|
+
controlled' = controlled,
|
|
377
|
+
accepting' = accepting,
|
|
378
|
+
valueProp' = valueProp,
|
|
379
|
+
requested' = requested,
|
|
380
|
+
requests' = requests,
|
|
381
|
+
outcome' = outcome,
|
|
382
|
+
base' = base
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// request: Enter selects the focused span (a key outside the timeline is inert)
|
|
386
|
+
action pressEnter = all {
|
|
387
|
+
expanded' = expanded,
|
|
388
|
+
selected' = landed(holds, selected, enterWant(hasFocus, focus, selected)),
|
|
389
|
+
focus' = focus,
|
|
390
|
+
hasFocus' = hasFocus,
|
|
391
|
+
viewStart' = viewStart,
|
|
392
|
+
viewEnd' = viewEnd,
|
|
393
|
+
controlled' = controlled,
|
|
394
|
+
accepting' = accepting,
|
|
395
|
+
valueProp' = propAfter(follows, valueProp, enterWant(hasFocus, focus, selected)),
|
|
396
|
+
requested' = if (moved(selected, enterWant(hasFocus, focus, selected)) == 1) enterWant(hasFocus, focus, selected) else requested,
|
|
397
|
+
requests' = requests + moved(selected, enterWant(hasFocus, focus, selected)),
|
|
398
|
+
outcome' = if (moved(selected, enterWant(hasFocus, focus, selected)) == 1) verdict else outcome,
|
|
399
|
+
base' = if (moved(selected, enterWant(hasFocus, focus, selected)) == 1) selected else base
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
action zoomIn = all {
|
|
403
|
+
expanded' = expanded,
|
|
404
|
+
selected' = selected,
|
|
405
|
+
focus' = focus,
|
|
406
|
+
hasFocus' = hasFocus,
|
|
407
|
+
viewStart' = zoomInStart(viewStart, viewEnd),
|
|
408
|
+
viewEnd' = zoomInEnd(viewStart, viewEnd),
|
|
409
|
+
controlled' = controlled,
|
|
410
|
+
accepting' = accepting,
|
|
411
|
+
valueProp' = valueProp,
|
|
412
|
+
requested' = requested,
|
|
413
|
+
requests' = requests,
|
|
414
|
+
outcome' = outcome,
|
|
415
|
+
base' = base
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
action zoomOut = all {
|
|
419
|
+
expanded' = expanded,
|
|
420
|
+
selected' = selected,
|
|
421
|
+
focus' = focus,
|
|
422
|
+
hasFocus' = hasFocus,
|
|
423
|
+
viewStart' = zoomOutStart(viewStart, viewEnd),
|
|
424
|
+
viewEnd' = zoomOutEnd(viewStart, viewEnd),
|
|
425
|
+
controlled' = controlled,
|
|
426
|
+
accepting' = accepting,
|
|
427
|
+
valueProp' = valueProp,
|
|
428
|
+
requested' = requested,
|
|
429
|
+
requests' = requests,
|
|
430
|
+
outcome' = outcome,
|
|
431
|
+
base' = base
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
action zoomReset = all {
|
|
435
|
+
expanded' = expanded,
|
|
436
|
+
selected' = selected,
|
|
437
|
+
focus' = focus,
|
|
438
|
+
hasFocus' = hasFocus,
|
|
439
|
+
viewStart' = domainMin,
|
|
440
|
+
viewEnd' = domainMax,
|
|
441
|
+
controlled' = controlled,
|
|
442
|
+
accepting' = accepting,
|
|
443
|
+
valueProp' = valueProp,
|
|
444
|
+
requested' = requested,
|
|
445
|
+
requests' = requests,
|
|
446
|
+
outcome' = outcome,
|
|
447
|
+
base' = base
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
action panLeft = all {
|
|
451
|
+
expanded' = expanded,
|
|
452
|
+
selected' = selected,
|
|
453
|
+
focus' = focus,
|
|
454
|
+
hasFocus' = hasFocus,
|
|
455
|
+
viewStart' = panLeftStart(viewStart, viewEnd),
|
|
456
|
+
viewEnd' = panLeftEnd(viewStart, viewEnd),
|
|
457
|
+
controlled' = controlled,
|
|
458
|
+
accepting' = accepting,
|
|
459
|
+
valueProp' = valueProp,
|
|
460
|
+
requested' = requested,
|
|
461
|
+
requests' = requests,
|
|
462
|
+
outcome' = outcome,
|
|
463
|
+
base' = base
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
action panRight = all {
|
|
467
|
+
expanded' = expanded,
|
|
468
|
+
selected' = selected,
|
|
469
|
+
focus' = focus,
|
|
470
|
+
hasFocus' = hasFocus,
|
|
471
|
+
viewStart' = panRightStart(viewStart, viewEnd),
|
|
472
|
+
viewEnd' = panRightEnd(viewStart, viewEnd),
|
|
473
|
+
controlled' = controlled,
|
|
474
|
+
accepting' = accepting,
|
|
475
|
+
valueProp' = valueProp,
|
|
476
|
+
requested' = requested,
|
|
477
|
+
requests' = requests,
|
|
478
|
+
outcome' = outcome,
|
|
479
|
+
base' = base
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// request: pointer click on a row. Only a visible row can be hit. It takes
|
|
483
|
+
// focus, requests becoming the sole selection, and a parent opens/closes.
|
|
484
|
+
action click = {
|
|
485
|
+
nondet node = Set(1, 2, 3, 4, 5).oneOf()
|
|
486
|
+
all {
|
|
487
|
+
expanded' = if (and(visible(node, expanded), isParent(node))) toggleBit(expanded, node) else expanded,
|
|
488
|
+
selected' = landed(holds, selected, clickWant(node, expanded, selected)),
|
|
489
|
+
focus' = if (visible(node, expanded)) node else focus,
|
|
490
|
+
hasFocus' = if (visible(node, expanded)) true else hasFocus,
|
|
491
|
+
viewStart' = viewStart,
|
|
492
|
+
viewEnd' = viewEnd,
|
|
493
|
+
controlled' = controlled,
|
|
494
|
+
accepting' = accepting,
|
|
495
|
+
valueProp' = propAfter(follows, valueProp, clickWant(node, expanded, selected)),
|
|
496
|
+
requested' = if (moved(selected, clickWant(node, expanded, selected)) == 1) clickWant(node, expanded, selected) else requested,
|
|
497
|
+
requests' = requests + moved(selected, clickWant(node, expanded, selected)),
|
|
498
|
+
outcome' = if (moved(selected, clickWant(node, expanded, selected)) == 1) verdict else outcome,
|
|
499
|
+
base' = if (moved(selected, clickWant(node, expanded, selected)) == 1) selected else base
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// request: the programmatic selection API (any node, no focus change)
|
|
504
|
+
// (named selectNode: `select` is a Quint built-in, QNT101)
|
|
505
|
+
action selectNode = {
|
|
506
|
+
nondet node = Set(1, 2, 3, 4, 5).oneOf()
|
|
507
|
+
all {
|
|
508
|
+
expanded' = expanded,
|
|
509
|
+
selected' = landed(holds, selected, node),
|
|
510
|
+
focus' = focus,
|
|
511
|
+
hasFocus' = hasFocus,
|
|
512
|
+
viewStart' = viewStart,
|
|
513
|
+
viewEnd' = viewEnd,
|
|
514
|
+
controlled' = controlled,
|
|
515
|
+
accepting' = accepting,
|
|
516
|
+
valueProp' = propAfter(follows, valueProp, node),
|
|
517
|
+
requested' = if (moved(selected, node) == 1) node else requested,
|
|
518
|
+
requests' = requests + moved(selected, node),
|
|
519
|
+
outcome' = if (moved(selected, node) == 1) verdict else outcome,
|
|
520
|
+
base' = if (moved(selected, node) == 1) selected else base
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// commit: the host writes the controlled selection (a no-op while uncontrolled)
|
|
525
|
+
action setValue = {
|
|
526
|
+
nondet v = Set(0, 1, 2, 3, 4, 5).oneOf()
|
|
527
|
+
all {
|
|
528
|
+
expanded' = expanded,
|
|
529
|
+
selected' = if (controlled) v else selected,
|
|
530
|
+
focus' = focus,
|
|
531
|
+
hasFocus' = hasFocus,
|
|
532
|
+
viewStart' = viewStart,
|
|
533
|
+
viewEnd' = viewEnd,
|
|
534
|
+
controlled' = controlled,
|
|
535
|
+
accepting' = accepting,
|
|
536
|
+
valueProp' = if (controlled) v else valueProp,
|
|
537
|
+
requested' = requested,
|
|
538
|
+
requests' = requests,
|
|
539
|
+
outcome' = if (controlled) "none" else outcome,
|
|
540
|
+
base' = base
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// configuration: the host starts owning the selection, adopting the current one
|
|
545
|
+
action control = all {
|
|
546
|
+
expanded' = expanded,
|
|
547
|
+
selected' = selected,
|
|
548
|
+
focus' = focus,
|
|
549
|
+
hasFocus' = hasFocus,
|
|
550
|
+
viewStart' = viewStart,
|
|
551
|
+
viewEnd' = viewEnd,
|
|
552
|
+
controlled' = true,
|
|
553
|
+
accepting' = accepting,
|
|
554
|
+
valueProp' = selected,
|
|
555
|
+
requested' = requested,
|
|
556
|
+
requests' = requests,
|
|
557
|
+
outcome' = outcome,
|
|
558
|
+
base' = base
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// configuration: the controlled host answers every request with its intent
|
|
562
|
+
action acceptRequests = all {
|
|
563
|
+
expanded' = expanded,
|
|
564
|
+
selected' = selected,
|
|
565
|
+
focus' = focus,
|
|
566
|
+
hasFocus' = hasFocus,
|
|
567
|
+
viewStart' = viewStart,
|
|
568
|
+
viewEnd' = viewEnd,
|
|
569
|
+
controlled' = controlled,
|
|
570
|
+
accepting' = true,
|
|
571
|
+
valueProp' = valueProp,
|
|
572
|
+
requested' = requested,
|
|
573
|
+
requests' = requests,
|
|
574
|
+
outcome' = outcome,
|
|
575
|
+
base' = base
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
action step = any {
|
|
579
|
+
init, focusTimeline, blurTimeline, focusNode,
|
|
580
|
+
pressDown, pressUp, pressRight, pressLeft, pressHome, pressEnd, pressEnter,
|
|
581
|
+
zoomIn, zoomOut, zoomReset, panLeft, panRight, click, selectNode,
|
|
582
|
+
setValue, control, acceptRequests
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ---- invariants ---------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
// the roving node exists
|
|
588
|
+
val FocusKnown: bool = and(focus >= 1, focus <= lastNode)
|
|
589
|
+
// APG: focus only ever rests on a focusable node — every ancestor is open
|
|
590
|
+
val FocusVisible: bool = visible(focus, expanded)
|
|
591
|
+
// selection is either none or a real node
|
|
592
|
+
val SelectedKnown: bool = or(selected == 0, and(selected >= 1, selected <= lastNode))
|
|
593
|
+
// the zoom window is a non-empty interval
|
|
594
|
+
val ZoomOrdered: bool = viewStart < viewEnd
|
|
595
|
+
// the zoom window stays inside the tick domain
|
|
596
|
+
val ZoomBounded: bool = and(viewStart >= domainMin, viewEnd <= domainMax)
|
|
597
|
+
|
|
598
|
+
def leafNeverOpen(n: int, e: int): bool = or(isParent(n), not(bit(e, n)))
|
|
599
|
+
// leaves are never recorded as expanded
|
|
600
|
+
val LeafNeverExpanded: bool = and(
|
|
601
|
+
leafNeverOpen(1, expanded), leafNeverOpen(2, expanded), leafNeverOpen(3, expanded),
|
|
602
|
+
leafNeverOpen(4, expanded), leafNeverOpen(5, expanded)
|
|
603
|
+
)
|
|
604
|
+
// callbacks only ever count up from zero
|
|
605
|
+
val RequestsNonNegative: bool = requests >= 0
|
|
606
|
+
// controlled: only the value prop sets the selection, so no event commits
|
|
607
|
+
val ControlledFollowsProp: bool = or(not(controlled), selected == valueProp)
|
|
608
|
+
// a rejected request leaves the selection as it found it
|
|
609
|
+
val RejectedRequestUntouched: bool = or(not(outcome == "rejected"), selected == base)
|
|
610
|
+
// an accepted request committed exactly its intent
|
|
611
|
+
val AcceptedRequestCommits: bool = or(not(outcome == "accepted"), selected == requested)
|
|
612
|
+
}
|