@realitycollective/webxr-uiextensions 0.1.0 → 0.1.1-preview.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.
- package/CHANGELOG.md +28 -1
- package/README.md +2 -0
- package/dist/adapter.d.ts +8 -0
- package/dist/adapter.js.map +1 -1
- package/dist/contract-cases.d.ts +3 -2
- package/dist/contract-cases.js +19 -0
- package/dist/contract-cases.js.map +1 -1
- package/dist/controls/element.d.ts +10 -1
- package/dist/controls/element.js +19 -2
- package/dist/controls/element.js.map +1 -1
- package/dist/core/window-manager.d.ts +11 -4
- package/dist/core/window-manager.js +23 -7
- package/dist/core/window-manager.js.map +1 -1
- package/dist/element-contract-cases.d.ts +47 -0
- package/dist/element-contract-cases.js +129 -0
- package/dist/element-contract-cases.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-window-host.d.ts +66 -0
- package/dist/memory-window-host.js +120 -0
- package/dist/memory-window-host.js.map +1 -0
- package/dist/scene-contract-cases.d.ts +48 -0
- package/dist/scene-contract-cases.js +91 -0
- package/dist/scene-contract-cases.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
Change log for the Reality Collective WebXR UI Extensions packages. All
|
|
3
|
+
Change log for the Reality Collective WebXR UI Extensions packages. All five packages are versioned and released together; the version below is the one carried by the `v<version>` release tag.
|
|
4
4
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Preview builds are not listed separately. The entry for a version accumulates while its previews are published, and is dated when that version is released.
|
|
6
6
|
|
|
7
|
+
## [0.1.1]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `uixElementContractCases()`, a shared conformance suite for `UixElement`, the interface the core's controls drive panel elements through, with `CONTRACT_PANEL_MARKUP` and the `UixElementContractSubject`, `UixElementContractDriver` and `UixElementContractCase` types. Every platform builds the same small panel from the markup its own way, and six cases check it: children follow the markup in order, `tagOf` reads a `uix-*` tag and nothing for a built-in one, `data-uix-*` attributes arrive camelCased in `userData`, `setProperties` reaches what the platform shows, one event reaches every listener on its element and no other, and a core stepper upgraded from the markup follows clicks to its maximum. It runs against real uikit elements on IWSDK and on XR Blocks and three.js, built headlessly the way each platform builds them, and against `NativeWindowHost` over the fake native app. It found the native defect under Fixed.
|
|
12
|
+
- `dataAttributeKey()`, the one rule for where a `data-*` attribute lands in `userData`: `data-uix-min` becomes `uixMin`, and an already camelCased `dataUixMin` gives the same key. The IWSDK component set and the native proxy elements both use it; the IWSDK copy was private.
|
|
13
|
+
- `sceneTargetContractCases()`, a shared conformance suite for `SceneTarget`, shipped as data beside `windowHostContractCases()`, with `CONTRACT_SCENE` and the `SceneTargetContractSetup` and `SceneTargetContractCase` types. A client builds its UI with the core's `applyScene()` and never asks which platform draws it, so every host must turn the same descriptor into the same windows. The suite checks that through the host's `WindowManager`: every descriptor window is opened under its id and title, a window named into a region is in that region, a window keeps its dock mode and chrome, and a window given both a region and a dock mode is world-locked in that region. The IWSDK, XR Blocks and native hosts all pass it.
|
|
14
|
+
- `MemoryWindowHost`, `MemoryWindowHandle`, `MemoryPanel` and `createMemoryWindowHostSetup()` in `@realitycollective/webxr-uiextensions`, an in-memory `WindowHost` and the family's mock. The package shipped `windowHostContractCases()` but nothing to run them against without an engine, so every headless consumer and every new adapter had to write a host first. The mock opens windows on a real `WindowManager`. A window's panel attaches when the test calls `attach(id)`, as on IWSDK, and closing a window through the manager disposes its panel and drops it from `onPanelReady` replay. `dispose()` closes every window it opened and forgets its listeners, as every platform host does. `createMemoryWindowHostSetup()` returns a fresh `WindowHostContractSetup`, so the suite runs in one loop. It passes every contract case, which `test/memory-window-host.test.ts` checks, and it is inside the 100% coverage gate. It came from a team building a native host, whose version passed the suite under Node and Hermes.
|
|
15
|
+
- `dispose()` on `WindowHost`, implemented by every platform host: `IwsdkSceneHost`, `UixWindowHost` (XR Blocks and three.js) and `NativeWindowHost`. It means the same thing everywhere: close every window the host opened through its `WindowManager`, so each goes down the normal close path, then release the host's own subscriptions and listeners, so `onPanelReady` replays nothing afterwards. It is safe to call twice. The XR Blocks host also removes the region groups it added to the scene. The IWSDK host silences its readiness system, which stays registered on the world, and forgets itself, so a later `createSceneHost(world)` builds a fresh host. No host could be torn down before, so an app that rebuilt its UI leaked listeners and scene objects on every platform. `windowHostContractCases()` gains a seventh case that holds every host to it. This adds a required member to the `WindowHost` interface, so a host written outside this repository must implement it.
|
|
16
|
+
- `@realitycollective/native-uiextensions` - the native app adapter, for an OpenXR or visionOS app that embeds a JavaScript engine such as Hermes, renders panels itself and installs `globalThis.__rcHost`, per the Reality Collective native host contract shared across the WebXR family repositories. `NativeWindowHost` implements the core `WindowHost` and `SceneTarget`, as the IWSDK and XR Blocks hosts do, so `applyScene` builds the same scene on native; regions go to the host as `createRegion` and are removed on `dispose`. Bare panels and windows build a tree of internal proxy `UixElement`s over the host's `NativeElementNode` tree, so the existing `data-uix` control upgraders work unchanged; `setProperties` forwards to `host.setProperties`, and `addEventListener` is routed from the host's single `onElementEvent` callback by `(panelId, elementHandle)`. Every `WindowManager` state change (open, focus, minimize, hide, dock, chrome, hand menu) mirrors to the host as `applyWindow(record)`; closing a window disposes its panel (once attached) and calls `closeWindow`. No engine dependency - the package reads and writes only plain data across the host boundary. Ships with an in-memory fake of the `ui` slice under `test/helpers` and runs the shared `windowHostContractCases()` against it.
|
|
17
|
+
- `demos/webxr-multiplatform` - `?uix-engine=<engine>&uix-autostart=1` boots that pipeline at once instead of showing the launch screen. It exists for the post-deploy smoke test, which now loads the desktop and IWSDK pipelines of the deployed lab as well as its launch screen; before this a pipeline that failed to start was invisible to CI, because nothing pressed START. The XR Blocks pipeline is not smoked: xrblocks renders into `<body>` rather than the mount point and logs a `console.error` about the three.js revision it wants, both of which the smoke test counts as failures.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- `tagOf()` reads nothing for a built-in element on every platform, as its doc always said. The XR Blocks and three.js parser records a built-in tag such as `span` the same way as a custom one, so `tagOf` read `"span"` there and nothing on IWSDK. A custom element name always holds a hyphen, so `tagOf` now returns a name only when it has one.
|
|
22
|
+
- `WindowManager` holds one rule for regions: a window in a region is always world-locked, because the region places it. Opening a window into a region, or `dockTo`, makes it world-locked whatever dock mode it asked for; `dockTo` emits `regionChanged` and then, if the mode changed, `dockChanged`. Setting a follow mode with `setDockMode` or `togglePin` takes a docked window out of its region first. The hosts disagreed before this: the XR Blocks host forced a docked window to world-locked on its own, while the IWSDK and native hosts left the manager's record in the requested follow mode. The same scene descriptor therefore laid out differently by platform. The XR Blocks host's own copy of the rule is gone. The native host now sends the native app the manager's dock mode for a new window, not the one the options asked for.
|
|
23
|
+
- `@realitycollective/iwsdk-uiextensions` - `TouchPointerLike` (the parameter type of `sampleOf`) is exported, and `UITouchGuardSystem` uses the core's `Hand` type rather than a private duplicate. `@realitycollective/xrblocks-uiextensions` exports `UikitComponentLike` (`UixPanelDocument.rootComponent`) and `InteractiveLike` (the return type of `pickInteractive`); `@realitycollective/uix-devtools` exports `CompileOptions` (the options of `compilePanelSource`). Each was a module-private type that appeared in an exported signature, so a consumer had nothing to name and TypeDoc reported it as referenced but undocumented.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- `@realitycollective/native-uiextensions` - the `data-uix-*` attributes of a native panel never reached the controls. `NativeElementNode` had no field for them, so on native every control ran on its defaults: a stepper lost its range, step and starting value, a toggle its labels and colours, and every control its `data-uix-id`, so `controls.stepper("count")` found nothing. `NativeElementNode` gains `attributes`, the element's `data-*` attributes as the markup wrote them, and the proxy lifts them into `userData` with `dataAttributeKey()`. The native app must send them.
|
|
28
|
+
|
|
29
|
+
- `demos/webxr-multiplatform` - the IWSDK pipeline failed to start on the deployed lab, for every visitor, with `Failed to resolve module specifier "three-mesh-bvh"`. The Vite config externalised `three-mesh-bvh` alongside the optional integrations xrblocks imports lazily, but `@iwsdk/core` imports it for real, so the IWSDK chunk shipped a bare import the browser cannot resolve. It resolves in the workspace through `@iwsdk/core`'s own dependency and is now bundled like `three-pathfinding`. Found while photographing the live lab for the Reality Collective site.
|
|
30
|
+
- `@realitycollective/iwsdk-uiextensions` - the README and `Examples/controls` still showed the `data-uix="stepper"` / `data-uix-role` attribute form that the core stopped upgrading when controls became custom elements, so the shipped example silently produced no controls. Both now use `<uix-stepper>`, `<uix-toggle>`, `<uix-expandable-label>` and `<uix-log-view>` with their part elements, and both register `uixComponentSet` when creating the world, which IWSDK 0.5 needs before it will parse a panel that uses a control. The README says why.
|
|
31
|
+
- `docs/developer-cycle.md` - the publishing section described GitHub Packages ("no npmjs.com for now"); the packages publish to npmjs.com under the `@realitycollective` scope through `publish-npm.yml`, and the section now describes that workflow, its two dist-tags and the consumer install commands.
|
|
32
|
+
|
|
7
33
|
## [0.1.0] - 2026-09-17
|
|
8
34
|
|
|
9
35
|
### Added
|
|
@@ -52,6 +78,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
52
78
|
- `@realitycollective/webxr-uiextensions` takes its geometry vocabulary from `@realitycollective/webxr-input` at `^0.1.1` rather than redeclaring it. `Vec3Tuple`, `QuatTuple`, `HeadPose`, `HeadPoseSource` and `PointerSample` are now that package's types, re-exported under the same names, so no import changes for a consumer. `PointerSample` is its `RayTuple`, which is what lets an input provider written against the shared contracts feed this contract unchanged. It is the core's only runtime dependency: the contracts package is engine-free and carries none of its own, and `test/architecture.test.ts` now allows exactly that one name and fails on any other.
|
|
53
79
|
- `PointerSample` states its ownership rule: a delivered sample belongs to the listener and the source never writes to it again, so the core's hold-to-drag and drag maths may keep a press-time sample without copying. It mirrors the rule `@realitycollective/webxr-input` 0.1.3 writes on `InputSourceSnapshot`, so a provider feeding both contracts has one promise to keep.
|
|
54
80
|
|
|
81
|
+
[0.1.1]: https://github.com/realitycollective/WebXR-UIExtensions/compare/v0.1.0...development
|
|
55
82
|
[0.1.0]: https://github.com/realitycollective/WebXR-UIExtensions/releases/tag/v0.1.0
|
|
56
83
|
|
|
57
84
|
### Fixed
|
package/README.md
CHANGED
|
@@ -65,6 +65,8 @@ Options are shared even though `createWindow` is not: every adapter's option typ
|
|
|
65
65
|
|
|
66
66
|
The `WindowManager` is the state API app code drives, and every adapter applies every one of its events. Beyond focus, minimize and dock mode it holds `hidden` (`hide`/`show`), `region` (`dockTo`/`undock`, plus `returnHome`), `chrome` (`setChrome`) and `handMenu` (`setHandMenu`), each with a typed event, and `close` is the one teardown call - an adapter must dispose on `closed`. A menu written against the manager therefore runs unchanged on every engine.
|
|
67
67
|
|
|
68
|
+
A window in a region is always world-locked, because the region places it. The manager holds this rule, not the adapters: opening into a region or `dockTo` makes a window world-locked, and setting a follow mode takes a docked window out of its region first.
|
|
69
|
+
|
|
68
70
|
Near touch has a press / hold / release state machine, `TouchPress` in `core/touch-press.ts`: fed a signed distance (positive in front of the surface) and the target under the finger each frame, it presses only on entering from the front, holds until the finger comes back out past a release distance or contact is lost, and cannot press again before that release. It reports the target at the press and at the release separately, because a finger can enter one button and leave through another; what that means for a click is the adapter's rule. The IWSDK adapter drives IWSDK's touch pointers from it.
|
|
69
71
|
|
|
70
72
|
Hand menus are the fourth dock mode, `hand-locked`. The core owns all of it except the hand pose: `hand-menu.ts` turns the window's `handMenu` options (hand, anchor, palm gate) and this frame's hand and head poses into "visible, and where", in the WebXR grip frame (`-Z` toward the thumb, `+Y` up the arm, palm at `-X` on the right hand and `+X` on the left), which a controller's grip and a tracked hand's `gripSpace` share. An adapter supplies a `HandPoseSource` and applies the result; one that has no hands falls back to body-follow placement. `HAND_MENU_SNIPPET` is the reference markup: a title-bar-free vertical stack that sizes to its content.
|
package/dist/adapter.d.ts
CHANGED
|
@@ -141,6 +141,14 @@ export interface WindowHost extends PanelHost {
|
|
|
141
141
|
* that are already live. Returns an unsubscribe function.
|
|
142
142
|
*/
|
|
143
143
|
onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;
|
|
144
|
+
/**
|
|
145
|
+
* Leave nothing behind. Closes every window this host opened, through its
|
|
146
|
+
* `WindowManager`, so each window goes down the same path a user close
|
|
147
|
+
* takes. Then releases the host's own subscriptions and listeners, so
|
|
148
|
+
* `onPanelReady` replays nothing afterwards. Safe to call more than once.
|
|
149
|
+
* Every platform implements it, so an app tears down the same way on each.
|
|
150
|
+
*/
|
|
151
|
+
dispose(): void;
|
|
144
152
|
}
|
|
145
153
|
/**
|
|
146
154
|
* A window an adapter spawned, before its panel necessarily exists.
|
package/dist/adapter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Platform-adapter contract.\n *\n * The core package owns every UX decision - window lifecycle, dock state,\n * region slot math, drag math, control models - and knows nothing about any\n * engine. An engine adapter supplies the three capabilities the core cannot\n * provide for itself, and drives the core from its own frame loop:\n *\n * - {@link PanelHost}: turn compiled UIKitML JSON into a live spatial panel\n * - {@link PointerInputSource}: deliver ray/pointer press-move-release\n * - {@link HeadPoseSource}: the viewer pose, for follow mode & body-lock\n * - {@link HandPoseSource}: the tracked hands, for hand menus (optional)\n *\n * On top of those, {@link WindowHost} adds the portable window surface:\n * {@link WindowHost.onPanelReady} for readiness, {@link PanelReadyEvent.kind}\n * to tell a managed window from a bare panel, and\n * {@link WindowHost.supportsStandalonePanels} to say whether `createPanel` is\n * usable at all. Each adapter's own `createWindow` returns a\n * {@link WindowHandle} and takes options extending {@link WindowOptionsBase},\n * so window code reads the same on every engine even though the `config`\n * payload does not.\n *\n * Known adapters:\n * - `@realitycollective/iwsdk-uiextensions` - Meta IWSDK (ECS systems bind\n * these capabilities to `@iwsdk/core` components)\n * - `@realitycollective/xrblocks-uiextensions` - Google XR Blocks / plain\n * three.js (experimental)\n *\n * The interfaces use plain tuples/records only - no engine, no three.js. The\n * geometry vocabulary itself comes from `@realitycollective/webxr-input`, the\n * engine-free contracts package both extension families share, so a pose or a\n * ray means the same thing to Interactions and to UI Extensions and one input\n * stack drives both. The names below are re-exported, so importing them from\n * this package keeps working.\n */\nimport type { PoseTuple, RayTuple, Vec3Tuple } from '@realitycollective/webxr-input';\nimport type { DockModeValue } from './core/dock-state.js';\nimport type { Hand, HandMenuOptions } from './core/hand-menu.js';\nimport type { UixElement } from './controls/element.js';\n\n/**\n * The shared geometry vocabulary, re-exported so this package stays the one\n * import an adapter needs:\n *\n * - `Vec3Tuple` - position as [x, y, z] in meters, world space unless stated\n * - `QuatTuple` - orientation quaternion as [x, y, z, w]\n * - `HeadPose` - a viewer (head) pose sample\n * - `HeadPoseSource` - supplies that pose each frame, camera on desktop and\n * HMD in XR\n */\nexport type {\n HeadPose,\n HeadPoseSource,\n PoseTuple,\n QuatTuple,\n Vec3Tuple,\n} from '@realitycollective/webxr-input';\n\n/**\n * Supplies a hand's pose each frame as a WebXR GRIP space, the frame\n * `hand-menu.ts` documents (`-Z` toward the thumb, `+Y` up the arm, the palm\n * at `-X` on the right hand and `+X` on the left). A controller's grip and a\n * tracked hand's `gripSpace` both are one; hand JOINT spaces are not, and\n * must be converted. Returns `undefined` while that hand is not tracked;\n * a hand menu on it is then hidden. An adapter without hands at all (a\n * desktop) supplies no source and falls back to body-follow placement for\n * hand-locked windows.\n */\nexport interface HandPoseSource {\n getHandPose(hand: Hand): PoseTuple | undefined;\n /**\n * Whether hands can be tracked at all right now - an XR session with hand\n * or controller input. Off (or absent from the session) means \"no hands\n * here\", and hand-locked windows fall back to body-follow placement rather\n * than staying hidden, so one page can serve a desktop and a headset.\n * Omit it when the source is always inside a session.\n */\n hasHands?(): boolean;\n}\n\n/**\n * A live spatial panel created from compiled UIKitML JSON.\n * The `root` is traversable with the core's `walk`/`findRole` helpers and\n * the `data-uix` control upgraders - identical markup works on every\n * adapter.\n */\nexport interface PanelHandle {\n /** Root element of the interpreted panel (UixElement-conformant). */\n readonly root: UixElement;\n /** Look up an element by its markup `id`. */\n getElementById(id: string): UixElement | undefined;\n /** Constrain the panel to fit within width × height meters. */\n setTargetDimensions(width: number, height: number): void;\n /** Release panel resources. */\n dispose(): void;\n}\n\n/** Creates spatial panels - the engine-specific half of UIKitML rendering. */\nexport interface PanelHost {\n /**\n * Create a panel from compiled UIKitML JSON (the `{ element, classes }`\n * shape produced by the build plugin or by\n * `@realitycollective/uix-devtools`' `compilePanelSource`).\n */\n createPanel(configJson: unknown): PanelHandle;\n}\n\n/**\n * A window whose panel has finished loading and is ready to be wired.\n * Delivered by {@link WindowHost.onPanelReady}.\n */\nexport interface PanelReadyEvent {\n /** The window's id, as given to the scene descriptor / create call. */\n id: string;\n /** The live panel - traverse it, or look elements up by markup id. */\n panel: PanelHandle;\n /**\n * What became ready.\n *\n * - `window` - created through the adapter's window factory and managed by\n * the window manager, so `id` is the id the caller asked for.\n * - `panel` - a bare panel the adapter noticed. `id` is then the adapter's\n * best stable identifier for it, which on IWSDK is the panel's config\n * path.\n *\n * Left optional so existing listeners keep compiling; adapters set it.\n */\n kind?: 'window' | 'panel';\n}\n\n/**\n * The engine-agnostic surface an app needs to build a UI: spawn windows and\n * regions from portable data, observe when panels become wireable, and reach\n * the shared `WindowManager`.\n *\n * Panels load asynchronously on every adapter (IWSDK fetches the config;\n * uikit lays out over following frames), so app code must never assume a\n * panel exists immediately after creating its window. {@link onPanelReady}\n * is the portable answer - it replaces engine-specific discovery (ECS\n * queries on IWSDK, polling anywhere else) and fires for panels that became\n * ready before the listener was registered, so wiring order never matters.\n */\nexport interface WindowHost extends PanelHost {\n /**\n * Whether {@link PanelHost.createPanel} works on this host. When `false`\n * the method is not available and throws; spawn a window instead, so the\n * engine owns the panel lifecycle. IWSDK is `false`, three.js/XR Blocks is\n * `true`.\n */\n readonly supportsStandalonePanels: boolean;\n /**\n * Subscribe to panel readiness. Late subscribers are replayed the windows\n * that are already live. Returns an unsubscribe function.\n */\n onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;\n}\n\n/**\n * A window an adapter spawned, before its panel necessarily exists.\n *\n * `createWindow` itself stays adapter-specific because the `config` payload\n * differs per engine, but what it hands back is the same everywhere: an id, a\n * panel once there is one, and a one-shot readiness callback.\n */\nexport interface WindowHandle {\n /** The window's id - the one passed in, or one the adapter generated. */\n readonly id: string;\n /**\n * The live panel, or `undefined` until the adapter has attached the\n * document. IWSDK loads and parses the markup over later frames; the\n * three.js host interprets it during `createWindow`, so there it is set\n * straight away.\n */\n readonly panel: PanelHandle | undefined;\n /**\n * Run `listener` once, when the panel is attached. Fires immediately if it\n * already is, so wiring order never matters. Returns an unsubscribe\n * function for the case where the caller gives up first.\n */\n onReady(listener: (panel: PanelHandle) => void): () => void;\n}\n\n/**\n * The window options every adapter understands.\n *\n * An adapter's own `CreateWindowOptions` extends this and adds only what its\n * engine needs - chiefly `config`, whose type differs (IWSDK takes a source\n * path, the three.js host takes parsed markup). Keeping the rest here is what\n * lets one `SceneWindow` map onto every adapter without a translation table.\n */\nexport interface WindowOptionsBase {\n /** Stable window id. Adapters generate one when it is absent. */\n id?: string;\n /** Title text written into the window chrome's title element. */\n title?: string;\n dockMode?: DockModeValue;\n /** World position for world-locked windows. */\n position?: Vec3Tuple;\n /** Fit the panel into this box in meters, preserving aspect ratio. */\n maxWidth?: number;\n maxHeight?: number;\n /** Whether the title bar drags the window. Default `true`. */\n movable?: boolean;\n /**\n * Title-bar buttons. Every button is OFF unless enabled here, or later\n * through `WindowManager.setChrome`. The chrome markup may still contain\n * the elements; a disabled button is hidden and its click ignored.\n */\n closable?: boolean;\n minimizable?: boolean;\n pinnable?: boolean;\n /** The DOCK button, which returns the window to where it spawned. */\n dockable?: boolean;\n /**\n * Where a `hand-locked` window rides: which hand, which side of the palm,\n * and whether the palm must face the viewer. Defaults: left hand, above\n * the fingertips, gated. Ignored in the other dock modes but kept, so a\n * later `setDockMode(id, 'hand-locked')` uses it.\n */\n handMenu?: Partial<HandMenuOptions>;\n /** Head-relative offset used in body-follow mode (meters). */\n followOffset?: Vec3Tuple;\n followSpeed?: number;\n followTolerance?: number;\n /** Dock straight into this region on spawn. */\n region?: string;\n}\n\n/**\n * One pointer/ray interaction stream, engine-normalised: a world-space\n * `origin` (ray origin or touch point) and a normalised `direction`. It is\n * the Input package's `RayTuple`, which is what lets a provider written\n * against `@realitycollective/webxr-input` feed this contract unchanged.\n *\n * Once delivered, a sample belongs to the listener: the source never writes\n * to it again, so `hold-to-drag` and `drag-math` may keep a press-time\n * sample without copying it. This is the rule `@realitycollective/webxr-input`\n * states on `InputSourceSnapshot`, so a provider feeding both contracts has\n * one promise to keep.\n */\nexport type PointerSample = RayTuple;\n\n/**\n * Delivers press-move-release for one interaction source (a controller ray,\n * a hand pinch, a mouse). The core's `hold-to-drag` and `drag-math` consume\n * these; the adapter decides what constitutes press/release.\n */\nexport interface PointerInputSource {\n onPress(listener: (sample: PointerSample) => void): () => void;\n onMove(listener: (sample: PointerSample) => void): () => void;\n onRelease(listener: (sample: PointerSample) => void): () => void;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Platform-adapter contract.\n *\n * The core package owns every UX decision - window lifecycle, dock state,\n * region slot math, drag math, control models - and knows nothing about any\n * engine. An engine adapter supplies the three capabilities the core cannot\n * provide for itself, and drives the core from its own frame loop:\n *\n * - {@link PanelHost}: turn compiled UIKitML JSON into a live spatial panel\n * - {@link PointerInputSource}: deliver ray/pointer press-move-release\n * - {@link HeadPoseSource}: the viewer pose, for follow mode & body-lock\n * - {@link HandPoseSource}: the tracked hands, for hand menus (optional)\n *\n * On top of those, {@link WindowHost} adds the portable window surface:\n * {@link WindowHost.onPanelReady} for readiness, {@link PanelReadyEvent.kind}\n * to tell a managed window from a bare panel, and\n * {@link WindowHost.supportsStandalonePanels} to say whether `createPanel` is\n * usable at all. Each adapter's own `createWindow` returns a\n * {@link WindowHandle} and takes options extending {@link WindowOptionsBase},\n * so window code reads the same on every engine even though the `config`\n * payload does not.\n *\n * Known adapters:\n * - `@realitycollective/iwsdk-uiextensions` - Meta IWSDK (ECS systems bind\n * these capabilities to `@iwsdk/core` components)\n * - `@realitycollective/xrblocks-uiextensions` - Google XR Blocks / plain\n * three.js (experimental)\n *\n * The interfaces use plain tuples/records only - no engine, no three.js. The\n * geometry vocabulary itself comes from `@realitycollective/webxr-input`, the\n * engine-free contracts package both extension families share, so a pose or a\n * ray means the same thing to Interactions and to UI Extensions and one input\n * stack drives both. The names below are re-exported, so importing them from\n * this package keeps working.\n */\nimport type { PoseTuple, RayTuple, Vec3Tuple } from '@realitycollective/webxr-input';\nimport type { DockModeValue } from './core/dock-state.js';\nimport type { Hand, HandMenuOptions } from './core/hand-menu.js';\nimport type { UixElement } from './controls/element.js';\n\n/**\n * The shared geometry vocabulary, re-exported so this package stays the one\n * import an adapter needs:\n *\n * - `Vec3Tuple` - position as [x, y, z] in meters, world space unless stated\n * - `QuatTuple` - orientation quaternion as [x, y, z, w]\n * - `HeadPose` - a viewer (head) pose sample\n * - `HeadPoseSource` - supplies that pose each frame, camera on desktop and\n * HMD in XR\n */\nexport type {\n HeadPose,\n HeadPoseSource,\n PoseTuple,\n QuatTuple,\n Vec3Tuple,\n} from '@realitycollective/webxr-input';\n\n/**\n * Supplies a hand's pose each frame as a WebXR GRIP space, the frame\n * `hand-menu.ts` documents (`-Z` toward the thumb, `+Y` up the arm, the palm\n * at `-X` on the right hand and `+X` on the left). A controller's grip and a\n * tracked hand's `gripSpace` both are one; hand JOINT spaces are not, and\n * must be converted. Returns `undefined` while that hand is not tracked;\n * a hand menu on it is then hidden. An adapter without hands at all (a\n * desktop) supplies no source and falls back to body-follow placement for\n * hand-locked windows.\n */\nexport interface HandPoseSource {\n getHandPose(hand: Hand): PoseTuple | undefined;\n /**\n * Whether hands can be tracked at all right now - an XR session with hand\n * or controller input. Off (or absent from the session) means \"no hands\n * here\", and hand-locked windows fall back to body-follow placement rather\n * than staying hidden, so one page can serve a desktop and a headset.\n * Omit it when the source is always inside a session.\n */\n hasHands?(): boolean;\n}\n\n/**\n * A live spatial panel created from compiled UIKitML JSON.\n * The `root` is traversable with the core's `walk`/`findRole` helpers and\n * the `data-uix` control upgraders - identical markup works on every\n * adapter.\n */\nexport interface PanelHandle {\n /** Root element of the interpreted panel (UixElement-conformant). */\n readonly root: UixElement;\n /** Look up an element by its markup `id`. */\n getElementById(id: string): UixElement | undefined;\n /** Constrain the panel to fit within width × height meters. */\n setTargetDimensions(width: number, height: number): void;\n /** Release panel resources. */\n dispose(): void;\n}\n\n/** Creates spatial panels - the engine-specific half of UIKitML rendering. */\nexport interface PanelHost {\n /**\n * Create a panel from compiled UIKitML JSON (the `{ element, classes }`\n * shape produced by the build plugin or by\n * `@realitycollective/uix-devtools`' `compilePanelSource`).\n */\n createPanel(configJson: unknown): PanelHandle;\n}\n\n/**\n * A window whose panel has finished loading and is ready to be wired.\n * Delivered by {@link WindowHost.onPanelReady}.\n */\nexport interface PanelReadyEvent {\n /** The window's id, as given to the scene descriptor / create call. */\n id: string;\n /** The live panel - traverse it, or look elements up by markup id. */\n panel: PanelHandle;\n /**\n * What became ready.\n *\n * - `window` - created through the adapter's window factory and managed by\n * the window manager, so `id` is the id the caller asked for.\n * - `panel` - a bare panel the adapter noticed. `id` is then the adapter's\n * best stable identifier for it, which on IWSDK is the panel's config\n * path.\n *\n * Left optional so existing listeners keep compiling; adapters set it.\n */\n kind?: 'window' | 'panel';\n}\n\n/**\n * The engine-agnostic surface an app needs to build a UI: spawn windows and\n * regions from portable data, observe when panels become wireable, and reach\n * the shared `WindowManager`.\n *\n * Panels load asynchronously on every adapter (IWSDK fetches the config;\n * uikit lays out over following frames), so app code must never assume a\n * panel exists immediately after creating its window. {@link onPanelReady}\n * is the portable answer - it replaces engine-specific discovery (ECS\n * queries on IWSDK, polling anywhere else) and fires for panels that became\n * ready before the listener was registered, so wiring order never matters.\n */\nexport interface WindowHost extends PanelHost {\n /**\n * Whether {@link PanelHost.createPanel} works on this host. When `false`\n * the method is not available and throws; spawn a window instead, so the\n * engine owns the panel lifecycle. IWSDK is `false`, three.js/XR Blocks is\n * `true`.\n */\n readonly supportsStandalonePanels: boolean;\n /**\n * Subscribe to panel readiness. Late subscribers are replayed the windows\n * that are already live. Returns an unsubscribe function.\n */\n onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;\n /**\n * Leave nothing behind. Closes every window this host opened, through its\n * `WindowManager`, so each window goes down the same path a user close\n * takes. Then releases the host's own subscriptions and listeners, so\n * `onPanelReady` replays nothing afterwards. Safe to call more than once.\n * Every platform implements it, so an app tears down the same way on each.\n */\n dispose(): void;\n}\n\n/**\n * A window an adapter spawned, before its panel necessarily exists.\n *\n * `createWindow` itself stays adapter-specific because the `config` payload\n * differs per engine, but what it hands back is the same everywhere: an id, a\n * panel once there is one, and a one-shot readiness callback.\n */\nexport interface WindowHandle {\n /** The window's id - the one passed in, or one the adapter generated. */\n readonly id: string;\n /**\n * The live panel, or `undefined` until the adapter has attached the\n * document. IWSDK loads and parses the markup over later frames; the\n * three.js host interprets it during `createWindow`, so there it is set\n * straight away.\n */\n readonly panel: PanelHandle | undefined;\n /**\n * Run `listener` once, when the panel is attached. Fires immediately if it\n * already is, so wiring order never matters. Returns an unsubscribe\n * function for the case where the caller gives up first.\n */\n onReady(listener: (panel: PanelHandle) => void): () => void;\n}\n\n/**\n * The window options every adapter understands.\n *\n * An adapter's own `CreateWindowOptions` extends this and adds only what its\n * engine needs - chiefly `config`, whose type differs (IWSDK takes a source\n * path, the three.js host takes parsed markup). Keeping the rest here is what\n * lets one `SceneWindow` map onto every adapter without a translation table.\n */\nexport interface WindowOptionsBase {\n /** Stable window id. Adapters generate one when it is absent. */\n id?: string;\n /** Title text written into the window chrome's title element. */\n title?: string;\n dockMode?: DockModeValue;\n /** World position for world-locked windows. */\n position?: Vec3Tuple;\n /** Fit the panel into this box in meters, preserving aspect ratio. */\n maxWidth?: number;\n maxHeight?: number;\n /** Whether the title bar drags the window. Default `true`. */\n movable?: boolean;\n /**\n * Title-bar buttons. Every button is OFF unless enabled here, or later\n * through `WindowManager.setChrome`. The chrome markup may still contain\n * the elements; a disabled button is hidden and its click ignored.\n */\n closable?: boolean;\n minimizable?: boolean;\n pinnable?: boolean;\n /** The DOCK button, which returns the window to where it spawned. */\n dockable?: boolean;\n /**\n * Where a `hand-locked` window rides: which hand, which side of the palm,\n * and whether the palm must face the viewer. Defaults: left hand, above\n * the fingertips, gated. Ignored in the other dock modes but kept, so a\n * later `setDockMode(id, 'hand-locked')` uses it.\n */\n handMenu?: Partial<HandMenuOptions>;\n /** Head-relative offset used in body-follow mode (meters). */\n followOffset?: Vec3Tuple;\n followSpeed?: number;\n followTolerance?: number;\n /** Dock straight into this region on spawn. */\n region?: string;\n}\n\n/**\n * One pointer/ray interaction stream, engine-normalised: a world-space\n * `origin` (ray origin or touch point) and a normalised `direction`. It is\n * the Input package's `RayTuple`, which is what lets a provider written\n * against `@realitycollective/webxr-input` feed this contract unchanged.\n *\n * Once delivered, a sample belongs to the listener: the source never writes\n * to it again, so `hold-to-drag` and `drag-math` may keep a press-time\n * sample without copying it. This is the rule `@realitycollective/webxr-input`\n * states on `InputSourceSnapshot`, so a provider feeding both contracts has\n * one promise to keep.\n */\nexport type PointerSample = RayTuple;\n\n/**\n * Delivers press-move-release for one interaction source (a controller ray,\n * a hand pinch, a mouse). The core's `hold-to-drag` and `drag-math` consume\n * these; the adapter decides what constitutes press/release.\n */\nexport interface PointerInputSource {\n onPress(listener: (sample: PointerSample) => void): () => void;\n onMove(listener: (sample: PointerSample) => void): () => void;\n onRelease(listener: (sample: PointerSample) => void): () => void;\n}\n"]}
|
package/dist/contract-cases.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shared `WindowHost` contract, shipped as data rather than as tests.
|
|
3
3
|
*
|
|
4
|
-
* Every adapter promises the same
|
|
4
|
+
* Every adapter promises the same six things, whatever engine sits behind
|
|
5
5
|
* it: it says whether bare panels work and behaves accordingly,
|
|
6
6
|
* `createWindow` hands back a {@link WindowHandle}, `onReady` fires exactly
|
|
7
7
|
* once and replays for a late subscriber, `onPanelReady` replays too, and
|
|
8
8
|
* closing a window through the `WindowManager` tears it down so it is not
|
|
9
|
-
* replayed afterwards
|
|
9
|
+
* replayed afterwards, and `dispose()` closes the host's windows and leaves
|
|
10
|
+
* nothing to replay.
|
|
10
11
|
* Running one suite from every adapter is what keeps those promises from
|
|
11
12
|
* drifting apart, and gives a new adapter a starting test for free.
|
|
12
13
|
*
|
package/dist/contract-cases.js
CHANGED
|
@@ -87,6 +87,25 @@ const CASES = [
|
|
|
87
87
|
assert(!ids.includes('contract-e'), `onPanelReady must not replay a window closed through the manager, got [${ids.join(', ')}]`);
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: 'dispose closes the host windows, stops replay, and can be called twice',
|
|
92
|
+
run(setup) {
|
|
93
|
+
setup.createWindow('contract-f');
|
|
94
|
+
setup.attach?.('contract-f');
|
|
95
|
+
assert(typeof setup.host.dispose === 'function', 'a WindowHost must implement dispose()');
|
|
96
|
+
setup.host.dispose();
|
|
97
|
+
assert(!setup.manager.has('contract-f'), 'dispose() must close the windows the host opened');
|
|
98
|
+
const ids = [];
|
|
99
|
+
detach(setup.host.onPanelReady((event) => ids.push(event.id)), 'onPanelReady');
|
|
100
|
+
assert(ids.length === 0, `a disposed host must replay nothing to onPanelReady, got [${ids.join(', ')}]`);
|
|
101
|
+
try {
|
|
102
|
+
setup.host.dispose();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`a second dispose() must be a no-op, it threw: ${String(error)}`);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
90
109
|
];
|
|
91
110
|
function assert(condition, message) {
|
|
92
111
|
if (!condition)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared `WindowHost` contract, shipped as data rather than as tests.\n *\n * Every adapter promises the same five things, whatever engine sits behind\n * it: it says whether bare panels work and behaves accordingly,\n * `createWindow` hands back a {@link WindowHandle}, `onReady` fires exactly\n * once and replays for a late subscriber, `onPanelReady` replays too, and\n * closing a window through the `WindowManager` tears it down so it is not\n * replayed afterwards.\n * Running one suite from every adapter is what keeps those promises from\n * drifting apart, and gives a new adapter a starting test for free.\n *\n * The suite is runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * `createWindow` itself is adapter-specific - IWSDK takes a config PATH, the\n * three.js host takes parsed markup - so the caller supplies a\n * {@link WindowHostContractSetup} that wraps those differences.\n */\nimport type { PanelHandle, WindowHandle, WindowHost } from './adapter.js';\nimport type { WindowManager } from './core/window-manager.js';\n\n/**\n * Everything a case needs to drive one adapter. Build a FRESH one per case:\n * cases spawn windows of their own and do not clean up after themselves.\n */\nexport interface WindowHostContractSetup {\n /** The host under test. */\n host: WindowHost;\n /**\n * The manager the host applies. `close()` on it is the one teardown call\n * app code has, so the suite proves the host honours it.\n */\n manager: WindowManager;\n /** Spawn one window with this id, using whatever config the adapter needs. */\n createWindow(id: string): WindowHandle;\n /**\n * Attach the panel for a window, where the adapter attaches asynchronously.\n * Omit it when the panel exists as soon as the window does.\n */\n attach?: (id: string) => void;\n /** A config `createPanel` accepts, for hosts that support bare panels. */\n panelConfig?: unknown;\n}\n\n/**\n * One check a {@link WindowHost} implementation must pass. `run` returns\n * silently on success and throws an `Error` describing the failure\n * otherwise, so any test runner can host it.\n */\nexport interface WindowHostContractCase {\n name: string;\n run(setup: WindowHostContractSetup): void;\n}\n\n/**\n * The shared host conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of windowHostContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSetup()));\n * }\n * ```\n *\n * `makeSetup()` runs per case, so each case gets a host of its own.\n */\nexport function windowHostContractCases(): readonly WindowHostContractCase[] {\n return CASES;\n}\n\nconst CASES: readonly WindowHostContractCase[] = [\n {\n name: 'reports whether bare panels work, and createPanel agrees',\n run(setup) {\n const supported = setup.host.supportsStandalonePanels;\n assert(\n typeof supported === 'boolean',\n `supportsStandalonePanels must be a boolean, got ${typeof supported}`,\n );\n if (supported) {\n try {\n setup.host.createPanel(setup.panelConfig);\n } catch (error) {\n throw new Error(\n `supportsStandalonePanels is true, so createPanel() must work, it threw: ${String(error)}`,\n );\n }\n return;\n }\n assert(\n threw(() => setup.host.createPanel(setup.panelConfig)),\n 'supportsStandalonePanels is false, so createPanel() must throw rather than return an unusable panel',\n );\n },\n },\n {\n name: 'createWindow returns a handle with an id and onReady',\n run(setup) {\n const handle = setup.createWindow('contract-a');\n assert(\n typeof handle.id === 'string',\n `WindowHandle.id must be a string, got ${typeof handle.id}`,\n );\n assert(\n handle.id === 'contract-a',\n `createWindow(\"contract-a\") must keep the id it was given, got \"${handle.id}\"`,\n );\n assert(\n typeof handle.onReady === 'function',\n 'a WindowHandle must implement onReady()',\n );\n },\n },\n {\n name: 'onReady fires once when the panel attaches',\n run(setup) {\n const handle = setup.createWindow('contract-b');\n const seen: PanelHandle[] = [];\n handle.onReady((panel) => seen.push(panel));\n setup.attach?.('contract-b');\n assert(\n seen.length === 1,\n `onReady must fire exactly once when the panel attaches, it fired ${String(seen.length)} time(s)`,\n );\n assert(\n handle.panel === seen[0],\n 'the panel passed to onReady must be the same one the handle reports',\n );\n },\n },\n {\n name: 'onReady replays for a subscriber that arrives late',\n run(setup) {\n const handle = setup.createWindow('contract-c');\n setup.attach?.('contract-c');\n const seen: PanelHandle[] = [];\n const stop = handle.onReady((panel) => seen.push(panel));\n assert(\n seen.length === 1,\n `onReady must replay for a subscriber that arrives after the panel, it fired ${String(seen.length)} time(s)`,\n );\n // Unsubscribing after the fact is a no-op, not an error.\n detach(stop, 'onReady');\n assert(\n seen.length === 1,\n 'unsubscribing after onReady has replayed must not deliver the panel again',\n );\n },\n },\n {\n name: 'onPanelReady replays for a subscriber that arrives late',\n run(setup) {\n setup.createWindow('contract-d');\n setup.attach?.('contract-d');\n const ids: string[] = [];\n const stop = setup.host.onPanelReady((event) => ids.push(event.id));\n assert(\n ids.includes('contract-d'),\n `onPanelReady must replay the windows already live, expected \"contract-d\" among [${ids.join(', ')}]`,\n );\n detach(stop, 'onPanelReady');\n },\n },\n {\n name: 'closing through the manager takes the window out of onPanelReady replay',\n run(setup) {\n setup.createWindow('contract-e');\n setup.attach?.('contract-e');\n assert(\n setup.manager.has('contract-e'),\n 'the host must open the window on the manager it was given',\n );\n setup.manager.close('contract-e');\n assert(\n !setup.manager.has('contract-e'),\n 'the manager must forget a closed window',\n );\n const ids: string[] = [];\n detach(setup.host.onPanelReady((event) => ids.push(event.id)), 'onPanelReady');\n assert(\n !ids.includes('contract-e'),\n `onPanelReady must not replay a window closed through the manager, got [${ids.join(', ')}]`,\n );\n },\n },\n];\n\nfunction assert(condition: boolean, message: string): void {\n if (!condition) throw new Error(message);\n}\n\n/** Whether `run` threw, without caring what it threw. */\nfunction threw(run: () => unknown): boolean {\n try {\n run();\n return false;\n } catch {\n return true;\n }\n}\n\nfunction detach(stop: () => void, label: string): void {\n try {\n stop();\n } catch (error) {\n throw new Error(\n `the unsubscribe returned by ${label}() threw: ${String(error)}`,\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"contract-cases.js","sourceRoot":"","sources":["../src/contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared `WindowHost` contract, shipped as data rather than as tests.\n *\n * Every adapter promises the same six things, whatever engine sits behind\n * it: it says whether bare panels work and behaves accordingly,\n * `createWindow` hands back a {@link WindowHandle}, `onReady` fires exactly\n * once and replays for a late subscriber, `onPanelReady` replays too, and\n * closing a window through the `WindowManager` tears it down so it is not\n * replayed afterwards, and `dispose()` closes the host's windows and leaves\n * nothing to replay.\n * Running one suite from every adapter is what keeps those promises from\n * drifting apart, and gives a new adapter a starting test for free.\n *\n * The suite is runner-free on purpose. Every adapter repository already has\n * its own test runner, and an adapter written outside this repository cannot\n * reach into this one's `test/` folder, so the checks ship as plain objects\n * that throw an `Error` on failure and the adapter iterates them.\n *\n * `createWindow` itself is adapter-specific - IWSDK takes a config PATH, the\n * three.js host takes parsed markup - so the caller supplies a\n * {@link WindowHostContractSetup} that wraps those differences.\n */\nimport type { PanelHandle, WindowHandle, WindowHost } from './adapter.js';\nimport type { WindowManager } from './core/window-manager.js';\n\n/**\n * Everything a case needs to drive one adapter. Build a FRESH one per case:\n * cases spawn windows of their own and do not clean up after themselves.\n */\nexport interface WindowHostContractSetup {\n /** The host under test. */\n host: WindowHost;\n /**\n * The manager the host applies. `close()` on it is the one teardown call\n * app code has, so the suite proves the host honours it.\n */\n manager: WindowManager;\n /** Spawn one window with this id, using whatever config the adapter needs. */\n createWindow(id: string): WindowHandle;\n /**\n * Attach the panel for a window, where the adapter attaches asynchronously.\n * Omit it when the panel exists as soon as the window does.\n */\n attach?: (id: string) => void;\n /** A config `createPanel` accepts, for hosts that support bare panels. */\n panelConfig?: unknown;\n}\n\n/**\n * One check a {@link WindowHost} implementation must pass. `run` returns\n * silently on success and throws an `Error` describing the failure\n * otherwise, so any test runner can host it.\n */\nexport interface WindowHostContractCase {\n name: string;\n run(setup: WindowHostContractSetup): void;\n}\n\n/**\n * The shared host conformance suite. An adapter's test file is a loop:\n *\n * ```ts\n * for (const contractCase of windowHostContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSetup()));\n * }\n * ```\n *\n * `makeSetup()` runs per case, so each case gets a host of its own.\n */\nexport function windowHostContractCases(): readonly WindowHostContractCase[] {\n return CASES;\n}\n\nconst CASES: readonly WindowHostContractCase[] = [\n {\n name: 'reports whether bare panels work, and createPanel agrees',\n run(setup) {\n const supported = setup.host.supportsStandalonePanels;\n assert(\n typeof supported === 'boolean',\n `supportsStandalonePanels must be a boolean, got ${typeof supported}`,\n );\n if (supported) {\n try {\n setup.host.createPanel(setup.panelConfig);\n } catch (error) {\n throw new Error(\n `supportsStandalonePanels is true, so createPanel() must work, it threw: ${String(error)}`,\n );\n }\n return;\n }\n assert(\n threw(() => setup.host.createPanel(setup.panelConfig)),\n 'supportsStandalonePanels is false, so createPanel() must throw rather than return an unusable panel',\n );\n },\n },\n {\n name: 'createWindow returns a handle with an id and onReady',\n run(setup) {\n const handle = setup.createWindow('contract-a');\n assert(\n typeof handle.id === 'string',\n `WindowHandle.id must be a string, got ${typeof handle.id}`,\n );\n assert(\n handle.id === 'contract-a',\n `createWindow(\"contract-a\") must keep the id it was given, got \"${handle.id}\"`,\n );\n assert(\n typeof handle.onReady === 'function',\n 'a WindowHandle must implement onReady()',\n );\n },\n },\n {\n name: 'onReady fires once when the panel attaches',\n run(setup) {\n const handle = setup.createWindow('contract-b');\n const seen: PanelHandle[] = [];\n handle.onReady((panel) => seen.push(panel));\n setup.attach?.('contract-b');\n assert(\n seen.length === 1,\n `onReady must fire exactly once when the panel attaches, it fired ${String(seen.length)} time(s)`,\n );\n assert(\n handle.panel === seen[0],\n 'the panel passed to onReady must be the same one the handle reports',\n );\n },\n },\n {\n name: 'onReady replays for a subscriber that arrives late',\n run(setup) {\n const handle = setup.createWindow('contract-c');\n setup.attach?.('contract-c');\n const seen: PanelHandle[] = [];\n const stop = handle.onReady((panel) => seen.push(panel));\n assert(\n seen.length === 1,\n `onReady must replay for a subscriber that arrives after the panel, it fired ${String(seen.length)} time(s)`,\n );\n // Unsubscribing after the fact is a no-op, not an error.\n detach(stop, 'onReady');\n assert(\n seen.length === 1,\n 'unsubscribing after onReady has replayed must not deliver the panel again',\n );\n },\n },\n {\n name: 'onPanelReady replays for a subscriber that arrives late',\n run(setup) {\n setup.createWindow('contract-d');\n setup.attach?.('contract-d');\n const ids: string[] = [];\n const stop = setup.host.onPanelReady((event) => ids.push(event.id));\n assert(\n ids.includes('contract-d'),\n `onPanelReady must replay the windows already live, expected \"contract-d\" among [${ids.join(', ')}]`,\n );\n detach(stop, 'onPanelReady');\n },\n },\n {\n name: 'closing through the manager takes the window out of onPanelReady replay',\n run(setup) {\n setup.createWindow('contract-e');\n setup.attach?.('contract-e');\n assert(\n setup.manager.has('contract-e'),\n 'the host must open the window on the manager it was given',\n );\n setup.manager.close('contract-e');\n assert(\n !setup.manager.has('contract-e'),\n 'the manager must forget a closed window',\n );\n const ids: string[] = [];\n detach(setup.host.onPanelReady((event) => ids.push(event.id)), 'onPanelReady');\n assert(\n !ids.includes('contract-e'),\n `onPanelReady must not replay a window closed through the manager, got [${ids.join(', ')}]`,\n );\n },\n },\n {\n name: 'dispose closes the host windows, stops replay, and can be called twice',\n run(setup) {\n setup.createWindow('contract-f');\n setup.attach?.('contract-f');\n assert(\n typeof setup.host.dispose === 'function',\n 'a WindowHost must implement dispose()',\n );\n setup.host.dispose();\n assert(\n !setup.manager.has('contract-f'),\n 'dispose() must close the windows the host opened',\n );\n const ids: string[] = [];\n detach(setup.host.onPanelReady((event) => ids.push(event.id)), 'onPanelReady');\n assert(\n ids.length === 0,\n `a disposed host must replay nothing to onPanelReady, got [${ids.join(', ')}]`,\n );\n try {\n setup.host.dispose();\n } catch (error) {\n throw new Error(`a second dispose() must be a no-op, it threw: ${String(error)}`);\n }\n },\n },\n];\n\nfunction assert(condition: boolean, message: string): void {\n if (!condition) throw new Error(message);\n}\n\n/** Whether `run` threw, without caring what it threw. */\nfunction threw(run: () => unknown): boolean {\n try {\n run();\n return false;\n } catch {\n return true;\n }\n}\n\nfunction detach(stop: () => void, label: string): void {\n try {\n stop();\n } catch (error) {\n throw new Error(\n `the unsubscribe returned by ${label}() threw: ${String(error)}`,\n );\n }\n}\n"]}
|
|
@@ -23,9 +23,18 @@ export interface UixElement {
|
|
|
23
23
|
export declare function walk(element: UixElement, visit: (element: UixElement) => void): void;
|
|
24
24
|
/**
|
|
25
25
|
* The custom-element tag this element was declared with, lowercased, or
|
|
26
|
-
* undefined for a plain built-in element.
|
|
26
|
+
* undefined for a plain built-in element. A custom element name always holds
|
|
27
|
+
* a hyphen, so a built-in tag a parser records the same way (`span`) reads
|
|
28
|
+
* as undefined on every platform.
|
|
27
29
|
*/
|
|
28
30
|
export declare function tagOf(element: UixElement): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The `userData` key a `data-*` attribute lands under: `data-` stripped and
|
|
33
|
+
* the rest camelCased, so `data-uix-min` becomes `uixMin`. A parser that has
|
|
34
|
+
* already camelCased the attribute (`dataUixMin`) gives the same key. Every
|
|
35
|
+
* platform that builds `UixElement`s from markup uses this one rule.
|
|
36
|
+
*/
|
|
37
|
+
export declare function dataAttributeKey(attribute: string): string;
|
|
29
38
|
/** All descendants (including self) declared as `<uix-{role}>`. */
|
|
30
39
|
export declare function findRoles(root: UixElement, role: string): UixElement[];
|
|
31
40
|
export declare function findRole(root: UixElement, role: string): UixElement | undefined;
|
package/dist/controls/element.js
CHANGED
|
@@ -7,12 +7,29 @@ export function walk(element, visit) {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* The custom-element tag this element was declared with, lowercased, or
|
|
10
|
-
* undefined for a plain built-in element.
|
|
10
|
+
* undefined for a plain built-in element. A custom element name always holds
|
|
11
|
+
* a hyphen, so a built-in tag a parser records the same way (`span`) reads
|
|
12
|
+
* as undefined on every platform.
|
|
11
13
|
*/
|
|
12
14
|
export function tagOf(element) {
|
|
13
15
|
const custom = element.userData?.['customElement'];
|
|
14
16
|
const name = custom?.componentName;
|
|
15
|
-
return typeof name === 'string' && name.
|
|
17
|
+
return typeof name === 'string' && name.includes('-') ? name.toLowerCase() : undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The `userData` key a `data-*` attribute lands under: `data-` stripped and
|
|
21
|
+
* the rest camelCased, so `data-uix-min` becomes `uixMin`. A parser that has
|
|
22
|
+
* already camelCased the attribute (`dataUixMin`) gives the same key. Every
|
|
23
|
+
* platform that builds `UixElement`s from markup uses this one rule.
|
|
24
|
+
*/
|
|
25
|
+
export function dataAttributeKey(attribute) {
|
|
26
|
+
if (/^data[A-Z]/.test(attribute)) {
|
|
27
|
+
const rest = attribute.slice(4);
|
|
28
|
+
return rest.charAt(0).toLowerCase() + rest.slice(1);
|
|
29
|
+
}
|
|
30
|
+
return attribute
|
|
31
|
+
.replace(/^data-/, '')
|
|
32
|
+
.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
16
33
|
}
|
|
17
34
|
/** All descendants (including self) declared as `<uix-{role}>`. */
|
|
18
35
|
export function findRoles(root, role) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"element.js","sourceRoot":"","sources":["../../src/controls/element.ts"],"names":[],"mappings":"AAsBA,yDAAyD;AACzD,MAAM,UAAU,IAAI,CAAC,OAAmB,EAAE,KAAoC;IAC5E,KAAK,CAAC,OAAO,CAAC,CAAC;IACf,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;QAC3C,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACrB,CAAC;AACH,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"element.js","sourceRoot":"","sources":["../../src/controls/element.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Structural view of a uikit element as the controls layer needs it.\n *\n * Controls and their parts are declared as CUSTOM ELEMENTS - `<uix-stepper>`,\n * `<uix-value>` - rather than as attributes on a `<div>`. That is what makes\n * one markup file portable across every adapter: the IWSDK 0.5 parser rejects\n * unknown attributes on built-in tags, but accepts a custom tag declared in a\n * component set, and the three.js / XR Blocks parser accepts custom tags with\n * no registration at all.\n *\n * Both paths expose the declared tag the same way, on\n * `userData.customElement.componentName`. Parameters stay `data-uix-*`\n * attributes, which land in `userData` camelCased with `data-` stripped:\n * `data-uix-min=\"0\"` → `userData.uixMin`.\n */\nexport interface UixElement {\n userData: Record<string, unknown>;\n children: UixElement[];\n addEventListener(type: string, listener: (event?: unknown) => void): void;\n setProperties(props: Record<string, unknown>): void;\n}\n\n/** Depth-first traversal over uikit element children. */\nexport function walk(element: UixElement, visit: (element: UixElement) => void): void {\n visit(element);\n for (const child of element.children ?? []) {\n walk(child, visit);\n }\n}\n\n/**\n * The custom-element tag this element was declared with, lowercased, or\n * undefined for a plain built-in element. A custom element name always holds\n * a hyphen, so a built-in tag a parser records the same way (`span`) reads\n * as undefined on every platform.\n */\nexport function tagOf(element: UixElement): string | undefined {\n const custom = element.userData?.['customElement'] as\n | { componentName?: unknown }\n | undefined;\n const name = custom?.componentName;\n return typeof name === 'string' && name.includes('-') ? name.toLowerCase() : undefined;\n}\n\n/**\n * The `userData` key a `data-*` attribute lands under: `data-` stripped and\n * the rest camelCased, so `data-uix-min` becomes `uixMin`. A parser that has\n * already camelCased the attribute (`dataUixMin`) gives the same key. Every\n * platform that builds `UixElement`s from markup uses this one rule.\n */\nexport function dataAttributeKey(attribute: string): string {\n if (/^data[A-Z]/.test(attribute)) {\n const rest = attribute.slice(4);\n return rest.charAt(0).toLowerCase() + rest.slice(1);\n }\n return attribute\n .replace(/^data-/, '')\n .replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase());\n}\n\n/** All descendants (including self) declared as `<uix-{role}>`. */\nexport function findRoles(root: UixElement, role: string): UixElement[] {\n const tag = `uix-${role}`;\n const found: UixElement[] = [];\n walk(root, (element) => {\n if (tagOf(element) === tag) {\n found.push(element);\n }\n });\n return found;\n}\n\nexport function findRole(root: UixElement, role: string): UixElement | undefined {\n return findRoles(root, role)[0];\n}\n\nexport function attrString(element: UixElement, key: string): string | undefined {\n const value = element.userData?.[key];\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nexport function attrNumber(element: UixElement, key: string): number | undefined {\n const raw = attrString(element, key);\n if (raw === undefined) {\n return undefined;\n }\n const value = Number(raw);\n return Number.isFinite(value) ? value : undefined;\n}\n\nexport function attrBoolean(element: UixElement, key: string): boolean | undefined {\n const raw = attrString(element, key);\n if (raw === undefined) {\n return undefined;\n }\n return raw === 'true' || raw === '1' || raw === 'on';\n}\n"]}
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
* It is also the ONE API app code calls to change a window: a hand menu that
|
|
14
14
|
* hides, docks or pins a targeted window talks to the manager, and every
|
|
15
15
|
* adapter applies the resulting events. Nothing here needs an engine handle.
|
|
16
|
+
*
|
|
17
|
+
* A window in a region is always world-locked: the region places it, so it
|
|
18
|
+
* cannot also follow the viewer. Opening into a region or `dockTo` makes it
|
|
19
|
+
* world-locked, and a follow mode set through `setDockMode` or `togglePin`
|
|
20
|
+
* takes it out of its region first. The manager holds this rule so every
|
|
21
|
+
* platform lays out the same descriptor the same way.
|
|
16
22
|
*/
|
|
17
23
|
import { Emitter } from './events.js';
|
|
18
24
|
import { DockModeValue } from './dock-state.js';
|
|
@@ -84,7 +90,7 @@ export interface OpenWindowOptions {
|
|
|
84
90
|
dockMode?: DockModeValue;
|
|
85
91
|
/** Open hidden; `show()` reveals it. */
|
|
86
92
|
hidden?: boolean;
|
|
87
|
-
/** Open docked into this region. */
|
|
93
|
+
/** Open docked into this region. The window is then world-locked, whatever `dockMode` says. */
|
|
88
94
|
region?: string;
|
|
89
95
|
/** Buttons to enable; anything omitted stays off. */
|
|
90
96
|
chrome?: Partial<WindowChrome>;
|
|
@@ -131,9 +137,10 @@ export declare class WindowManager {
|
|
|
131
137
|
toggleHidden(id: string): void;
|
|
132
138
|
setDockMode(id: string, mode: DockModeValue): void;
|
|
133
139
|
/**
|
|
134
|
-
* Dock a window into a layout region. The manager records the intent
|
|
135
|
-
*
|
|
136
|
-
*
|
|
140
|
+
* Dock a window into a layout region. The manager records the intent,
|
|
141
|
+
* makes the window world-locked, and emits `regionChanged` then, if the
|
|
142
|
+
* mode changed, `dockChanged`. The adapter places the window in a slot
|
|
143
|
+
* (and may call `undock` back if the region is full or unknown).
|
|
137
144
|
*/
|
|
138
145
|
dockTo(id: string, regionId: string): void;
|
|
139
146
|
/** Take a window out of its region. No-op when it is not docked. */
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
* It is also the ONE API app code calls to change a window: a hand menu that
|
|
14
14
|
* hides, docks or pins a targeted window talks to the manager, and every
|
|
15
15
|
* adapter applies the resulting events. Nothing here needs an engine handle.
|
|
16
|
+
*
|
|
17
|
+
* A window in a region is always world-locked: the region places it, so it
|
|
18
|
+
* cannot also follow the viewer. Opening into a region or `dockTo` makes it
|
|
19
|
+
* world-locked, and a follow mode set through `setDockMode` or `togglePin`
|
|
20
|
+
* takes it out of its region first. The manager holds this rule so every
|
|
21
|
+
* platform lays out the same descriptor the same way.
|
|
16
22
|
*/
|
|
17
23
|
import { Emitter } from './events.js';
|
|
18
24
|
import { DockMode, isDockMode, togglePinned } from './dock-state.js';
|
|
@@ -53,14 +59,14 @@ export class WindowManager {
|
|
|
53
59
|
if (this.windows.has(id)) {
|
|
54
60
|
throw new Error(`[uix] window "${id}" is already open`);
|
|
55
61
|
}
|
|
56
|
-
const
|
|
57
|
-
if (!isDockMode(
|
|
58
|
-
throw new Error(`[uix] "${String(
|
|
62
|
+
const requested = options.dockMode ?? DockMode.WorldLocked;
|
|
63
|
+
if (!isDockMode(requested)) {
|
|
64
|
+
throw new Error(`[uix] "${String(requested)}" is not a dock mode`);
|
|
59
65
|
}
|
|
60
66
|
const record = {
|
|
61
67
|
id,
|
|
62
68
|
title: options.title ?? id,
|
|
63
|
-
dockMode,
|
|
69
|
+
dockMode: options.region !== undefined ? DockMode.WorldLocked : requested,
|
|
64
70
|
minimized: false,
|
|
65
71
|
hidden: options.hidden ?? false,
|
|
66
72
|
dragging: false,
|
|
@@ -162,14 +168,19 @@ export class WindowManager {
|
|
|
162
168
|
if (record.dockMode === mode) {
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
171
|
+
if (mode !== DockMode.WorldLocked) {
|
|
172
|
+
// A region places its windows, so a window that follows cannot stay in one.
|
|
173
|
+
this.undock(id);
|
|
174
|
+
}
|
|
165
175
|
const previous = record.dockMode;
|
|
166
176
|
record.dockMode = mode;
|
|
167
177
|
this.events.emit('dockChanged', { window: record, previous });
|
|
168
178
|
}
|
|
169
179
|
/**
|
|
170
|
-
* Dock a window into a layout region. The manager records the intent
|
|
171
|
-
*
|
|
172
|
-
*
|
|
180
|
+
* Dock a window into a layout region. The manager records the intent,
|
|
181
|
+
* makes the window world-locked, and emits `regionChanged` then, if the
|
|
182
|
+
* mode changed, `dockChanged`. The adapter places the window in a slot
|
|
183
|
+
* (and may call `undock` back if the region is full or unknown).
|
|
173
184
|
*/
|
|
174
185
|
dockTo(id, regionId) {
|
|
175
186
|
if (!regionId) {
|
|
@@ -180,8 +191,13 @@ export class WindowManager {
|
|
|
180
191
|
return;
|
|
181
192
|
}
|
|
182
193
|
const previous = record.region;
|
|
194
|
+
const previousMode = record.dockMode;
|
|
183
195
|
record.region = regionId;
|
|
196
|
+
record.dockMode = DockMode.WorldLocked;
|
|
184
197
|
this.events.emit('regionChanged', { window: record, previous });
|
|
198
|
+
if (previousMode !== DockMode.WorldLocked) {
|
|
199
|
+
this.events.emit('dockChanged', { window: record, previous: previousMode });
|
|
200
|
+
}
|
|
185
201
|
}
|
|
186
202
|
/** Take a window out of its region. No-op when it is not docked. */
|
|
187
203
|
undock(id) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window-manager.js","sourceRoot":"","sources":["../../src/core/window-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WindowManager - pure window registry, focus ordering and window state.\n *\n * The manager knows nothing about entities, three.js or uikit. It deals in\n * opaque window ids and answers the questions the engine layer applies each\n * frame:\n *\n * 1. What is the focus (z) order? (`orderOf` → depth bias so the focused\n * window renders nearest the user and receives pointer priority)\n * 2. What state is a window in? (minimized / hidden / focused / dock mode /\n * region / which chrome buttons are enabled)\n *\n * It is also the ONE API app code calls to change a window: a hand menu that\n * hides, docks or pins a targeted window talks to the manager, and every\n * adapter applies the resulting events. Nothing here needs an engine handle.\n */\nimport { Emitter } from './events.js';\nimport { DockMode, DockModeValue, isDockMode, togglePinned } from './dock-state.js';\nimport { resolveHandMenu, type HandMenuOptions } from './hand-menu.js';\n\n/**\n * Which title-bar buttons are enabled. Keys match the chrome element ids in\n * `WINDOW_CHROME_IDS`. A disabled button is hidden and its click ignored.\n * Every button is OFF unless the app turns it on.\n */\nexport interface WindowChrome {\n /** Body-follow ⇄ world-locked toggle. */\n pin: boolean;\n /** Return the window to its home (spawn region or original placement). */\n dock: boolean;\n /** Minimize / restore toggle. */\n minimize: boolean;\n /** Close (destroy) the window. */\n close: boolean;\n}\n\nexport const NO_CHROME: Readonly<WindowChrome> = Object.freeze({\n pin: false,\n dock: false,\n minimize: false,\n close: false,\n});\n\nexport interface WindowRecord {\n id: string;\n title: string;\n dockMode: DockModeValue;\n minimized: boolean;\n /** Not drawn and not hittable; everything else about the window is kept. */\n hidden: boolean;\n /** True while the user is actively dragging the window by its title bar. */\n dragging: boolean;\n /** The layout region the window is docked into, if any. */\n region: string | undefined;\n /** Which title-bar buttons are enabled. */\n chrome: WindowChrome;\n /** Hand, anchor and palm gate used while the window is `hand-locked`. */\n handMenu: HandMenuOptions;\n}\n\nexport interface WindowManagerEvents extends Record<string, unknown> {\n opened: WindowRecord;\n closed: WindowRecord;\n focused: WindowRecord;\n minimized: WindowRecord;\n restored: WindowRecord;\n hidden: WindowRecord;\n shown: WindowRecord;\n dockChanged: { window: WindowRecord; previous: DockModeValue };\n /** The window entered, left or moved between regions. */\n regionChanged: { window: WindowRecord; previous: string | undefined };\n /** The app asked for the window to go back to where it spawned. */\n returnHome: WindowRecord;\n chromeChanged: { window: WindowRecord; previous: WindowChrome };\n handMenuChanged: { window: WindowRecord; previous: HandMenuOptions };\n dragStarted: WindowRecord;\n dragEnded: WindowRecord;\n}\n\nexport interface OpenWindowOptions {\n title?: string;\n dockMode?: DockModeValue;\n /** Open hidden; `show()` reveals it. */\n hidden?: boolean;\n /** Open docked into this region. */\n region?: string;\n /** Buttons to enable; anything omitted stays off. */\n chrome?: Partial<WindowChrome>;\n /** Hand-menu placement, used when `dockMode` is (or becomes) `hand-locked`. */\n handMenu?: Partial<HandMenuOptions>;\n}\n\n/**\n * What the pin affordance should read for a window's current state:\n * - dragging → \"PIN\" (the window is loose in your hand)\n * - placed/world-locked → \"UNPIN\" (click releases it to follow)\n * - following → \"PIN\" (click pins it where it is)\n */\nexport function pinLabelFor(record: Pick<WindowRecord, 'dockMode' | 'dragging'>): 'PIN' | 'UNPIN' {\n if (record.dragging) {\n return 'PIN';\n }\n return record.dockMode === DockMode.WorldLocked ? 'UNPIN' : 'PIN';\n}\n\n/**\n * What the minimize affordance should read for a window's current state -\n * the label always names what the NEXT click does, matching `pinLabelFor`:\n * - open → \"MIN\" (click collapses the content)\n * - minimized → \"MAX\" (click restores it)\n */\nexport function minimizeLabelFor(record: Pick<WindowRecord, 'minimized'>): 'MIN' | 'MAX' {\n return record.minimized ? 'MAX' : 'MIN';\n}\n\nexport class WindowManager {\n readonly events = new Emitter<WindowManagerEvents>();\n\n private windows = new Map<string, WindowRecord>();\n /** Most-recently-focused last (top of the stack). */\n private focusStack: string[] = [];\n\n open(id: string, options: OpenWindowOptions = {}): WindowRecord {\n if (this.windows.has(id)) {\n throw new Error(`[uix] window \"${id}\" is already open`);\n }\n const dockMode = options.dockMode ?? DockMode.WorldLocked;\n if (!isDockMode(dockMode)) {\n throw new Error(`[uix] \"${String(dockMode)}\" is not a dock mode`);\n }\n const record: WindowRecord = {\n id,\n title: options.title ?? id,\n dockMode,\n minimized: false,\n hidden: options.hidden ?? false,\n dragging: false,\n region: options.region,\n chrome: { ...NO_CHROME, ...options.chrome },\n handMenu: resolveHandMenu(options.handMenu),\n };\n this.windows.set(id, record);\n this.focusStack.push(id);\n this.events.emit('opened', record);\n this.events.emit('focused', record);\n return record;\n }\n\n /**\n * Close a window. This is the one teardown call: adapters listen for\n * `closed` and dispose whatever they created for the window.\n */\n close(id: string): void {\n const record = this.require(id);\n this.windows.delete(id);\n this.focusStack = this.focusStack.filter((w) => w !== id);\n this.events.emit('closed', record);\n const top = this.focused;\n if (top) {\n this.events.emit('focused', top);\n }\n }\n\n focus(id: string): void {\n const record = this.require(id);\n const top = this.focusStack[this.focusStack.length - 1];\n if (top === id) {\n return;\n }\n this.focusStack = this.focusStack.filter((w) => w !== id);\n this.focusStack.push(id);\n this.events.emit('focused', record);\n }\n\n minimize(id: string): void {\n const record = this.require(id);\n if (record.minimized) {\n return;\n }\n record.minimized = true;\n this.events.emit('minimized', record);\n }\n\n restore(id: string): void {\n const record = this.require(id);\n if (!record.minimized) {\n return;\n }\n record.minimized = false;\n this.events.emit('restored', record);\n this.focus(id);\n }\n\n toggleMinimized(id: string): void {\n if (this.require(id).minimized) {\n this.restore(id);\n } else {\n this.minimize(id);\n }\n }\n\n /**\n * Take a window out of view without closing it. Dock mode, region slot and\n * minimized state are all kept, so `show()` brings it back exactly where\n * it was. A hidden docked window keeps its slot.\n */\n hide(id: string): void {\n const record = this.require(id);\n if (record.hidden) {\n return;\n }\n record.hidden = true;\n this.events.emit('hidden', record);\n }\n\n /** Reveal a hidden window and bring it to the front. */\n show(id: string): void {\n const record = this.require(id);\n if (!record.hidden) {\n return;\n }\n record.hidden = false;\n this.events.emit('shown', record);\n this.focus(id);\n }\n\n toggleHidden(id: string): void {\n if (this.require(id).hidden) {\n this.show(id);\n } else {\n this.hide(id);\n }\n }\n\n setDockMode(id: string, mode: DockModeValue): void {\n if (!isDockMode(mode)) {\n throw new Error(`[uix] \"${String(mode)}\" is not a dock mode`);\n }\n const record = this.require(id);\n if (record.dockMode === mode) {\n return;\n }\n const previous = record.dockMode;\n record.dockMode = mode;\n this.events.emit('dockChanged', { window: record, previous });\n }\n\n /**\n * Dock a window into a layout region. The manager records the intent and\n * emits `regionChanged`; the adapter places the window in a slot (and may\n * call `undock` back if the region is full or unknown).\n */\n dockTo(id: string, regionId: string): void {\n if (!regionId) {\n throw new Error('[uix] dockTo needs a region id; use undock() to leave a region');\n }\n const record = this.require(id);\n if (record.region === regionId) {\n return;\n }\n const previous = record.region;\n record.region = regionId;\n this.events.emit('regionChanged', { window: record, previous });\n }\n\n /** Take a window out of its region. No-op when it is not docked. */\n undock(id: string): void {\n const record = this.require(id);\n if (record.region === undefined) {\n return;\n }\n const previous = record.region;\n record.region = undefined;\n this.events.emit('regionChanged', { window: record, previous });\n }\n\n /**\n * Ask for the window to go back to where it spawned: its spawn region, or\n * its original placement and dock mode. The adapter owns that snapshot,\n * so this only emits `returnHome`; this is also what the DOCK title-bar\n * button does.\n */\n returnHome(id: string): void {\n this.events.emit('returnHome', this.require(id));\n }\n\n /** Enable or disable title-bar buttons after the window is open. */\n setChrome(id: string, chrome: Partial<WindowChrome>): void {\n const record = this.require(id);\n const next: WindowChrome = { ...record.chrome, ...chrome };\n if (\n next.pin === record.chrome.pin &&\n next.dock === record.chrome.dock &&\n next.minimize === record.chrome.minimize &&\n next.close === record.chrome.close\n ) {\n return;\n }\n const previous = record.chrome;\n record.chrome = next;\n this.events.emit('chromeChanged', { window: record, previous });\n }\n\n /**\n * Change where a hand menu rides: the hand, the anchor, the palm gate. Takes\n * effect at once if the window is `hand-locked`, and is remembered otherwise.\n */\n setHandMenu(id: string, options: Partial<HandMenuOptions>): void {\n const record = this.require(id);\n const next = resolveHandMenu({ ...record.handMenu, ...options });\n const same =\n next.hand === record.handMenu.hand &&\n next.anchor === record.handMenu.anchor &&\n next.anchorDistance === record.handMenu.anchorDistance &&\n next.palmGate === record.handMenu.palmGate &&\n next.palmAngle === record.handMenu.palmAngle &&\n next.offset.every((value, index) => value === record.handMenu.offset[index]);\n if (same) {\n return;\n }\n const previous = record.handMenu;\n record.handMenu = next;\n this.events.emit('handMenuChanged', { window: record, previous });\n }\n\n /** Track an active title-bar drag; emits dragStarted/dragEnded on change. */\n setDragging(id: string, dragging: boolean): void {\n const record = this.require(id);\n if (record.dragging === dragging) {\n return;\n }\n record.dragging = dragging;\n this.events.emit(dragging ? 'dragStarted' : 'dragEnded', record);\n }\n\n /** Title-bar pin button behaviour: place in space ↔ follow the player. */\n togglePin(id: string): DockModeValue {\n const next = togglePinned(this.require(id).dockMode);\n this.setDockMode(id, next);\n return next;\n }\n\n get(id: string): WindowRecord | undefined {\n return this.windows.get(id);\n }\n\n has(id: string): boolean {\n return this.windows.has(id);\n }\n\n get focused(): WindowRecord | undefined {\n const top = this.focusStack[this.focusStack.length - 1];\n return top === undefined ? undefined : this.windows.get(top);\n }\n\n /**\n * Focus depth of a window: 0 = focused (topmost), 1 = next, and so on.\n * The ECS layer converts this into a small z bias toward the viewer so\n * overlapping panels resolve in focus order.\n */\n orderOf(id: string): number {\n this.require(id);\n // focusStack is bottom→top; depth counts down from the top.\n const index = this.focusStack.lastIndexOf(id);\n return this.focusStack.length - 1 - index;\n }\n\n get count(): number {\n return this.windows.size;\n }\n\n list(): WindowRecord[] {\n // Top-of-stack first - the natural order for \"window list\" UIs.\n return [...this.focusStack]\n .reverse()\n .map((id) => this.windows.get(id))\n .filter((record): record is WindowRecord => record !== undefined);\n }\n\n private require(id: string): WindowRecord {\n const record = this.windows.get(id);\n if (!record) {\n throw new Error(`[uix] unknown window \"${id}\"`);\n }\n return record;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"window-manager.js","sourceRoot":"","sources":["../../src/core/window-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * WindowManager - pure window registry, focus ordering and window state.\n *\n * The manager knows nothing about entities, three.js or uikit. It deals in\n * opaque window ids and answers the questions the engine layer applies each\n * frame:\n *\n * 1. What is the focus (z) order? (`orderOf` → depth bias so the focused\n * window renders nearest the user and receives pointer priority)\n * 2. What state is a window in? (minimized / hidden / focused / dock mode /\n * region / which chrome buttons are enabled)\n *\n * It is also the ONE API app code calls to change a window: a hand menu that\n * hides, docks or pins a targeted window talks to the manager, and every\n * adapter applies the resulting events. Nothing here needs an engine handle.\n *\n * A window in a region is always world-locked: the region places it, so it\n * cannot also follow the viewer. Opening into a region or `dockTo` makes it\n * world-locked, and a follow mode set through `setDockMode` or `togglePin`\n * takes it out of its region first. The manager holds this rule so every\n * platform lays out the same descriptor the same way.\n */\nimport { Emitter } from './events.js';\nimport { DockMode, DockModeValue, isDockMode, togglePinned } from './dock-state.js';\nimport { resolveHandMenu, type HandMenuOptions } from './hand-menu.js';\n\n/**\n * Which title-bar buttons are enabled. Keys match the chrome element ids in\n * `WINDOW_CHROME_IDS`. A disabled button is hidden and its click ignored.\n * Every button is OFF unless the app turns it on.\n */\nexport interface WindowChrome {\n /** Body-follow ⇄ world-locked toggle. */\n pin: boolean;\n /** Return the window to its home (spawn region or original placement). */\n dock: boolean;\n /** Minimize / restore toggle. */\n minimize: boolean;\n /** Close (destroy) the window. */\n close: boolean;\n}\n\nexport const NO_CHROME: Readonly<WindowChrome> = Object.freeze({\n pin: false,\n dock: false,\n minimize: false,\n close: false,\n});\n\nexport interface WindowRecord {\n id: string;\n title: string;\n dockMode: DockModeValue;\n minimized: boolean;\n /** Not drawn and not hittable; everything else about the window is kept. */\n hidden: boolean;\n /** True while the user is actively dragging the window by its title bar. */\n dragging: boolean;\n /** The layout region the window is docked into, if any. */\n region: string | undefined;\n /** Which title-bar buttons are enabled. */\n chrome: WindowChrome;\n /** Hand, anchor and palm gate used while the window is `hand-locked`. */\n handMenu: HandMenuOptions;\n}\n\nexport interface WindowManagerEvents extends Record<string, unknown> {\n opened: WindowRecord;\n closed: WindowRecord;\n focused: WindowRecord;\n minimized: WindowRecord;\n restored: WindowRecord;\n hidden: WindowRecord;\n shown: WindowRecord;\n dockChanged: { window: WindowRecord; previous: DockModeValue };\n /** The window entered, left or moved between regions. */\n regionChanged: { window: WindowRecord; previous: string | undefined };\n /** The app asked for the window to go back to where it spawned. */\n returnHome: WindowRecord;\n chromeChanged: { window: WindowRecord; previous: WindowChrome };\n handMenuChanged: { window: WindowRecord; previous: HandMenuOptions };\n dragStarted: WindowRecord;\n dragEnded: WindowRecord;\n}\n\nexport interface OpenWindowOptions {\n title?: string;\n dockMode?: DockModeValue;\n /** Open hidden; `show()` reveals it. */\n hidden?: boolean;\n /** Open docked into this region. The window is then world-locked, whatever `dockMode` says. */\n region?: string;\n /** Buttons to enable; anything omitted stays off. */\n chrome?: Partial<WindowChrome>;\n /** Hand-menu placement, used when `dockMode` is (or becomes) `hand-locked`. */\n handMenu?: Partial<HandMenuOptions>;\n}\n\n/**\n * What the pin affordance should read for a window's current state:\n * - dragging → \"PIN\" (the window is loose in your hand)\n * - placed/world-locked → \"UNPIN\" (click releases it to follow)\n * - following → \"PIN\" (click pins it where it is)\n */\nexport function pinLabelFor(record: Pick<WindowRecord, 'dockMode' | 'dragging'>): 'PIN' | 'UNPIN' {\n if (record.dragging) {\n return 'PIN';\n }\n return record.dockMode === DockMode.WorldLocked ? 'UNPIN' : 'PIN';\n}\n\n/**\n * What the minimize affordance should read for a window's current state -\n * the label always names what the NEXT click does, matching `pinLabelFor`:\n * - open → \"MIN\" (click collapses the content)\n * - minimized → \"MAX\" (click restores it)\n */\nexport function minimizeLabelFor(record: Pick<WindowRecord, 'minimized'>): 'MIN' | 'MAX' {\n return record.minimized ? 'MAX' : 'MIN';\n}\n\nexport class WindowManager {\n readonly events = new Emitter<WindowManagerEvents>();\n\n private windows = new Map<string, WindowRecord>();\n /** Most-recently-focused last (top of the stack). */\n private focusStack: string[] = [];\n\n open(id: string, options: OpenWindowOptions = {}): WindowRecord {\n if (this.windows.has(id)) {\n throw new Error(`[uix] window \"${id}\" is already open`);\n }\n const requested = options.dockMode ?? DockMode.WorldLocked;\n if (!isDockMode(requested)) {\n throw new Error(`[uix] \"${String(requested)}\" is not a dock mode`);\n }\n const record: WindowRecord = {\n id,\n title: options.title ?? id,\n dockMode: options.region !== undefined ? DockMode.WorldLocked : requested,\n minimized: false,\n hidden: options.hidden ?? false,\n dragging: false,\n region: options.region,\n chrome: { ...NO_CHROME, ...options.chrome },\n handMenu: resolveHandMenu(options.handMenu),\n };\n this.windows.set(id, record);\n this.focusStack.push(id);\n this.events.emit('opened', record);\n this.events.emit('focused', record);\n return record;\n }\n\n /**\n * Close a window. This is the one teardown call: adapters listen for\n * `closed` and dispose whatever they created for the window.\n */\n close(id: string): void {\n const record = this.require(id);\n this.windows.delete(id);\n this.focusStack = this.focusStack.filter((w) => w !== id);\n this.events.emit('closed', record);\n const top = this.focused;\n if (top) {\n this.events.emit('focused', top);\n }\n }\n\n focus(id: string): void {\n const record = this.require(id);\n const top = this.focusStack[this.focusStack.length - 1];\n if (top === id) {\n return;\n }\n this.focusStack = this.focusStack.filter((w) => w !== id);\n this.focusStack.push(id);\n this.events.emit('focused', record);\n }\n\n minimize(id: string): void {\n const record = this.require(id);\n if (record.minimized) {\n return;\n }\n record.minimized = true;\n this.events.emit('minimized', record);\n }\n\n restore(id: string): void {\n const record = this.require(id);\n if (!record.minimized) {\n return;\n }\n record.minimized = false;\n this.events.emit('restored', record);\n this.focus(id);\n }\n\n toggleMinimized(id: string): void {\n if (this.require(id).minimized) {\n this.restore(id);\n } else {\n this.minimize(id);\n }\n }\n\n /**\n * Take a window out of view without closing it. Dock mode, region slot and\n * minimized state are all kept, so `show()` brings it back exactly where\n * it was. A hidden docked window keeps its slot.\n */\n hide(id: string): void {\n const record = this.require(id);\n if (record.hidden) {\n return;\n }\n record.hidden = true;\n this.events.emit('hidden', record);\n }\n\n /** Reveal a hidden window and bring it to the front. */\n show(id: string): void {\n const record = this.require(id);\n if (!record.hidden) {\n return;\n }\n record.hidden = false;\n this.events.emit('shown', record);\n this.focus(id);\n }\n\n toggleHidden(id: string): void {\n if (this.require(id).hidden) {\n this.show(id);\n } else {\n this.hide(id);\n }\n }\n\n setDockMode(id: string, mode: DockModeValue): void {\n if (!isDockMode(mode)) {\n throw new Error(`[uix] \"${String(mode)}\" is not a dock mode`);\n }\n const record = this.require(id);\n if (record.dockMode === mode) {\n return;\n }\n if (mode !== DockMode.WorldLocked) {\n // A region places its windows, so a window that follows cannot stay in one.\n this.undock(id);\n }\n const previous = record.dockMode;\n record.dockMode = mode;\n this.events.emit('dockChanged', { window: record, previous });\n }\n\n /**\n * Dock a window into a layout region. The manager records the intent,\n * makes the window world-locked, and emits `regionChanged` then, if the\n * mode changed, `dockChanged`. The adapter places the window in a slot\n * (and may call `undock` back if the region is full or unknown).\n */\n dockTo(id: string, regionId: string): void {\n if (!regionId) {\n throw new Error('[uix] dockTo needs a region id; use undock() to leave a region');\n }\n const record = this.require(id);\n if (record.region === regionId) {\n return;\n }\n const previous = record.region;\n const previousMode = record.dockMode;\n record.region = regionId;\n record.dockMode = DockMode.WorldLocked;\n this.events.emit('regionChanged', { window: record, previous });\n if (previousMode !== DockMode.WorldLocked) {\n this.events.emit('dockChanged', { window: record, previous: previousMode });\n }\n }\n\n /** Take a window out of its region. No-op when it is not docked. */\n undock(id: string): void {\n const record = this.require(id);\n if (record.region === undefined) {\n return;\n }\n const previous = record.region;\n record.region = undefined;\n this.events.emit('regionChanged', { window: record, previous });\n }\n\n /**\n * Ask for the window to go back to where it spawned: its spawn region, or\n * its original placement and dock mode. The adapter owns that snapshot,\n * so this only emits `returnHome`; this is also what the DOCK title-bar\n * button does.\n */\n returnHome(id: string): void {\n this.events.emit('returnHome', this.require(id));\n }\n\n /** Enable or disable title-bar buttons after the window is open. */\n setChrome(id: string, chrome: Partial<WindowChrome>): void {\n const record = this.require(id);\n const next: WindowChrome = { ...record.chrome, ...chrome };\n if (\n next.pin === record.chrome.pin &&\n next.dock === record.chrome.dock &&\n next.minimize === record.chrome.minimize &&\n next.close === record.chrome.close\n ) {\n return;\n }\n const previous = record.chrome;\n record.chrome = next;\n this.events.emit('chromeChanged', { window: record, previous });\n }\n\n /**\n * Change where a hand menu rides: the hand, the anchor, the palm gate. Takes\n * effect at once if the window is `hand-locked`, and is remembered otherwise.\n */\n setHandMenu(id: string, options: Partial<HandMenuOptions>): void {\n const record = this.require(id);\n const next = resolveHandMenu({ ...record.handMenu, ...options });\n const same =\n next.hand === record.handMenu.hand &&\n next.anchor === record.handMenu.anchor &&\n next.anchorDistance === record.handMenu.anchorDistance &&\n next.palmGate === record.handMenu.palmGate &&\n next.palmAngle === record.handMenu.palmAngle &&\n next.offset.every((value, index) => value === record.handMenu.offset[index]);\n if (same) {\n return;\n }\n const previous = record.handMenu;\n record.handMenu = next;\n this.events.emit('handMenuChanged', { window: record, previous });\n }\n\n /** Track an active title-bar drag; emits dragStarted/dragEnded on change. */\n setDragging(id: string, dragging: boolean): void {\n const record = this.require(id);\n if (record.dragging === dragging) {\n return;\n }\n record.dragging = dragging;\n this.events.emit(dragging ? 'dragStarted' : 'dragEnded', record);\n }\n\n /** Title-bar pin button behaviour: place in space ↔ follow the player. */\n togglePin(id: string): DockModeValue {\n const next = togglePinned(this.require(id).dockMode);\n this.setDockMode(id, next);\n return next;\n }\n\n get(id: string): WindowRecord | undefined {\n return this.windows.get(id);\n }\n\n has(id: string): boolean {\n return this.windows.has(id);\n }\n\n get focused(): WindowRecord | undefined {\n const top = this.focusStack[this.focusStack.length - 1];\n return top === undefined ? undefined : this.windows.get(top);\n }\n\n /**\n * Focus depth of a window: 0 = focused (topmost), 1 = next, and so on.\n * The ECS layer converts this into a small z bias toward the viewer so\n * overlapping panels resolve in focus order.\n */\n orderOf(id: string): number {\n this.require(id);\n // focusStack is bottom→top; depth counts down from the top.\n const index = this.focusStack.lastIndexOf(id);\n return this.focusStack.length - 1 - index;\n }\n\n get count(): number {\n return this.windows.size;\n }\n\n list(): WindowRecord[] {\n // Top-of-stack first - the natural order for \"window list\" UIs.\n return [...this.focusStack]\n .reverse()\n .map((id) => this.windows.get(id))\n .filter((record): record is WindowRecord => record !== undefined);\n }\n\n private require(id: string): WindowRecord {\n const record = this.windows.get(id);\n if (!record) {\n throw new Error(`[uix] unknown window \"${id}\"`);\n }\n return record;\n }\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `UixElement` contract, shipped as data rather than as tests.
|
|
3
|
+
*
|
|
4
|
+
* The core's controls (`<uix-stepper>`, `<uix-toggle>` and the rest) never
|
|
5
|
+
* see a platform. They walk a panel's elements through `UixElement`: its
|
|
6
|
+
* `children`, its `userData`, `setProperties` and `addEventListener`. uikit
|
|
7
|
+
* implements that on the web platforms, and native implements it with proxy
|
|
8
|
+
* elements over the native app's own. A control only behaves the same on
|
|
9
|
+
* every platform if every one builds the same elements from the same markup,
|
|
10
|
+
* so these cases check that, ending with a real stepper driven by clicks.
|
|
11
|
+
*
|
|
12
|
+
* Every subject builds its tree from {@link CONTRACT_PANEL_MARKUP}, however
|
|
13
|
+
* the platform turns markup into elements. The suite is runner-free, like
|
|
14
|
+
* `windowHostContractCases()`: each case returns on success and throws a
|
|
15
|
+
* plain `Error` otherwise.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* for (const contractCase of uixElementContractCases()) {
|
|
19
|
+
* it(contractCase.name, () => contractCase.run(makeSubject()));
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { type UixElement } from './controls/element.js';
|
|
24
|
+
/**
|
|
25
|
+
* The panel every subject builds. It uses only tags every platform's parser
|
|
26
|
+
* accepts: IWSDK's rejects `<text>`, so the plain element is a `<span>`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CONTRACT_PANEL_MARKUP = "<div>\n <uix-stepper data-uix-id=\"contract-count\" data-uix-min=\"0\" data-uix-max=\"3\" data-uix-step=\"1\" data-uix-value=\"2\" data-uix-chars-per-line=\"4\">\n <uix-decrement>-</uix-decrement>\n <uix-value>.</uix-value>\n <uix-increment>+</uix-increment>\n </uix-stepper>\n <span>plain</span>\n</div>";
|
|
29
|
+
/** How a case observes and pokes the platform's real elements. */
|
|
30
|
+
export interface UixElementContractDriver {
|
|
31
|
+
/** The value of `property` the platform is showing for `element` now. */
|
|
32
|
+
rendered(element: UixElement, property: string): unknown;
|
|
33
|
+
/** Deliver a user event of `type` to `element`, the way the platform does. */
|
|
34
|
+
fire(element: UixElement, type: string): void;
|
|
35
|
+
}
|
|
36
|
+
/** A panel built from {@link CONTRACT_PANEL_MARKUP}. Build a FRESH one per case. */
|
|
37
|
+
export interface UixElementContractSubject {
|
|
38
|
+
root: UixElement;
|
|
39
|
+
drive: UixElementContractDriver;
|
|
40
|
+
}
|
|
41
|
+
/** One check a platform's `UixElement`s must pass. */
|
|
42
|
+
export interface UixElementContractCase {
|
|
43
|
+
name: string;
|
|
44
|
+
run(subject: UixElementContractSubject): void;
|
|
45
|
+
}
|
|
46
|
+
/** The shared `UixElement` conformance suite. */
|
|
47
|
+
export declare function uixElementContractCases(): readonly UixElementContractCase[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `UixElement` contract, shipped as data rather than as tests.
|
|
3
|
+
*
|
|
4
|
+
* The core's controls (`<uix-stepper>`, `<uix-toggle>` and the rest) never
|
|
5
|
+
* see a platform. They walk a panel's elements through `UixElement`: its
|
|
6
|
+
* `children`, its `userData`, `setProperties` and `addEventListener`. uikit
|
|
7
|
+
* implements that on the web platforms, and native implements it with proxy
|
|
8
|
+
* elements over the native app's own. A control only behaves the same on
|
|
9
|
+
* every platform if every one builds the same elements from the same markup,
|
|
10
|
+
* so these cases check that, ending with a real stepper driven by clicks.
|
|
11
|
+
*
|
|
12
|
+
* Every subject builds its tree from {@link CONTRACT_PANEL_MARKUP}, however
|
|
13
|
+
* the platform turns markup into elements. The suite is runner-free, like
|
|
14
|
+
* `windowHostContractCases()`: each case returns on success and throws a
|
|
15
|
+
* plain `Error` otherwise.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* for (const contractCase of uixElementContractCases()) {
|
|
19
|
+
* it(contractCase.name, () => contractCase.run(makeSubject()));
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import { attrNumber, attrString, findRole, tagOf } from './controls/element.js';
|
|
24
|
+
import { upgradePanel } from './controls/upgrade.js';
|
|
25
|
+
/**
|
|
26
|
+
* The panel every subject builds. It uses only tags every platform's parser
|
|
27
|
+
* accepts: IWSDK's rejects `<text>`, so the plain element is a `<span>`.
|
|
28
|
+
*/
|
|
29
|
+
export const CONTRACT_PANEL_MARKUP = `<div>
|
|
30
|
+
<uix-stepper data-uix-id="contract-count" data-uix-min="0" data-uix-max="3" data-uix-step="1" data-uix-value="2" data-uix-chars-per-line="4">
|
|
31
|
+
<uix-decrement>-</uix-decrement>
|
|
32
|
+
<uix-value>.</uix-value>
|
|
33
|
+
<uix-increment>+</uix-increment>
|
|
34
|
+
</uix-stepper>
|
|
35
|
+
<span>plain</span>
|
|
36
|
+
</div>`;
|
|
37
|
+
function assert(condition, message) {
|
|
38
|
+
if (!condition)
|
|
39
|
+
throw new Error(message);
|
|
40
|
+
}
|
|
41
|
+
function stepperOf(root) {
|
|
42
|
+
const stepper = findRole(root, 'stepper');
|
|
43
|
+
assert(stepper !== undefined, 'the panel must hold a <uix-stepper> readable through tagOf');
|
|
44
|
+
return stepper;
|
|
45
|
+
}
|
|
46
|
+
function partOf(root, role) {
|
|
47
|
+
const part = findRole(root, role);
|
|
48
|
+
assert(part !== undefined, `the panel must hold a <uix-${role}> readable through tagOf`);
|
|
49
|
+
return part;
|
|
50
|
+
}
|
|
51
|
+
const CASES = [
|
|
52
|
+
{
|
|
53
|
+
name: 'children mirror the markup, in order',
|
|
54
|
+
run({ root }) {
|
|
55
|
+
assert(root.children.length === 2, `the panel root must have 2 children, got ${String(root.children.length)}`);
|
|
56
|
+
const parts = stepperOf(root).children.map((child) => tagOf(child));
|
|
57
|
+
assert(parts.join(',') === 'uix-decrement,uix-value,uix-increment', `the stepper's children must be decrement, value, increment in order, got [${parts.join(', ')}]`);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'tagOf reads a declared uix-* tag and nothing for a built-in one',
|
|
62
|
+
run({ root }) {
|
|
63
|
+
const [first, second] = root.children;
|
|
64
|
+
assert(first !== undefined && second !== undefined, 'the panel root must have 2 children');
|
|
65
|
+
assert(tagOf(first) === 'uix-stepper', `tagOf must read "uix-stepper", got ${JSON.stringify(tagOf(first))}`);
|
|
66
|
+
assert(tagOf(second) === undefined, `tagOf must read nothing for a <span>, got ${JSON.stringify(tagOf(second))}`);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'data-uix-* attributes arrive in userData, camelCased',
|
|
71
|
+
run({ root }) {
|
|
72
|
+
const stepper = stepperOf(root);
|
|
73
|
+
const id = attrString(stepper, 'uixId');
|
|
74
|
+
assert(id === 'contract-count', `data-uix-id must read as userData.uixId "contract-count", got ${JSON.stringify(id)}`);
|
|
75
|
+
const max = attrNumber(stepper, 'uixMax');
|
|
76
|
+
assert(max === 3, `data-uix-max must read as userData.uixMax 3, got ${JSON.stringify(max)}`);
|
|
77
|
+
const chars = attrNumber(stepper, 'uixCharsPerLine');
|
|
78
|
+
assert(chars === 4, `data-uix-chars-per-line must read as userData.uixCharsPerLine 4, got ${JSON.stringify(chars)}`);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'setProperties reaches what the platform shows',
|
|
83
|
+
run({ root, drive }) {
|
|
84
|
+
const value = partOf(root, 'value');
|
|
85
|
+
value.setProperties({ text: 'contract' });
|
|
86
|
+
const shown = drive.rendered(value, 'text');
|
|
87
|
+
assert(shown === 'contract', `setProperties({ text }) must be what the platform shows, got ${JSON.stringify(shown)}`);
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'a user event reaches every listener on that element and no other',
|
|
92
|
+
run({ root, drive }) {
|
|
93
|
+
const increment = partOf(root, 'increment');
|
|
94
|
+
const decrement = partOf(root, 'decrement');
|
|
95
|
+
let first = 0;
|
|
96
|
+
let second = 0;
|
|
97
|
+
let other = 0;
|
|
98
|
+
increment.addEventListener('click', () => (first += 1));
|
|
99
|
+
increment.addEventListener('click', () => (second += 1));
|
|
100
|
+
decrement.addEventListener('click', () => (other += 1));
|
|
101
|
+
drive.fire(increment, 'click');
|
|
102
|
+
assert(first === 1 && second === 1, `one click must reach both listeners once, got ${String(first)} and ${String(second)}`);
|
|
103
|
+
assert(other === 0, `a click on one element must not reach another's listener, got ${String(other)}`);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'a core stepper upgrades from the markup and follows clicks',
|
|
108
|
+
run({ root, drive }) {
|
|
109
|
+
const controls = upgradePanel({}, root);
|
|
110
|
+
const stepper = controls.get('contract-count');
|
|
111
|
+
assert(stepper !== undefined, `upgradePanel must register the stepper as "contract-count", got [${controls.ids().join(', ')}]`);
|
|
112
|
+
const value = partOf(root, 'value');
|
|
113
|
+
const increment = partOf(root, 'increment');
|
|
114
|
+
const shownAtStart = drive.rendered(value, 'text');
|
|
115
|
+
assert(shownAtStart === '2', `the stepper must start at data-uix-value 2, showing ${JSON.stringify(shownAtStart)}`);
|
|
116
|
+
drive.fire(increment, 'click');
|
|
117
|
+
drive.fire(increment, 'click');
|
|
118
|
+
const shown = drive.rendered(value, 'text');
|
|
119
|
+
assert(shown === '3', `two increments from 2 must stop at data-uix-max 3, showing ${JSON.stringify(shown)}`);
|
|
120
|
+
const opacity = drive.rendered(increment, 'opacity');
|
|
121
|
+
assert(typeof opacity === 'number' && opacity < 1, `the increment must dim at the maximum, opacity ${JSON.stringify(opacity)}`);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
/** The shared `UixElement` conformance suite. */
|
|
126
|
+
export function uixElementContractCases() {
|
|
127
|
+
return CASES;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=element-contract-cases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"element-contract-cases.js","sourceRoot":"","sources":["../src/element-contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared `UixElement` contract, shipped as data rather than as tests.\n *\n * The core's controls (`<uix-stepper>`, `<uix-toggle>` and the rest) never\n * see a platform. They walk a panel's elements through `UixElement`: its\n * `children`, its `userData`, `setProperties` and `addEventListener`. uikit\n * implements that on the web platforms, and native implements it with proxy\n * elements over the native app's own. A control only behaves the same on\n * every platform if every one builds the same elements from the same markup,\n * so these cases check that, ending with a real stepper driven by clicks.\n *\n * Every subject builds its tree from {@link CONTRACT_PANEL_MARKUP}, however\n * the platform turns markup into elements. The suite is runner-free, like\n * `windowHostContractCases()`: each case returns on success and throws a\n * plain `Error` otherwise.\n *\n * ```ts\n * for (const contractCase of uixElementContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSubject()));\n * }\n * ```\n */\nimport { attrNumber, attrString, findRole, tagOf, type UixElement } from './controls/element.js';\nimport { upgradePanel } from './controls/upgrade.js';\n\n/**\n * The panel every subject builds. It uses only tags every platform's parser\n * accepts: IWSDK's rejects `<text>`, so the plain element is a `<span>`.\n */\nexport const CONTRACT_PANEL_MARKUP = `<div>\n <uix-stepper data-uix-id=\"contract-count\" data-uix-min=\"0\" data-uix-max=\"3\" data-uix-step=\"1\" data-uix-value=\"2\" data-uix-chars-per-line=\"4\">\n <uix-decrement>-</uix-decrement>\n <uix-value>.</uix-value>\n <uix-increment>+</uix-increment>\n </uix-stepper>\n <span>plain</span>\n</div>`;\n\n/** How a case observes and pokes the platform's real elements. */\nexport interface UixElementContractDriver {\n /** The value of `property` the platform is showing for `element` now. */\n rendered(element: UixElement, property: string): unknown;\n /** Deliver a user event of `type` to `element`, the way the platform does. */\n fire(element: UixElement, type: string): void;\n}\n\n/** A panel built from {@link CONTRACT_PANEL_MARKUP}. Build a FRESH one per case. */\nexport interface UixElementContractSubject {\n root: UixElement;\n drive: UixElementContractDriver;\n}\n\n/** One check a platform's `UixElement`s must pass. */\nexport interface UixElementContractCase {\n name: string;\n run(subject: UixElementContractSubject): void;\n}\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nfunction stepperOf(root: UixElement): UixElement {\n const stepper = findRole(root, 'stepper');\n assert(stepper !== undefined, 'the panel must hold a <uix-stepper> readable through tagOf');\n return stepper;\n}\n\nfunction partOf(root: UixElement, role: string): UixElement {\n const part = findRole(root, role);\n assert(part !== undefined, `the panel must hold a <uix-${role}> readable through tagOf`);\n return part;\n}\n\nconst CASES: readonly UixElementContractCase[] = [\n {\n name: 'children mirror the markup, in order',\n run({ root }) {\n assert(root.children.length === 2, `the panel root must have 2 children, got ${String(root.children.length)}`);\n const parts = stepperOf(root).children.map((child) => tagOf(child));\n assert(\n parts.join(',') === 'uix-decrement,uix-value,uix-increment',\n `the stepper's children must be decrement, value, increment in order, got [${parts.join(', ')}]`,\n );\n },\n },\n {\n name: 'tagOf reads a declared uix-* tag and nothing for a built-in one',\n run({ root }) {\n const [first, second] = root.children;\n assert(first !== undefined && second !== undefined, 'the panel root must have 2 children');\n assert(tagOf(first) === 'uix-stepper', `tagOf must read \"uix-stepper\", got ${JSON.stringify(tagOf(first))}`);\n assert(tagOf(second) === undefined, `tagOf must read nothing for a <span>, got ${JSON.stringify(tagOf(second))}`);\n },\n },\n {\n name: 'data-uix-* attributes arrive in userData, camelCased',\n run({ root }) {\n const stepper = stepperOf(root);\n const id = attrString(stepper, 'uixId');\n assert(id === 'contract-count', `data-uix-id must read as userData.uixId \"contract-count\", got ${JSON.stringify(id)}`);\n const max = attrNumber(stepper, 'uixMax');\n assert(max === 3, `data-uix-max must read as userData.uixMax 3, got ${JSON.stringify(max)}`);\n const chars = attrNumber(stepper, 'uixCharsPerLine');\n assert(chars === 4, `data-uix-chars-per-line must read as userData.uixCharsPerLine 4, got ${JSON.stringify(chars)}`);\n },\n },\n {\n name: 'setProperties reaches what the platform shows',\n run({ root, drive }) {\n const value = partOf(root, 'value');\n value.setProperties({ text: 'contract' });\n const shown = drive.rendered(value, 'text');\n assert(shown === 'contract', `setProperties({ text }) must be what the platform shows, got ${JSON.stringify(shown)}`);\n },\n },\n {\n name: 'a user event reaches every listener on that element and no other',\n run({ root, drive }) {\n const increment = partOf(root, 'increment');\n const decrement = partOf(root, 'decrement');\n let first = 0;\n let second = 0;\n let other = 0;\n increment.addEventListener('click', () => (first += 1));\n increment.addEventListener('click', () => (second += 1));\n decrement.addEventListener('click', () => (other += 1));\n drive.fire(increment, 'click');\n assert(first === 1 && second === 1, `one click must reach both listeners once, got ${String(first)} and ${String(second)}`);\n assert(other === 0, `a click on one element must not reach another's listener, got ${String(other)}`);\n },\n },\n {\n name: 'a core stepper upgrades from the markup and follows clicks',\n run({ root, drive }) {\n const controls = upgradePanel({}, root);\n const stepper = controls.get('contract-count');\n assert(stepper !== undefined, `upgradePanel must register the stepper as \"contract-count\", got [${controls.ids().join(', ')}]`);\n const value = partOf(root, 'value');\n const increment = partOf(root, 'increment');\n const shownAtStart = drive.rendered(value, 'text');\n assert(shownAtStart === '2', `the stepper must start at data-uix-value 2, showing ${JSON.stringify(shownAtStart)}`);\n drive.fire(increment, 'click');\n drive.fire(increment, 'click');\n const shown = drive.rendered(value, 'text');\n assert(shown === '3', `two increments from 2 must stop at data-uix-max 3, showing ${JSON.stringify(shown)}`);\n const opacity = drive.rendered(increment, 'opacity');\n assert(typeof opacity === 'number' && opacity < 1, `the increment must dim at the maximum, opacity ${JSON.stringify(opacity)}`);\n },\n },\n];\n\n/** The shared `UixElement` conformance suite. */\nexport function uixElementContractCases(): readonly UixElementContractCase[] {\n return CASES;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -30,3 +30,6 @@ export * from './controls/upgrade.js';
|
|
|
30
30
|
export * from './adapter.js';
|
|
31
31
|
export * from './scene.js';
|
|
32
32
|
export * from './contract-cases.js';
|
|
33
|
+
export * from './scene-contract-cases.js';
|
|
34
|
+
export * from './element-contract-cases.js';
|
|
35
|
+
export * from './memory-window-host.js';
|
package/dist/index.js
CHANGED
|
@@ -35,4 +35,7 @@ export * from './adapter.js';
|
|
|
35
35
|
export * from './scene.js';
|
|
36
36
|
// The WindowHost conformance suite, as data an adapter runs in its own runner
|
|
37
37
|
export * from './contract-cases.js';
|
|
38
|
+
export * from './scene-contract-cases.js';
|
|
39
|
+
export * from './element-contract-cases.js';
|
|
40
|
+
export * from './memory-window-host.js';
|
|
38
41
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,aAAa;AACb,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AAEpC,sDAAsD;AACtD,cAAc,oBAAoB,CAAC;AAEnC,qCAAqC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AAEtC,yDAAyD;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAE3B,8EAA8E;AAC9E,cAAc,qBAAqB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-uiextensions - the engine-free core.\n *\n * Everything exported here is pure TypeScript with no engine imports\n * (enforced by test/architecture.test.ts): window/dock/region/drag logic,\n * control models, the `data-uix` markup upgraders, the window chrome\n * conventions, and the platform-adapter interfaces engine packages\n * implement.\n */\n// Pure logic\nexport * from './core/events.js';\nexport * from './core/dock-state.js';\nexport * from './core/window-manager.js';\nexport * from './core/region-layout.js';\nexport * from './core/region-registry.js';\nexport * from './core/drag-math.js';\nexport * from './core/hold-to-drag.js';\nexport * from './core/hand-menu.js';\nexport * from './core/touch-press.js';\nexport * from './core/stepper-model.js';\nexport * from './core/toggle-model.js';\nexport * from './core/expandable-model.js';\nexport * from './core/log-model.js';\n\n// Chrome conventions (markup ids + reference snippet)\nexport * from './chrome/markup.js';\n\n// Interface-driven control upgraders\nexport * from './controls/element.js';\nexport * from './controls/stepper.js';\nexport * from './controls/toggle.js';\nexport * from './controls/expandable-label.js';\nexport * from './controls/log-view.js';\nexport * from './controls/upgrade.js';\n\n// Platform-adapter contract + portable scene descriptors\nexport * from './adapter.js';\nexport * from './scene.js';\n\n// The WindowHost conformance suite, as data an adapter runs in its own runner\nexport * from './contract-cases.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,aAAa;AACb,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,qBAAqB,CAAC;AAEpC,sDAAsD;AACtD,cAAc,oBAAoB,CAAC;AAEnC,qCAAqC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AAEtC,yDAAyD;AACzD,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAE3B,8EAA8E;AAC9E,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @realitycollective/webxr-uiextensions - the engine-free core.\n *\n * Everything exported here is pure TypeScript with no engine imports\n * (enforced by test/architecture.test.ts): window/dock/region/drag logic,\n * control models, the `data-uix` markup upgraders, the window chrome\n * conventions, and the platform-adapter interfaces engine packages\n * implement.\n */\n// Pure logic\nexport * from './core/events.js';\nexport * from './core/dock-state.js';\nexport * from './core/window-manager.js';\nexport * from './core/region-layout.js';\nexport * from './core/region-registry.js';\nexport * from './core/drag-math.js';\nexport * from './core/hold-to-drag.js';\nexport * from './core/hand-menu.js';\nexport * from './core/touch-press.js';\nexport * from './core/stepper-model.js';\nexport * from './core/toggle-model.js';\nexport * from './core/expandable-model.js';\nexport * from './core/log-model.js';\n\n// Chrome conventions (markup ids + reference snippet)\nexport * from './chrome/markup.js';\n\n// Interface-driven control upgraders\nexport * from './controls/element.js';\nexport * from './controls/stepper.js';\nexport * from './controls/toggle.js';\nexport * from './controls/expandable-label.js';\nexport * from './controls/log-view.js';\nexport * from './controls/upgrade.js';\n\n// Platform-adapter contract + portable scene descriptors\nexport * from './adapter.js';\nexport * from './scene.js';\n\n// The WindowHost conformance suite, as data an adapter runs in its own runner\nexport * from './contract-cases.js';\nexport * from './scene-contract-cases.js';\nexport * from './element-contract-cases.js';\nexport * from './memory-window-host.js';\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory `WindowHost`: the family's mock, for headless tests and for
|
|
3
|
+
* checking a new adapter against the same reference.
|
|
4
|
+
*
|
|
5
|
+
* It needs no engine and no renderer, and it passes every case in
|
|
6
|
+
* `windowHostContractCases()`. Windows are opened on a real
|
|
7
|
+
* {@link WindowManager}, and a window's panel attaches when the test calls
|
|
8
|
+
* {@link MemoryWindowHost.attach}, the way IWSDK attaches one on a later
|
|
9
|
+
* frame. {@link createMemoryWindowHostSetup} builds a fresh setup for the
|
|
10
|
+
* contract suite:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* for (const contractCase of windowHostContractCases()) {
|
|
14
|
+
* it(contractCase.name, () => contractCase.run(createMemoryWindowHostSetup()));
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { PanelHandle, PanelReadyEvent, WindowHandle, WindowHost } from './adapter.js';
|
|
19
|
+
import type { WindowHostContractSetup } from './contract-cases.js';
|
|
20
|
+
import type { UixElement } from './controls/element.js';
|
|
21
|
+
import { WindowManager } from './core/window-manager.js';
|
|
22
|
+
/** A panel with an empty root. It records whether it was disposed. */
|
|
23
|
+
export declare class MemoryPanel implements PanelHandle {
|
|
24
|
+
readonly root: UixElement;
|
|
25
|
+
disposed: boolean;
|
|
26
|
+
getElementById(_id: string): UixElement | undefined;
|
|
27
|
+
setTargetDimensions(_width: number, _height: number): void;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
30
|
+
/** A window whose panel arrives when {@link MemoryWindowHandle.attach} is called. */
|
|
31
|
+
export declare class MemoryWindowHandle implements WindowHandle {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
panel: PanelHandle | undefined;
|
|
34
|
+
private listeners;
|
|
35
|
+
constructor(id: string);
|
|
36
|
+
onReady(listener: (panel: PanelHandle) => void): () => void;
|
|
37
|
+
/** Attach the panel and run every waiting `onReady` listener once. */
|
|
38
|
+
attach(panel: PanelHandle): void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A host whose windows live on the {@link WindowManager} it is given.
|
|
42
|
+
* Closing a window through the manager disposes its panel and drops it from
|
|
43
|
+
* `onPanelReady` replay, as the engine adapters do.
|
|
44
|
+
*/
|
|
45
|
+
export declare class MemoryWindowHost implements WindowHost {
|
|
46
|
+
readonly manager: WindowManager;
|
|
47
|
+
readonly supportsStandalonePanels = true;
|
|
48
|
+
private readonly handles;
|
|
49
|
+
private readyListeners;
|
|
50
|
+
private readonly stopClosed;
|
|
51
|
+
constructor(manager: WindowManager);
|
|
52
|
+
createPanel(_configJson: unknown): PanelHandle;
|
|
53
|
+
onPanelReady(listener: (event: PanelReadyEvent) => void): () => void;
|
|
54
|
+
/** Open a window on the manager. Its panel arrives on {@link attach}. */
|
|
55
|
+
createWindow(id: string): MemoryWindowHandle;
|
|
56
|
+
/** Attach a panel to an open window. Does nothing if it has one already. */
|
|
57
|
+
attach(id: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Leave nothing behind: close every window through the manager, so each
|
|
60
|
+
* panel is disposed on the normal close path, then stop following the
|
|
61
|
+
* manager and forget every listener. Safe to call twice.
|
|
62
|
+
*/
|
|
63
|
+
dispose(): void;
|
|
64
|
+
}
|
|
65
|
+
/** A fresh host on a fresh manager, shaped for `windowHostContractCases()`. */
|
|
66
|
+
export declare function createMemoryWindowHostSetup(): WindowHostContractSetup;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { WindowManager } from './core/window-manager.js';
|
|
2
|
+
/** A panel with an empty root. It records whether it was disposed. */
|
|
3
|
+
export class MemoryPanel {
|
|
4
|
+
root = {
|
|
5
|
+
userData: {},
|
|
6
|
+
children: [],
|
|
7
|
+
addEventListener: () => { },
|
|
8
|
+
setProperties: () => { },
|
|
9
|
+
};
|
|
10
|
+
disposed = false;
|
|
11
|
+
getElementById(_id) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
setTargetDimensions(_width, _height) { }
|
|
15
|
+
dispose() {
|
|
16
|
+
this.disposed = true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** A window whose panel arrives when {@link MemoryWindowHandle.attach} is called. */
|
|
20
|
+
export class MemoryWindowHandle {
|
|
21
|
+
id;
|
|
22
|
+
panel;
|
|
23
|
+
listeners = [];
|
|
24
|
+
constructor(id) {
|
|
25
|
+
this.id = id;
|
|
26
|
+
}
|
|
27
|
+
onReady(listener) {
|
|
28
|
+
if (this.panel) {
|
|
29
|
+
listener(this.panel);
|
|
30
|
+
return () => { };
|
|
31
|
+
}
|
|
32
|
+
this.listeners.push(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
this.listeners = this.listeners.filter((entry) => entry !== listener);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Attach the panel and run every waiting `onReady` listener once. */
|
|
38
|
+
attach(panel) {
|
|
39
|
+
this.panel = panel;
|
|
40
|
+
const waiting = this.listeners;
|
|
41
|
+
this.listeners = [];
|
|
42
|
+
for (const listener of waiting)
|
|
43
|
+
listener(panel);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A host whose windows live on the {@link WindowManager} it is given.
|
|
48
|
+
* Closing a window through the manager disposes its panel and drops it from
|
|
49
|
+
* `onPanelReady` replay, as the engine adapters do.
|
|
50
|
+
*/
|
|
51
|
+
export class MemoryWindowHost {
|
|
52
|
+
manager;
|
|
53
|
+
supportsStandalonePanels = true;
|
|
54
|
+
handles = new Map();
|
|
55
|
+
readyListeners = [];
|
|
56
|
+
stopClosed;
|
|
57
|
+
constructor(manager) {
|
|
58
|
+
this.manager = manager;
|
|
59
|
+
this.stopClosed = manager.events.on('closed', (record) => {
|
|
60
|
+
this.handles.get(record.id)?.panel?.dispose();
|
|
61
|
+
this.handles.delete(record.id);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
createPanel(_configJson) {
|
|
65
|
+
return new MemoryPanel();
|
|
66
|
+
}
|
|
67
|
+
onPanelReady(listener) {
|
|
68
|
+
for (const [id, handle] of this.handles) {
|
|
69
|
+
if (handle.panel)
|
|
70
|
+
listener({ id, panel: handle.panel, kind: 'window' });
|
|
71
|
+
}
|
|
72
|
+
this.readyListeners.push(listener);
|
|
73
|
+
return () => {
|
|
74
|
+
this.readyListeners = this.readyListeners.filter((entry) => entry !== listener);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Open a window on the manager. Its panel arrives on {@link attach}. */
|
|
78
|
+
createWindow(id) {
|
|
79
|
+
const handle = new MemoryWindowHandle(id);
|
|
80
|
+
this.handles.set(id, handle);
|
|
81
|
+
this.manager.open(id, { title: id });
|
|
82
|
+
return handle;
|
|
83
|
+
}
|
|
84
|
+
/** Attach a panel to an open window. Does nothing if it has one already. */
|
|
85
|
+
attach(id) {
|
|
86
|
+
const handle = this.handles.get(id);
|
|
87
|
+
if (!handle || handle.panel)
|
|
88
|
+
return;
|
|
89
|
+
const panel = new MemoryPanel();
|
|
90
|
+
handle.attach(panel);
|
|
91
|
+
for (const listener of this.readyListeners)
|
|
92
|
+
listener({ id, panel, kind: 'window' });
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Leave nothing behind: close every window through the manager, so each
|
|
96
|
+
* panel is disposed on the normal close path, then stop following the
|
|
97
|
+
* manager and forget every listener. Safe to call twice.
|
|
98
|
+
*/
|
|
99
|
+
dispose() {
|
|
100
|
+
// A handle is dropped the moment its window closes, so every id here is open.
|
|
101
|
+
for (const id of [...this.handles.keys()])
|
|
102
|
+
this.manager.close(id);
|
|
103
|
+
this.stopClosed();
|
|
104
|
+
this.handles.clear();
|
|
105
|
+
this.readyListeners = [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** A fresh host on a fresh manager, shaped for `windowHostContractCases()`. */
|
|
109
|
+
export function createMemoryWindowHostSetup() {
|
|
110
|
+
const manager = new WindowManager();
|
|
111
|
+
const host = new MemoryWindowHost(manager);
|
|
112
|
+
return {
|
|
113
|
+
host,
|
|
114
|
+
manager,
|
|
115
|
+
createWindow: (id) => host.createWindow(id),
|
|
116
|
+
attach: (id) => host.attach(id),
|
|
117
|
+
panelConfig: {},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=memory-window-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-window-host.js","sourceRoot":"","sources":["../src/memory-window-host.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * An in-memory `WindowHost`: the family's mock, for headless tests and for\n * checking a new adapter against the same reference.\n *\n * It needs no engine and no renderer, and it passes every case in\n * `windowHostContractCases()`. Windows are opened on a real\n * {@link WindowManager}, and a window's panel attaches when the test calls\n * {@link MemoryWindowHost.attach}, the way IWSDK attaches one on a later\n * frame. {@link createMemoryWindowHostSetup} builds a fresh setup for the\n * contract suite:\n *\n * ```ts\n * for (const contractCase of windowHostContractCases()) {\n * it(contractCase.name, () => contractCase.run(createMemoryWindowHostSetup()));\n * }\n * ```\n */\nimport type {\n PanelHandle,\n PanelReadyEvent,\n WindowHandle,\n WindowHost,\n} from './adapter.js';\nimport type { WindowHostContractSetup } from './contract-cases.js';\nimport type { UixElement } from './controls/element.js';\nimport { WindowManager } from './core/window-manager.js';\n\n/** A panel with an empty root. It records whether it was disposed. */\nexport class MemoryPanel implements PanelHandle {\n readonly root: UixElement = {\n userData: {},\n children: [],\n addEventListener: () => {},\n setProperties: () => {},\n };\n disposed = false;\n\n getElementById(_id: string): UixElement | undefined {\n return undefined;\n }\n\n setTargetDimensions(_width: number, _height: number): void {}\n\n dispose(): void {\n this.disposed = true;\n }\n}\n\n/** A window whose panel arrives when {@link MemoryWindowHandle.attach} is called. */\nexport class MemoryWindowHandle implements WindowHandle {\n panel: PanelHandle | undefined;\n private listeners: Array<(panel: PanelHandle) => void> = [];\n\n constructor(readonly id: string) {}\n\n onReady(listener: (panel: PanelHandle) => void): () => void {\n if (this.panel) {\n listener(this.panel);\n return () => {};\n }\n this.listeners.push(listener);\n return () => {\n this.listeners = this.listeners.filter((entry) => entry !== listener);\n };\n }\n\n /** Attach the panel and run every waiting `onReady` listener once. */\n attach(panel: PanelHandle): void {\n this.panel = panel;\n const waiting = this.listeners;\n this.listeners = [];\n for (const listener of waiting) listener(panel);\n }\n}\n\n/**\n * A host whose windows live on the {@link WindowManager} it is given.\n * Closing a window through the manager disposes its panel and drops it from\n * `onPanelReady` replay, as the engine adapters do.\n */\nexport class MemoryWindowHost implements WindowHost {\n readonly supportsStandalonePanels = true;\n private readonly handles = new Map<string, MemoryWindowHandle>();\n private readyListeners: Array<(event: PanelReadyEvent) => void> = [];\n private readonly stopClosed: () => void;\n\n constructor(readonly manager: WindowManager) {\n this.stopClosed = manager.events.on('closed', (record) => {\n this.handles.get(record.id)?.panel?.dispose();\n this.handles.delete(record.id);\n });\n }\n\n createPanel(_configJson: unknown): PanelHandle {\n return new MemoryPanel();\n }\n\n onPanelReady(listener: (event: PanelReadyEvent) => void): () => void {\n for (const [id, handle] of this.handles) {\n if (handle.panel) listener({ id, panel: handle.panel, kind: 'window' });\n }\n this.readyListeners.push(listener);\n return () => {\n this.readyListeners = this.readyListeners.filter((entry) => entry !== listener);\n };\n }\n\n /** Open a window on the manager. Its panel arrives on {@link attach}. */\n createWindow(id: string): MemoryWindowHandle {\n const handle = new MemoryWindowHandle(id);\n this.handles.set(id, handle);\n this.manager.open(id, { title: id });\n return handle;\n }\n\n /** Attach a panel to an open window. Does nothing if it has one already. */\n attach(id: string): void {\n const handle = this.handles.get(id);\n if (!handle || handle.panel) return;\n const panel = new MemoryPanel();\n handle.attach(panel);\n for (const listener of this.readyListeners) listener({ id, panel, kind: 'window' });\n }\n\n /**\n * Leave nothing behind: close every window through the manager, so each\n * panel is disposed on the normal close path, then stop following the\n * manager and forget every listener. Safe to call twice.\n */\n dispose(): void {\n // A handle is dropped the moment its window closes, so every id here is open.\n for (const id of [...this.handles.keys()]) this.manager.close(id);\n this.stopClosed();\n this.handles.clear();\n this.readyListeners = [];\n }\n}\n\n/** A fresh host on a fresh manager, shaped for `windowHostContractCases()`. */\nexport function createMemoryWindowHostSetup(): WindowHostContractSetup {\n const manager = new WindowManager();\n const host = new MemoryWindowHost(manager);\n return {\n host,\n manager,\n createWindow: (id) => host.createWindow(id),\n attach: (id) => host.attach(id),\n panelConfig: {},\n };\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared `SceneTarget` contract, shipped as data rather than as tests.
|
|
3
|
+
*
|
|
4
|
+
* A client builds its UI with the core's `applyScene(target, descriptor)` and
|
|
5
|
+
* never asks which platform draws it. That only holds if every host turns the
|
|
6
|
+
* same descriptor into the same windows: each one opened on the host's
|
|
7
|
+
* `WindowManager` under its own id and title, in its region, with its dock
|
|
8
|
+
* mode and chrome. These cases check exactly that, through the manager every
|
|
9
|
+
* client already reads, so they need nothing platform-specific beyond a way
|
|
10
|
+
* to let a host finish what it does over later frames.
|
|
11
|
+
*
|
|
12
|
+
* When a descriptor gives a window both a region and a dock mode, the region
|
|
13
|
+
* wins and the window is world-locked. The core `WindowManager` holds that
|
|
14
|
+
* rule, and a case checks every host keeps it.
|
|
15
|
+
*
|
|
16
|
+
* The suite is runner-free, like `windowHostContractCases()`: each case
|
|
17
|
+
* resolves on success and rejects with a plain `Error` otherwise.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* for (const contractCase of sceneTargetContractCases()) {
|
|
21
|
+
* it(contractCase.name, () => contractCase.run(makeSetup()));
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import type { WindowManager } from './core/window-manager.js';
|
|
26
|
+
import { type SceneDescriptor, type SceneTarget } from './scene.js';
|
|
27
|
+
/** Everything a case needs to drive one host. Build a FRESH one per case. */
|
|
28
|
+
export interface SceneTargetContractSetup {
|
|
29
|
+
/** The host under test. */
|
|
30
|
+
target: SceneTarget;
|
|
31
|
+
/** The manager the host opens its windows on. */
|
|
32
|
+
manager: WindowManager;
|
|
33
|
+
/**
|
|
34
|
+
* Let the host finish building the windows with these ids: load their
|
|
35
|
+
* configs, attach their panels, run a frame. Omit it when windows exist as
|
|
36
|
+
* soon as `applyScene` returns.
|
|
37
|
+
*/
|
|
38
|
+
settle?: (windowIds: readonly string[]) => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
/** One check a {@link SceneTarget} host must pass. */
|
|
41
|
+
export interface SceneTargetContractCase {
|
|
42
|
+
name: string;
|
|
43
|
+
run(setup: SceneTargetContractSetup): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/** The descriptor every case applies: two regions, three windows. */
|
|
46
|
+
export declare const CONTRACT_SCENE: SceneDescriptor;
|
|
47
|
+
/** The shared `SceneTarget` conformance suite. */
|
|
48
|
+
export declare function sceneTargetContractCases(): readonly SceneTargetContractCase[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { DockMode } from './core/dock-state.js';
|
|
2
|
+
import { applyScene } from './scene.js';
|
|
3
|
+
/** The descriptor every case applies: two regions, three windows. */
|
|
4
|
+
export const CONTRACT_SCENE = {
|
|
5
|
+
name: 'contract',
|
|
6
|
+
regions: [{ id: 'contract-shelf' }, { id: 'contract-rail' }],
|
|
7
|
+
windows: [
|
|
8
|
+
{
|
|
9
|
+
id: 'contract-stats',
|
|
10
|
+
title: 'Stats',
|
|
11
|
+
config: '/ui/contract-stats.uikitml',
|
|
12
|
+
region: 'contract-shelf',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'contract-log',
|
|
16
|
+
title: 'Log',
|
|
17
|
+
config: '/ui/contract-log.uikitml',
|
|
18
|
+
dockMode: DockMode.HeadLocked,
|
|
19
|
+
closable: true,
|
|
20
|
+
minimizable: true,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'contract-docked',
|
|
24
|
+
title: 'Docked',
|
|
25
|
+
config: '/ui/contract-docked.uikitml',
|
|
26
|
+
region: 'contract-rail',
|
|
27
|
+
dockMode: DockMode.HeadLocked,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
const WINDOW_IDS = CONTRACT_SCENE.windows.map((window) => window.id);
|
|
32
|
+
function assert(condition, message) {
|
|
33
|
+
if (!condition)
|
|
34
|
+
throw new Error(message);
|
|
35
|
+
}
|
|
36
|
+
async function build(setup) {
|
|
37
|
+
applyScene(setup.target, CONTRACT_SCENE);
|
|
38
|
+
await setup.settle?.(WINDOW_IDS);
|
|
39
|
+
}
|
|
40
|
+
const CASES = [
|
|
41
|
+
{
|
|
42
|
+
name: 'applyScene opens every descriptor window on the manager',
|
|
43
|
+
async run(setup) {
|
|
44
|
+
await build(setup);
|
|
45
|
+
for (const id of WINDOW_IDS) {
|
|
46
|
+
assert(setup.manager.has(id), `applyScene must open window "${id}" on the host's manager`);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'each window keeps its descriptor title',
|
|
52
|
+
async run(setup) {
|
|
53
|
+
await build(setup);
|
|
54
|
+
for (const window of CONTRACT_SCENE.windows) {
|
|
55
|
+
const title = setup.manager.get(window.id)?.title;
|
|
56
|
+
assert(title === window.title, `window "${window.id}" must be titled "${window.title}", got ${JSON.stringify(title)}`);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'a window named into a region is in that region',
|
|
62
|
+
async run(setup) {
|
|
63
|
+
await build(setup);
|
|
64
|
+
const region = setup.manager.get('contract-stats')?.region;
|
|
65
|
+
assert(region === 'contract-shelf', `window "contract-stats" must be in region "contract-shelf", got ${JSON.stringify(region)}`);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'a window keeps its descriptor dock mode and chrome',
|
|
70
|
+
async run(setup) {
|
|
71
|
+
await build(setup);
|
|
72
|
+
const record = setup.manager.get('contract-log');
|
|
73
|
+
assert(record?.dockMode === DockMode.HeadLocked, `window "contract-log" must be ${DockMode.HeadLocked}, got ${JSON.stringify(record?.dockMode)}`);
|
|
74
|
+
assert(record.chrome.close && record.chrome.minimize, `window "contract-log" must show the close and minimize chrome it asked for, got ${JSON.stringify(record.chrome)}`);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'a window given a region and a dock mode is world-locked in that region',
|
|
79
|
+
async run(setup) {
|
|
80
|
+
await build(setup);
|
|
81
|
+
const record = setup.manager.get('contract-docked');
|
|
82
|
+
assert(record?.region === 'contract-rail', `window "contract-docked" must be in region "contract-rail", got ${JSON.stringify(record?.region)}`);
|
|
83
|
+
assert(record.dockMode === DockMode.WorldLocked, `window "contract-docked" is in a region, so it must be ${DockMode.WorldLocked}, got ${JSON.stringify(record.dockMode)}`);
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
/** The shared `SceneTarget` conformance suite. */
|
|
88
|
+
export function sceneTargetContractCases() {
|
|
89
|
+
return CASES;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=scene-contract-cases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene-contract-cases.js","sourceRoot":"","sources":["../src/scene-contract-cases.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared `SceneTarget` contract, shipped as data rather than as tests.\n *\n * A client builds its UI with the core's `applyScene(target, descriptor)` and\n * never asks which platform draws it. That only holds if every host turns the\n * same descriptor into the same windows: each one opened on the host's\n * `WindowManager` under its own id and title, in its region, with its dock\n * mode and chrome. These cases check exactly that, through the manager every\n * client already reads, so they need nothing platform-specific beyond a way\n * to let a host finish what it does over later frames.\n *\n * When a descriptor gives a window both a region and a dock mode, the region\n * wins and the window is world-locked. The core `WindowManager` holds that\n * rule, and a case checks every host keeps it.\n *\n * The suite is runner-free, like `windowHostContractCases()`: each case\n * resolves on success and rejects with a plain `Error` otherwise.\n *\n * ```ts\n * for (const contractCase of sceneTargetContractCases()) {\n * it(contractCase.name, () => contractCase.run(makeSetup()));\n * }\n * ```\n */\nimport type { WindowManager } from './core/window-manager.js';\nimport { DockMode } from './core/dock-state.js';\nimport { applyScene, type SceneDescriptor, type SceneTarget } from './scene.js';\n\n/** Everything a case needs to drive one host. Build a FRESH one per case. */\nexport interface SceneTargetContractSetup {\n /** The host under test. */\n target: SceneTarget;\n /** The manager the host opens its windows on. */\n manager: WindowManager;\n /**\n * Let the host finish building the windows with these ids: load their\n * configs, attach their panels, run a frame. Omit it when windows exist as\n * soon as `applyScene` returns.\n */\n settle?: (windowIds: readonly string[]) => void | Promise<void>;\n}\n\n/** One check a {@link SceneTarget} host must pass. */\nexport interface SceneTargetContractCase {\n name: string;\n run(setup: SceneTargetContractSetup): Promise<void>;\n}\n\n/** The descriptor every case applies: two regions, three windows. */\nexport const CONTRACT_SCENE: SceneDescriptor = {\n name: 'contract',\n regions: [{ id: 'contract-shelf' }, { id: 'contract-rail' }],\n windows: [\n {\n id: 'contract-stats',\n title: 'Stats',\n config: '/ui/contract-stats.uikitml',\n region: 'contract-shelf',\n },\n {\n id: 'contract-log',\n title: 'Log',\n config: '/ui/contract-log.uikitml',\n dockMode: DockMode.HeadLocked,\n closable: true,\n minimizable: true,\n },\n {\n id: 'contract-docked',\n title: 'Docked',\n config: '/ui/contract-docked.uikitml',\n region: 'contract-rail',\n dockMode: DockMode.HeadLocked,\n },\n ],\n};\n\nconst WINDOW_IDS = CONTRACT_SCENE.windows.map((window) => window.id);\n\nfunction assert(condition: boolean, message: string): asserts condition {\n if (!condition) throw new Error(message);\n}\n\nasync function build(setup: SceneTargetContractSetup): Promise<void> {\n applyScene(setup.target, CONTRACT_SCENE);\n await setup.settle?.(WINDOW_IDS);\n}\n\nconst CASES: readonly SceneTargetContractCase[] = [\n {\n name: 'applyScene opens every descriptor window on the manager',\n async run(setup) {\n await build(setup);\n for (const id of WINDOW_IDS) {\n assert(setup.manager.has(id), `applyScene must open window \"${id}\" on the host's manager`);\n }\n },\n },\n {\n name: 'each window keeps its descriptor title',\n async run(setup) {\n await build(setup);\n for (const window of CONTRACT_SCENE.windows) {\n const title = setup.manager.get(window.id)?.title;\n assert(title === window.title, `window \"${window.id}\" must be titled \"${window.title}\", got ${JSON.stringify(title)}`);\n }\n },\n },\n {\n name: 'a window named into a region is in that region',\n async run(setup) {\n await build(setup);\n const region = setup.manager.get('contract-stats')?.region;\n assert(region === 'contract-shelf', `window \"contract-stats\" must be in region \"contract-shelf\", got ${JSON.stringify(region)}`);\n },\n },\n {\n name: 'a window keeps its descriptor dock mode and chrome',\n async run(setup) {\n await build(setup);\n const record = setup.manager.get('contract-log');\n assert(\n record?.dockMode === DockMode.HeadLocked,\n `window \"contract-log\" must be ${DockMode.HeadLocked}, got ${JSON.stringify(record?.dockMode)}`,\n );\n assert(\n record.chrome.close && record.chrome.minimize,\n `window \"contract-log\" must show the close and minimize chrome it asked for, got ${JSON.stringify(record.chrome)}`,\n );\n },\n },\n {\n name: 'a window given a region and a dock mode is world-locked in that region',\n async run(setup) {\n await build(setup);\n const record = setup.manager.get('contract-docked');\n assert(\n record?.region === 'contract-rail',\n `window \"contract-docked\" must be in region \"contract-rail\", got ${JSON.stringify(record?.region)}`,\n );\n assert(\n record.dockMode === DockMode.WorldLocked,\n `window \"contract-docked\" is in a region, so it must be ${DockMode.WorldLocked}, got ${JSON.stringify(record.dockMode)}`,\n );\n },\n },\n];\n\n/** The shared `SceneTarget` conformance suite. */\nexport function sceneTargetContractCases(): readonly SceneTargetContractCase[] {\n return CASES;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@realitycollective/webxr-uiextensions",
|
|
3
|
-
"version": "0.1.0",
|
|
3
|
+
"version": "0.1.1-preview.0",
|
|
4
4
|
"description": "Engine-free core of the Reality Collective UI Extensions: windowing, docking, layout regions and control models for WebXR spatial UI, driven through platform-adapter interfaces. Pair with an engine adapter - @realitycollective/iwsdk-uiextensions (Meta IWSDK) or @realitycollective/xrblocks-uiextensions (Google XR Blocks).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"realitycollective",
|