@niivue/uikit 1.0.0-rc.6 → 1.0.0-rc.7
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/README.md +702 -8
- package/dist/button.d.ts +97 -0
- package/dist/buttonOverlay.d.ts +121 -0
- package/dist/colorControl.d.ts +130 -0
- package/dist/colorControlOverlay.d.ts +72 -0
- package/dist/controls.d.ts +151 -0
- package/dist/dialog.d.ts +138 -0
- package/dist/dialogOverlay.d.ts +115 -0
- package/dist/filePicker.d.ts +95 -0
- package/dist/filePickerOverlay.d.ts +75 -0
- package/dist/host.d.ts +87 -0
- package/dist/index.d.ts +57 -0
- package/dist/menu.d.ts +227 -0
- package/dist/menuOverlay.d.ts +149 -0
- package/dist/numberInput.d.ts +172 -0
- package/dist/numberInputOverlay.d.ts +131 -0
- package/dist/rect.d.ts +27 -0
- package/dist/rectOverlay.d.ts +13 -0
- package/dist/render/glRectRenderer.d.ts +13 -0
- package/dist/render/shaders.d.ts +3 -0
- package/dist/render/wgpuRectRenderer.d.ts +18 -0
- package/dist/scroll.d.ts +70 -0
- package/dist/segmented.d.ts +127 -0
- package/dist/segmentedOverlay.d.ts +87 -0
- package/dist/select.d.ts +66 -0
- package/dist/selectOverlay.d.ts +102 -0
- package/dist/slider.d.ts +152 -0
- package/dist/sliderOverlay.d.ts +92 -0
- package/dist/text/layout.d.ts +8 -0
- package/dist/textArea.d.ts +216 -0
- package/dist/textAreaOverlay.d.ts +145 -0
- package/dist/textEdit.d.ts +72 -0
- package/dist/textField.d.ts +33 -0
- package/dist/textInput.d.ts +123 -0
- package/dist/textInputOverlay.d.ts +139 -0
- package/dist/toggle.d.ts +115 -0
- package/dist/toggleOverlay.d.ts +80 -0
- package/dist/uikit.js +6152 -216
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# @niivue/uikit
|
|
2
2
|
|
|
3
|
-
UIKit: a collection of controls and widgets (rulers, crosshairs
|
|
4
|
-
|
|
3
|
+
UIKit: a collection of controls and widgets (rulers, crosshairs and
|
|
4
|
+
annotations, plus an on-canvas control layer of buttons, toggles, sliders,
|
|
5
|
+
menus, selects, segmented controls, number and text fields, color controls,
|
|
6
|
+
file pickers and dialogs) integrated into the
|
|
5
7
|
[NiiVue](https://github.com/niivue/niivue) rendering lifecycle.
|
|
6
8
|
|
|
7
9
|
> **Status: shipping widgets.** The rendering-lifecycle hook, UIKit's own
|
|
8
|
-
> line/text renderers (WebGL2 + WebGPU, with a text transform),
|
|
9
|
-
> widgets (ruler, annotation, crosshair)
|
|
10
|
+
> line/text/rect renderers (WebGL2 + WebGPU, with a text transform), the
|
|
11
|
+
> measuring widgets (ruler, annotation, crosshair) and the control layer with
|
|
12
|
+
> every widget in the table below are in. See `docs/ruler-port.md` in
|
|
10
13
|
> `@niivue/niivue` for the design.
|
|
11
14
|
|
|
12
15
|
## Design in one paragraph
|
|
@@ -22,13 +25,34 @@ proven, core's overlays cut over onto it and the duplicate in core is removed.
|
|
|
22
25
|
|
|
23
26
|
Each widget is a pure geometry builder (spec in, plain line + text draw data out,
|
|
24
27
|
unit-testable without a GPU) paired with an overlay that owns the GPU resources
|
|
25
|
-
and draws that data through the lifecycle hook.
|
|
28
|
+
and draws that data through the lifecycle hook. The overlays are the API. Of
|
|
29
|
+
the builders, only the measuring widgets' and the primitives' (`buildRuler`,
|
|
30
|
+
`buildCrosshair`, `buildAnnotationGeometry`, `buildRect`, `buildLine`,
|
|
31
|
+
`buildTerminatedLine`) are exported; the control widgets' models are
|
|
32
|
+
package-private (see "Conventions for contributors").
|
|
26
33
|
|
|
27
|
-
|
|
|
34
|
+
| Model | Overlay | Draws |
|
|
28
35
|
| --- | --- | --- |
|
|
29
36
|
| `buildRuler` | `UIKitRulerOverlay` | A measuring ruler with graduated ticks and a distance label |
|
|
30
37
|
| `buildCrosshair` | `UIKitCrosshairOverlay` | A screen-space cross marking a point, optionally graduated and numbered |
|
|
31
38
|
| `buildAnnotationGeometry` | `UIKitAnnotationOverlay` | Free-form annotation lines |
|
|
39
|
+
| `buildRect` | `UIKitRectOverlay` | Filled rounded rectangles with an optional border ring (the first filled primitive) |
|
|
40
|
+
| `buildButton` | `UIKitButtonOverlay` | Clickable, stylable push buttons with a text label and a press animation |
|
|
41
|
+
| `buildToggle` | `UIKitToggleOverlay` | Check boxes with a label: pointer or Space/Enter flips them |
|
|
42
|
+
| `buildSlider` | `UIKitSliderOverlay` | Horizontal sliders with min/max/step, ticks, a value readout, drag and arrow keys |
|
|
43
|
+
| `buildNumberInput` | `UIKitNumberInputOverlay` | Number fields: typed entry with validation, a spinner, arrow keys and the wheel |
|
|
44
|
+
| `buildTextInput` | `UIKitTextInputOverlay` | Single-line text fields with a placeholder, selection, Enter to submit |
|
|
45
|
+
| `buildTextArea` | `UIKitTextAreaOverlay` | Multi-line text fields that soft-wrap and scroll by whole rows |
|
|
46
|
+
| `buildColorControl` | `UIKitColorControlOverlay` | Color pickers the legacy way: a preview, R/G/B(/A) channel sliders and palette swatches |
|
|
47
|
+
| `buildFilePicker` | `UIKitFilePickerOverlay` | File pickers: a Choose button and a readout; the host chooser and downloads go through bridges |
|
|
48
|
+
| `buildDialog` | `UIKitDialogOverlay` | Modal dialogs: a scrim, a panel with a title, wrapped message, hosted widgets and action buttons |
|
|
49
|
+
| `buildMenuButton`, `buildMenuPopup` | `UIKitMenuOverlay` | Menu buttons with popups of actions, check items and radio groups |
|
|
50
|
+
| `selectMenuSpec` | `UIKitSelectOverlay` | Drop-down lists: a button showing the chosen option over a popup of options |
|
|
51
|
+
| `buildSegmented` | `UIKitSegmentedOverlay` | Segmented controls: one row of segments with exactly one selected |
|
|
52
|
+
| `scrollWindow`, `revealRow`, `buildScrollArrow` | (used by the overlays above) | Row scrolling for any container whose list outgrows its viewport |
|
|
53
|
+
| `editKey`, `textWindow`, `caretIndexAt` | (used by the overlays above) | Single-line text editing: caret, selection, key edits, whole-glyph scrolling |
|
|
54
|
+
| `buildTextFieldContent` | (used by the overlays above) | The text, selection and caret of any field, clipped to the glyphs that fit |
|
|
55
|
+
| | `UIKitControls` | The control layer: one owner of the pointer, wheel and keyboard for every widget above |
|
|
32
56
|
|
|
33
57
|
```ts
|
|
34
58
|
import { loadDefaultFont, UIKitCrosshairOverlay } from '@niivue/uikit'
|
|
@@ -48,8 +72,8 @@ crosshair.setCrosshair({
|
|
|
48
72
|
})
|
|
49
73
|
```
|
|
50
74
|
|
|
51
|
-
The builders are usable on their own if you want the geometry but not
|
|
52
|
-
renderers:
|
|
75
|
+
The exported builders are usable on their own if you want the geometry but not
|
|
76
|
+
UIKit's renderers:
|
|
53
77
|
|
|
54
78
|
```ts
|
|
55
79
|
import { buildTerminatedLine, LineTerminator } from '@niivue/uikit'
|
|
@@ -59,3 +83,673 @@ const segments = buildTerminatedLine(0, 0, 100, 40, 2, [1, 0, 0, 1], {
|
|
|
59
83
|
end: LineTerminator.ARROW,
|
|
60
84
|
})
|
|
61
85
|
```
|
|
86
|
+
|
|
87
|
+
## Buttons
|
|
88
|
+
|
|
89
|
+
`UIKitButtonOverlay` draws push buttons into the scene and handles the pointer
|
|
90
|
+
for them. Each button is a rounded rect plus a centred label. The pointer gives
|
|
91
|
+
it a hover tint, a press that shrinks and darkens the face, and a release that
|
|
92
|
+
eases back; a click fires on pointer-up inside the button the pointer went down
|
|
93
|
+
on. `attach(nv)` wires the canvas pointer events so a click on a button is
|
|
94
|
+
consumed before NiiVue's own handlers see it (no stray drag or crosshair move),
|
|
95
|
+
and shows a pointer cursor over buttons.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { loadDefaultFont, UIKitButtonOverlay } from '@niivue/uikit'
|
|
99
|
+
|
|
100
|
+
// cssUnits: lay the buttons out in CSS pixels; the overlay scales by the DPR.
|
|
101
|
+
const buttons = new UIKitButtonOverlay(await loadDefaultFont(), { cssUnits: true })
|
|
102
|
+
nv.registerOverlayRenderer(buttons)
|
|
103
|
+
const detach = buttons.attach(nv)
|
|
104
|
+
|
|
105
|
+
buttons.addButton({
|
|
106
|
+
id: 'axial',
|
|
107
|
+
label: 'Axial',
|
|
108
|
+
x: 12,
|
|
109
|
+
y: 12, // top-left; width/height default to the label plus padding
|
|
110
|
+
onClick: () => { nv.sliceType = SLICE_TYPE.AXIAL },
|
|
111
|
+
})
|
|
112
|
+
buttons.addButton({
|
|
113
|
+
id: 'reset',
|
|
114
|
+
label: 'Reset',
|
|
115
|
+
x: 90,
|
|
116
|
+
y: 12,
|
|
117
|
+
width: 120, // a fixed size is honoured per axis
|
|
118
|
+
style: { fill: [0.55, 0.1, 0.12, 0.95], border: [1, 0.55, 0.55, 1], radius: 14 },
|
|
119
|
+
onClick: resetView,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
buttons.updateButton('reset', { label: 'Reset (3)' })
|
|
123
|
+
buttons.setEnabled('axial', false)
|
|
124
|
+
buttons.click('reset') // from a keyboard shortcut: same animation + callback
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Every visual knob is in `ButtonStyle` (fill, hover, pressed and disabled fills,
|
|
128
|
+
border color and width, corner radius, text color and size, padding, press
|
|
129
|
+
shrink scale and press/release durations). Set defaults for the overlay with the
|
|
130
|
+
`style` option or `setDefaultStyle`, and override per button with `spec.style`.
|
|
131
|
+
Without `cssUnits`, all lengths are canvas (device) pixels, like every other
|
|
132
|
+
UIKit widget.
|
|
133
|
+
|
|
134
|
+
A host that is not a NiiVue controller can feed the overlay itself: call
|
|
135
|
+
`pointerDown`, `pointerMove`, `pointerUp` and `pointerCancel` with canvas-pixel
|
|
136
|
+
coordinates (each returns whether a button consumed the event) and pass a
|
|
137
|
+
`requestRedraw` option so the press animation can ask for frames.
|
|
138
|
+
|
|
139
|
+
## Control layer
|
|
140
|
+
|
|
141
|
+
`UIKitControls` is the one place that owns the canvas pointer, wheel and
|
|
142
|
+
keyboard for every interactive widget, so widgets never compete for an event. Add the widget
|
|
143
|
+
overlays to it in draw order, register the layer as NiiVue's overlay renderer,
|
|
144
|
+
and `attach(nv)` to the canvas. Pointer down goes to the topmost widget that
|
|
145
|
+
claims it; that widget then captures every move and the up and takes keyboard
|
|
146
|
+
focus. The layer is single-pointer: one pointer at a time, and a second down
|
|
147
|
+
(another touch, or a down whose up was never seen) cancels the held press
|
|
148
|
+
before any widget, including the one holding it, sees the new press, so a
|
|
149
|
+
drag or button press restarts from the new pointer rather than sharing two.
|
|
150
|
+
Moves and releases from any other pointer are ignored while one holds the
|
|
151
|
+
press. When pointer capture is refused and the release lands outside the
|
|
152
|
+
canvas, the layer ends the press from the window but leaves the event to the
|
|
153
|
+
element under it, so a page button still gets its click.
|
|
154
|
+
Only the topmost widget under the pointer hovers. A widget that is modal
|
|
155
|
+
(a menu with its popup open) sees every event first until it closes, and the
|
|
156
|
+
click that dismisses it is consumed. Keys go to the modal widget, else the
|
|
157
|
+
focused one. A wheel turn goes to the modal widget, else to the topmost widget
|
|
158
|
+
under the pointer that handles wheels (an open popup scrolls with it; the page
|
|
159
|
+
scrolls otherwise). Events a widget consumes are stopped in the capture phase,
|
|
160
|
+
so a click on a control never starts a NiiVue drag or moves the crosshair. Popups are
|
|
161
|
+
drawn after every widget. A press anywhere else in the page, focus moving to
|
|
162
|
+
another element or the window losing focus deactivates the layer (held press
|
|
163
|
+
cancelled, popups dismissed, focus dropped), so keys reach the page again
|
|
164
|
+
instead of the last widget the user touched; `deactivate()` does the same from
|
|
165
|
+
code. The layer owns keys again on the next canvas press or `focus` call, and
|
|
166
|
+
a widget opened or focused from code takes them back the same way: a dialog
|
|
167
|
+
opened from a page button, a menu opened with `open`, or a text field given
|
|
168
|
+
`focus` claims the keyboard through the layer at once. An open dialog does not
|
|
169
|
+
keep the keys while the user is elsewhere in the page.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import {
|
|
173
|
+
loadDefaultFont, UIKitButtonOverlay, UIKitControls, UIKitMenuOverlay,
|
|
174
|
+
UIKitSliderOverlay, UIKitToggleOverlay,
|
|
175
|
+
} from '@niivue/uikit'
|
|
176
|
+
|
|
177
|
+
const font = await loadDefaultFont()
|
|
178
|
+
const units = { cssUnits: true }
|
|
179
|
+
const buttons = new UIKitButtonOverlay(font, units)
|
|
180
|
+
const toggles = new UIKitToggleOverlay(font, units)
|
|
181
|
+
const sliders = new UIKitSliderOverlay(font, units)
|
|
182
|
+
const menus = new UIKitMenuOverlay(font, units)
|
|
183
|
+
const controls = new UIKitControls().add(buttons).add(toggles).add(sliders).add(menus)
|
|
184
|
+
nv.registerOverlayRenderer(controls)
|
|
185
|
+
const detach = controls.attach(nv) // call detach() to unwire the events
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A widget added to a layer takes the layer's redraw requester, which `attach`
|
|
189
|
+
defaults to `nv.drawScene`. A host that is not a NiiVue controller can feed the
|
|
190
|
+
layer itself through `pointerDown`, `pointerMove`, `pointerUp`, `pointerCancel`,
|
|
191
|
+
`wheel` and `keyDown` in canvas pixels; each returns whether a widget consumed
|
|
192
|
+
the event.
|
|
193
|
+
`UIKitButtonOverlay.attach` still works on its own: it is a one-widget layer.
|
|
194
|
+
|
|
195
|
+
## Toggles
|
|
196
|
+
|
|
197
|
+
A toggle is a square box with a check mark and a label beside it. Pointer press
|
|
198
|
+
and release inside flips it; the pressed toggle takes focus (a ring around the
|
|
199
|
+
box) and Space or Enter flips it from the keyboard. `onChange` fires for user
|
|
200
|
+
changes only; `setChecked` is silent.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
toggles.addToggle({
|
|
204
|
+
id: 'colorbar',
|
|
205
|
+
label: 'Colorbar',
|
|
206
|
+
x: 12,
|
|
207
|
+
y: 52,
|
|
208
|
+
checked: false,
|
|
209
|
+
onChange: (checked) => { nv.isColorbarVisible = checked },
|
|
210
|
+
})
|
|
211
|
+
toggles.setChecked('colorbar', true) // from code, no callback
|
|
212
|
+
toggles.toggle('colorbar') // as the user would: fires onChange
|
|
213
|
+
toggles.setEnabled('colorbar', false)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`ToggleStyle` holds the box size, radius, gap, fills for each state, border,
|
|
217
|
+
check color and width, text color and size, the label halo
|
|
218
|
+
(`textOutlineWidth`, default 1, and `textOutlineColor`, null for automatic
|
|
219
|
+
contrast), press scale and focus ring.
|
|
220
|
+
|
|
221
|
+
## Sliders
|
|
222
|
+
|
|
223
|
+
A slider is a horizontal track with a round thumb, an optional label on the
|
|
224
|
+
left and value readout on the right, and optional tick marks. Pressing the track
|
|
225
|
+
jumps the value there and starts a drag; the thumb follows the pointer however
|
|
226
|
+
far it strays vertically. Values clamp to `[min, max]` and snap to `step`
|
|
227
|
+
when one is given; without a step the slider is continuous and the keyboard
|
|
228
|
+
moves by a hundredth of the range. With focus, ArrowLeft/Down and
|
|
229
|
+
ArrowRight/Up move one step, Shift and PageUp/PageDown move ten, Home and End
|
|
230
|
+
go to the ends.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
sliders.addSlider({
|
|
234
|
+
id: 'gamma',
|
|
235
|
+
label: 'Gamma',
|
|
236
|
+
x: 12,
|
|
237
|
+
y: 84,
|
|
238
|
+
width: 220,
|
|
239
|
+
min: 0.1,
|
|
240
|
+
max: 3,
|
|
241
|
+
step: 0.05,
|
|
242
|
+
value: 1,
|
|
243
|
+
showValue: true,
|
|
244
|
+
format: (v) => v.toFixed(2), // optional; default shows the step's decimals
|
|
245
|
+
tickStep: 0.5, // optional tick marks (skipped when denser than a pixel)
|
|
246
|
+
onInput: (v) => { nv.gamma = v }, // every change, including during a drag
|
|
247
|
+
onChange: (v) => save(v), // once, when a drag or key press settles a new value
|
|
248
|
+
})
|
|
249
|
+
sliders.setValue('gamma', 1) // silent
|
|
250
|
+
sliders.getValue('gamma')
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The callback contract: `onInput` fires on every value change the user makes
|
|
254
|
+
and `onChange` fires when the interaction ends with a value different from the
|
|
255
|
+
one it started with (a drag releases, a key press lands). Neither fires for
|
|
256
|
+
`setValue`.
|
|
257
|
+
|
|
258
|
+
The label and value sit on the scene, so they draw with a halo outline:
|
|
259
|
+
`textOutlineWidth` (default 1; 0 for none) and `textOutlineColor` (default
|
|
260
|
+
null, which picks black or white for contrast with `textColor`) in
|
|
261
|
+
`SliderStyle`.
|
|
262
|
+
|
|
263
|
+
## Menus
|
|
264
|
+
|
|
265
|
+
`UIKitMenuOverlay` draws menu buttons (a label with a chevron) and, when one is
|
|
266
|
+
open, its popup. Items are actions, check items, radio items (exclusive within
|
|
267
|
+
their `group`) or separators, each with an optional right-aligned shortcut
|
|
268
|
+
label and `enabled` flag. The popup opens below the button and flips above it
|
|
269
|
+
when it would overflow the canvas; when it fits neither way it takes the
|
|
270
|
+
roomier side and scrolls (see "Scrolling containers" below). While open the
|
|
271
|
+
overlay is modal: it
|
|
272
|
+
highlights the row under the pointer, a click on a row activates it, a click
|
|
273
|
+
anywhere else closes the menu (and is not passed on), and Escape closes it.
|
|
274
|
+
ArrowUp/Down, Home and End move the highlight over selectable rows, Enter or
|
|
275
|
+
Space activates. A focused closed menu opens on Enter, Space or ArrowDown.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
menus.addMenu({
|
|
279
|
+
id: 'view',
|
|
280
|
+
label: 'View',
|
|
281
|
+
x: 12,
|
|
282
|
+
y: 12,
|
|
283
|
+
items: [
|
|
284
|
+
{ id: 'axial', label: 'Axial', kind: 'radio', group: 'view', shortcut: '1' },
|
|
285
|
+
{ id: 'mpr', label: 'Multiplanar', kind: 'radio', group: 'view', checked: true },
|
|
286
|
+
{ id: 'sep', kind: 'separator' },
|
|
287
|
+
{ id: 'colorbar', label: 'Colorbar', kind: 'check' },
|
|
288
|
+
{ id: 'reset', label: 'Reset view', onSelect: resetView },
|
|
289
|
+
{ id: 'undo', label: 'Undo', enabled: false },
|
|
290
|
+
],
|
|
291
|
+
onSelect: (itemId, menuId) => { /* every activation */ },
|
|
292
|
+
onChange: (itemId, checked, menuId) => { /* check and radio state changes */ },
|
|
293
|
+
})
|
|
294
|
+
menus.setItemChecked('view', 'colorbar', true) // silent; radio items clear their group
|
|
295
|
+
menus.isItemChecked('view', 'axial')
|
|
296
|
+
menus.setItems('view', newItems)
|
|
297
|
+
menus.open('view'); menus.close()
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Activating a check item flips it; activating a radio item checks it and clears
|
|
301
|
+
the rest of its group. `onChange` reports each item whose state changed (the
|
|
302
|
+
activated one first), then the item's `onSelect` and the menu's `onSelect`
|
|
303
|
+
fire. `MenuStyle` holds the popup fill, border, radius and padding, row height,
|
|
304
|
+
highlight fill, text, mark and shortcut colors, separator and chevron sizes, and
|
|
305
|
+
`buttonStyle` is a `ButtonStyle` for the trigger.
|
|
306
|
+
|
|
307
|
+
## Selects
|
|
308
|
+
|
|
309
|
+
`UIKitSelectOverlay` draws drop-down lists: a menu button whose label is the
|
|
310
|
+
chosen option (or a `placeholder` while nothing is chosen) over a popup listing
|
|
311
|
+
every option as one radio group. The button is sized to the widest option so it
|
|
312
|
+
keeps its width as the value changes. Opening, highlighting, choosing on release
|
|
313
|
+
or Enter, dismissing on an outside press or Escape and being modal while open
|
|
314
|
+
all behave as for a menu, a long list scrolls, and the list opens with the
|
|
315
|
+
current option highlighted and in view. A focused closed select also changes its value straight from the
|
|
316
|
+
keyboard: ArrowUp/Down (or Left/Right) step over the enabled options without
|
|
317
|
+
wrapping, Shift or PageUp/PageDown step by ten, Home and End jump to the ends,
|
|
318
|
+
and Enter, Space or Alt+ArrowDown open the list.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
const selects = new UIKitSelectOverlay(font, units)
|
|
322
|
+
selects.addSelect({
|
|
323
|
+
id: 'colormap',
|
|
324
|
+
x: 12,
|
|
325
|
+
y: 12,
|
|
326
|
+
options: nv.colormaps.map((name) => ({ value: name, label: name })),
|
|
327
|
+
value: 'gray',
|
|
328
|
+
onChange: (value) => nv.setVolume(0, { colormap: value }),
|
|
329
|
+
})
|
|
330
|
+
selects.setValue('colormap', 'hot') // silent; a value no option holds clears it
|
|
331
|
+
selects.setOptions('colormap', fewerOptions) // a value no longer offered clears
|
|
332
|
+
selects.getValue('colormap')
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`onChange(value, id)` fires once per user choice and not when the same option
|
|
336
|
+
is chosen again or the value is set from code. Options carry `enabled`; a
|
|
337
|
+
disabled option draws dimmed and is skipped by the keyboard. `buttonStyle` and
|
|
338
|
+
`style` are the menu's `ButtonStyle` and `MenuStyle`.
|
|
339
|
+
|
|
340
|
+
## Scrolling containers
|
|
341
|
+
|
|
342
|
+
Any container that shows a list taller than the space it has scrolls by whole
|
|
343
|
+
rows through the shared model in `scroll.ts`, so a container only ever draws
|
|
344
|
+
rows that lie fully inside its viewport and needs no GPU scissor.
|
|
345
|
+
`scrollWindow(heights, viewport, first)` returns the run of rows that fits from
|
|
346
|
+
`first`, clamped so the list never scrolls past its end; `revealRow` finds the
|
|
347
|
+
least scroll that brings a row into view (a keyboard move, or opening on an
|
|
348
|
+
item); `WheelAccumulator` turns a trackpad's small wheel deltas into row steps;
|
|
349
|
+
`buildScrollArrow` draws the chevron on a strip. Menu and select popups are the
|
|
350
|
+
first consumers: a scrollable popup reserves a strip at each end
|
|
351
|
+
(`scrollStripHeight`, `scrollArrowSize`, `scrollArrowWidth`, `scrollArrowColor`
|
|
352
|
+
in `MenuStyle`), shows an arrow on a strip with rows beyond it, scrolls on the
|
|
353
|
+
wheel or a press on a strip, keeps the keyboard highlight in view, and reports
|
|
354
|
+
`scrollable`, `firstRow`, `endRow` and the strip boxes in its popup layout. A
|
|
355
|
+
widget that scrolls implements `wheel(x, y, deltaX, deltaY)` from
|
|
356
|
+
`UIKitInteractive`; the control layer routes the canvas wheel to it. The scroll
|
|
357
|
+
functions are package-private; only the `ScrollRange`, `ScrollWindow`,
|
|
358
|
+
`ScrollStripStyle` and `UIKitBox` types are exported.
|
|
359
|
+
|
|
360
|
+
## Segmented controls
|
|
361
|
+
|
|
362
|
+
`UIKitSegmentedOverlay` draws a row of equal-width segments in one rounded
|
|
363
|
+
track with exactly one selected: a compact radio group for a mode or a view.
|
|
364
|
+
The pointer tints the segment under it, pressing darkens it, and releasing on
|
|
365
|
+
the segment the press started on selects it. The pressed control takes
|
|
366
|
+
keyboard focus (a ring around the track); ArrowLeft/Right (or Up/Down) then
|
|
367
|
+
move the selection through the enabled segments, wrapping, and Home and End
|
|
368
|
+
jump to the ends.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
const segmented = new UIKitSegmentedOverlay(font, units)
|
|
372
|
+
segmented.addSegmented({
|
|
373
|
+
id: 'view',
|
|
374
|
+
x: 12,
|
|
375
|
+
y: 52,
|
|
376
|
+
segments: [
|
|
377
|
+
{ value: 'axial', label: 'Axial' },
|
|
378
|
+
{ value: 'mpr', label: 'MPR' },
|
|
379
|
+
{ value: 'render', label: 'Render', enabled: false },
|
|
380
|
+
],
|
|
381
|
+
value: 'mpr',
|
|
382
|
+
onChange: (value) => { /* apply the view */ },
|
|
383
|
+
})
|
|
384
|
+
segmented.setValue('view', 'axial') // silent
|
|
385
|
+
segmented.select('view', 'axial') // as the user would: fires onChange
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
`segmentWidth` fixes every segment's width instead of fitting the widest label;
|
|
389
|
+
`SegmentedStyle` holds the track fill and border, inset, radius, the selected,
|
|
390
|
+
hover and pressed faces, text colors and size, the divider and the focus ring.
|
|
391
|
+
|
|
392
|
+
## Number inputs
|
|
393
|
+
|
|
394
|
+
`UIKitNumberInputOverlay` draws a bordered field showing a number, with a
|
|
395
|
+
two-button spinner at its right end and an optional label to its left. A
|
|
396
|
+
press on the text takes keyboard focus and selects the whole number, so typing
|
|
397
|
+
replaces it; a second press places the caret and dragging selects. Typed text
|
|
398
|
+
is checked on every keystroke (the border turns `invalidBorder` while it is
|
|
399
|
+
not a number) and `onInput` fires whenever it means a new valid number. Enter
|
|
400
|
+
commits, Escape reverts to the committed value, and losing focus commits, as
|
|
401
|
+
does every step: ArrowUp/Down (Shift: ten steps), PageUp/PageDown, the spinner
|
|
402
|
+
buttons and the wheel over a focused field. A committed value is clamped to
|
|
403
|
+
`[min, max]` (either end optional) and snapped to `step` (counted from `min`,
|
|
404
|
+
or 0), and `onChange` fires once per committed change. The field shows only
|
|
405
|
+
the glyphs that fit, scrolled by whole glyphs to keep the caret in view.
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
const numbers = new UIKitNumberInputOverlay(font, units)
|
|
409
|
+
numbers.addNumberInput({
|
|
410
|
+
id: 'elevation',
|
|
411
|
+
label: 'Elevation',
|
|
412
|
+
x: 250,
|
|
413
|
+
y: 172,
|
|
414
|
+
width: 80, // the field; the label adds to it
|
|
415
|
+
min: -90,
|
|
416
|
+
max: 90,
|
|
417
|
+
step: 5,
|
|
418
|
+
value: 10,
|
|
419
|
+
format: (v) => `${v}`, // optional; default shows the step's decimals
|
|
420
|
+
onInput: (v) => { nv.elevation = v }, // each new valid number while typing, each step
|
|
421
|
+
onChange: (v) => save(v), // once per committed change
|
|
422
|
+
})
|
|
423
|
+
numbers.setValue('elevation', 0) // silent
|
|
424
|
+
numbers.getValue('elevation') // the committed value
|
|
425
|
+
numbers.getText('elevation') // what the field shows, edit in progress included
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`NumberInputStyle` holds the field fill, hover and disabled fills, the border
|
|
429
|
+
with its focus and invalid colors, text color and size, the label halo
|
|
430
|
+
(`textOutlineWidth`, `textOutlineColor`), padding, the default `fieldWidth`,
|
|
431
|
+
the spinner width, faces, dividers and chevrons, and the caret and selection
|
|
432
|
+
colors. `parse` on a spec replaces the default `Number` parser. The overlay
|
|
433
|
+
option `wheelStepPx` (default 40) is how many wheel pixels make one step.
|
|
434
|
+
|
|
435
|
+
The editing itself is the shared model in `textEdit.ts`: a `TextEditState`
|
|
436
|
+
(text, caret, selection anchor), `editKey` for the caret moves, Backspace,
|
|
437
|
+
Delete, select-all and typed characters a key means, `caretIndexAt` for the
|
|
438
|
+
pointer, and `textWindow`, which scrolls a field by whole glyphs through the
|
|
439
|
+
scroll model. `textField.ts` turns that state into the selection, text and
|
|
440
|
+
caret geometry of any field. Text inputs reuse both. Like the other widget
|
|
441
|
+
models they are package-private; only the `TextEditState` and
|
|
442
|
+
`TextFieldStyle` types are exported.
|
|
443
|
+
|
|
444
|
+
## Text inputs
|
|
445
|
+
|
|
446
|
+
`UIKitTextInputOverlay` draws a bordered single-line field with an optional
|
|
447
|
+
label and a dimmed placeholder while it is empty. A press takes keyboard
|
|
448
|
+
focus and places the caret at the pointer, dragging selects, and a second
|
|
449
|
+
press within the double-click interval selects all. The arrows, Home and End
|
|
450
|
+
move the caret (Shift extends the selection, Meta or Ctrl jumps to the ends),
|
|
451
|
+
Backspace and Delete edit, Meta or Ctrl plus A selects all. `onInput` fires
|
|
452
|
+
on every keystroke that changes the text. Enter commits the text (firing
|
|
453
|
+
`onChange` when it differs from the committed text) and then fires `onSubmit`
|
|
454
|
+
whether or not it changed; Escape reverts to the committed text; losing focus
|
|
455
|
+
commits. Long text scrolls by whole glyphs to keep the caret in view.
|
|
456
|
+
|
|
457
|
+
Meta or Ctrl plus C, X and V copy, cut and paste the selection through a
|
|
458
|
+
`ClipboardBridge` (the browser's async clipboard unless the overlay's
|
|
459
|
+
`clipboard` option supplies one; see "Host bridges" below). A pasted text
|
|
460
|
+
loses its line breaks, and the spec's `accept` and `maxLength` apply to it.
|
|
461
|
+
A host that handles the DOM paste event itself can call `paste(text)`
|
|
462
|
+
instead. No DOM input backs the field, so there is no IME composition. Tab
|
|
463
|
+
and other keys the field does not use fall through to the host.
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
const texts = new UIKitTextInputOverlay(font, units)
|
|
467
|
+
texts.addTextInput({
|
|
468
|
+
id: 'command',
|
|
469
|
+
label: 'niimath',
|
|
470
|
+
x: 12,
|
|
471
|
+
y: 340,
|
|
472
|
+
width: 220, // the field; the label adds to it
|
|
473
|
+
placeholder: '-sqr -add 2',
|
|
474
|
+
maxLength: 200,
|
|
475
|
+
accept: (ch) => ch !== '\t', // optional keyboard filter
|
|
476
|
+
onInput: (text) => preview(text), // every change
|
|
477
|
+
onChange: (text) => save(text), // Enter or blur, when changed
|
|
478
|
+
onSubmit: (text) => run(text), // Enter, always
|
|
479
|
+
})
|
|
480
|
+
texts.setValue('command', '') // silent
|
|
481
|
+
texts.getValue('command') // the committed text
|
|
482
|
+
texts.getText('command') // what the field shows, edit in progress included
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`TextInputStyle` holds the field fill, hover and disabled fills, the border
|
|
486
|
+
and its focus color, text, placeholder and disabled text colors, the label
|
|
487
|
+
halo (`textOutlineWidth`, `textOutlineColor`), padding, the default
|
|
488
|
+
`fieldWidth`, and the caret and selection colors. The overlay options take
|
|
489
|
+
`doubleClickMs` (default 400) and a `now` clock for it.
|
|
490
|
+
|
|
491
|
+
## Text areas
|
|
492
|
+
|
|
493
|
+
`UIKitTextAreaOverlay` draws a bordered field of a fixed number of rows with
|
|
494
|
+
an optional label above it. Text soft-wraps at the field's width (after the
|
|
495
|
+
last space that fits, else by glyph) and scrolls by whole rows through the
|
|
496
|
+
shared scroll model, with a scrollbar once the rows overflow: the wheel
|
|
497
|
+
scrolls a hovered area, and the caret is kept in view while editing. A press
|
|
498
|
+
takes keyboard focus and places the caret at the pointer, dragging selects
|
|
499
|
+
across rows, and a second press within the double-click interval selects
|
|
500
|
+
all. Enter inserts a newline; the arrows move by glyph and by row (a run of
|
|
501
|
+
vertical moves keeps its column), Home and End move within the row, PageUp
|
|
502
|
+
and PageDown move by a page of rows, and Meta or Ctrl jumps to the text's
|
|
503
|
+
ends. Shift extends the selection; Backspace, Delete and Meta or Ctrl plus A
|
|
504
|
+
come from the single-line model. `onInput` fires on every keystroke that
|
|
505
|
+
changes the text. Meta or Ctrl plus Enter commits (firing `onChange` when the
|
|
506
|
+
text changed) and then fires `onSubmit`; Escape reverts to the committed
|
|
507
|
+
text; losing focus commits.
|
|
508
|
+
|
|
509
|
+
Copy, cut and paste work as in the text input, through the same
|
|
510
|
+
`ClipboardBridge`; a pasted text keeps its line breaks (CRLF becomes LF).
|
|
511
|
+
There is no IME composition.
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
const areas = new UIKitTextAreaOverlay(font, units)
|
|
515
|
+
areas.addTextArea({
|
|
516
|
+
id: 'notes',
|
|
517
|
+
label: 'Notes',
|
|
518
|
+
x: 12,
|
|
519
|
+
y: 540,
|
|
520
|
+
width: 300,
|
|
521
|
+
rows: 4, // visible rows; longer text scrolls
|
|
522
|
+
placeholder: 'Findings...',
|
|
523
|
+
maxLength: 2000,
|
|
524
|
+
onInput: (text) => preview(text), // every change
|
|
525
|
+
onChange: (text) => save(text), // Meta/Ctrl+Enter or blur, when changed
|
|
526
|
+
onSubmit: (text) => run(text), // Meta/Ctrl+Enter, always
|
|
527
|
+
})
|
|
528
|
+
areas.setValue('notes', '') // silent
|
|
529
|
+
areas.getValue('notes') // the committed text
|
|
530
|
+
areas.getText('notes') // what the field shows, edit in progress included
|
|
531
|
+
areas.getFirstRow('notes') // the scroll position, in rows
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`TextAreaStyle` adds to the text-input knobs a `lineHeight` (rows are
|
|
535
|
+
`textSizePx * lineHeight`, default 1.4), the default `rows`, and the
|
|
536
|
+
scrollbar's `scrollbarWidth`, `scrollbarGap`, `scrollbarColor` and
|
|
537
|
+
`scrollbarTrackColor`. The pure model (`wrapRows`, `rowOfCaret`,
|
|
538
|
+
`moveCaretRows`, `textAreaKey`) is package-private; hosts that lay out
|
|
539
|
+
multi-line text themselves should ask for it to be exported.
|
|
540
|
+
|
|
541
|
+
## Color controls
|
|
542
|
+
|
|
543
|
+
`UIKitColorControlOverlay` edits a color the way the legacy demos do, with
|
|
544
|
+
channel sliders and named palette choices rather than a browser color picker.
|
|
545
|
+
Each control shows a preview swatch (over a grey backdrop, so a translucent
|
|
546
|
+
color shows its alpha), a stack of R, G and B sliders (and A with `alpha`)
|
|
547
|
+
composed from a slider overlay, and a row of palette swatches that wraps to
|
|
548
|
+
the control's width. Dragging a slider, or pressing an arrow, Home, End or
|
|
549
|
+
Page key on the focused one, changes a channel; pressing a swatch sets the
|
|
550
|
+
whole color, and the swatch matching the current color wears a ring.
|
|
551
|
+
`onInput` fires on every change and `onChange` once per committed one (a
|
|
552
|
+
slider release, a key press, a swatch press).
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
const colors = new UIKitColorControlOverlay(font, units)
|
|
556
|
+
colors.addColorControl({
|
|
557
|
+
id: 'crosshair',
|
|
558
|
+
label: 'Crosshair',
|
|
559
|
+
x: 12,
|
|
560
|
+
y: 380,
|
|
561
|
+
width: 220,
|
|
562
|
+
value: [1, 0, 0, 1],
|
|
563
|
+
alpha: false,
|
|
564
|
+
palette: [
|
|
565
|
+
{ name: 'Red', color: [1, 0, 0, 1] },
|
|
566
|
+
{ name: 'Green', color: [0, 1, 0, 1] },
|
|
567
|
+
{ name: 'White', color: [1, 1, 1, 1] },
|
|
568
|
+
],
|
|
569
|
+
onInput: (c) => { nv.crosshairColor = c },
|
|
570
|
+
onChange: (c) => save(c),
|
|
571
|
+
})
|
|
572
|
+
colors.setValue('crosshair', [0, 1, 0, 1]) // silent
|
|
573
|
+
colors.getValue('crosshair')
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
`ColorControlStyle` holds the text color and size with the label halo
|
|
577
|
+
(`textOutlineWidth`, `textOutlineColor`), the preview size, radius, border
|
|
578
|
+
and backdrop, the gaps, `slider` overrides for the channel sliders, and the
|
|
579
|
+
swatch size, gap, radius, border and the selected and hover rings.
|
|
580
|
+
|
|
581
|
+
## Host bridges: file pickers, downloads and the clipboard
|
|
582
|
+
|
|
583
|
+
Opening a file chooser, saving a download and reaching the clipboard are the
|
|
584
|
+
things a canvas widget cannot do by itself. UIKit keeps the affordance and
|
|
585
|
+
its state on the canvas and hands the browser-only step to a host bridge:
|
|
586
|
+
`FilePickerBridge` opens the chooser and resolves with the files (an empty
|
|
587
|
+
array on cancel; where the browser has no `cancel` event, once the window
|
|
588
|
+
has had focus back for a second with no pick), `DownloadBridge` saves a blob
|
|
589
|
+
under a name (revoking its object URL well after the click), and
|
|
590
|
+
`ClipboardBridge` writes text and reads it back (`read` resolves empty when
|
|
591
|
+
the host refuses). `createBrowserFilePicker()`, `createBrowserDownload()`
|
|
592
|
+
and `createBrowserClipboard()` are the browser implementations (a hidden
|
|
593
|
+
`<input type="file">`, a temporary anchor over an object URL,
|
|
594
|
+
`navigator.clipboard`); each takes the browser object as a parameter so a
|
|
595
|
+
test or another host can supply its own. Browsers only open a chooser or
|
|
596
|
+
grant the clipboard inside a user activation, so call a bridge synchronously
|
|
597
|
+
from a click, menu or key handler, as the widgets do. The text input and
|
|
598
|
+
text area create the browser clipboard on first use unless their `clipboard`
|
|
599
|
+
option names another bridge.
|
|
600
|
+
|
|
601
|
+
`UIKitFilePickerOverlay` is the canvas half of `<input type="file">`: a push
|
|
602
|
+
button ("Choose file...") composed from a button overlay beside a readout of
|
|
603
|
+
what was chosen ("No file chosen", one name, or "3 files"; a long name keeps
|
|
604
|
+
its tail behind leading dots). Pressing the button, Enter or Space on it, or
|
|
605
|
+
`open(id)` asks the bridge with the spec's `accept`, `multiple` and
|
|
606
|
+
`directory`; the picker then shows the names and fires `onPick`, or
|
|
607
|
+
`onCancel` when the chooser closed empty.
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
import {
|
|
611
|
+
acceptExtensions,
|
|
612
|
+
createBrowserDownload,
|
|
613
|
+
UIKitFilePickerOverlay,
|
|
614
|
+
} from '@niivue/uikit'
|
|
615
|
+
|
|
616
|
+
const pickers = new UIKitFilePickerOverlay(font, units) // pickFiles: a custom bridge
|
|
617
|
+
pickers.addFilePicker({
|
|
618
|
+
id: 'volume',
|
|
619
|
+
label: 'Open volume...',
|
|
620
|
+
x: 12,
|
|
621
|
+
y: 480,
|
|
622
|
+
width: 300,
|
|
623
|
+
accept: acceptExtensions(['nii', 'nii.gz', 'mgz']),
|
|
624
|
+
onPick: ([file]) => nv.loadVolumes([{ url: file, colormap: 'Gray' }]),
|
|
625
|
+
})
|
|
626
|
+
pickers.setFiles('volume', ['mni152.nii.gz']) // silent
|
|
627
|
+
pickers.getFiles('volume') // the shown names
|
|
628
|
+
|
|
629
|
+
const download = createBrowserDownload()
|
|
630
|
+
menus.addMenu({ id: 'file', label: 'File', x: 12, y: 12, items: [
|
|
631
|
+
{ id: 'export', label: 'Export settings', onSelect: () =>
|
|
632
|
+
download(new Blob([JSON.stringify(settings)], { type: 'application/json' }), 'settings.json') },
|
|
633
|
+
]})
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
`FilePickerStyle` holds the `button` overrides, the gap, the readout colors
|
|
637
|
+
(chosen, empty and disabled), size and halo, and the default readout width.
|
|
638
|
+
|
|
639
|
+
## Dialogs
|
|
640
|
+
|
|
641
|
+
`UIKitDialogOverlay` holds dialogs that open one at a time. An open dialog is
|
|
642
|
+
modal: a scrim covers the canvas, the panel (centered, or placed at the spec's
|
|
643
|
+
`x`, `y`) shows a title, a message wrapped to the panel width (`\n` forces a
|
|
644
|
+
line), a content box `contentHeight` tall for hosted widgets, and a row of
|
|
645
|
+
right-aligned action buttons. Pressing a button closes the dialog and fires
|
|
646
|
+
`onClose` with that button's id. Enter presses the button with the `default`
|
|
647
|
+
role (drawn in the accent style), Escape the one with the `cancel` role, or
|
|
648
|
+
closes with a null result when there is none. A press on the scrim closes with
|
|
649
|
+
null only when `closeOnScrim` is set; otherwise it does nothing, and every
|
|
650
|
+
other pointer, wheel and key event is swallowed while the dialog is open.
|
|
651
|
+
|
|
652
|
+
Hosted widgets are ordinary overlays (text inputs, number inputs, toggles,
|
|
653
|
+
selects, ...) added with `addChild`: while the dialog is open it routes
|
|
654
|
+
events among them like a control layer (a press captures and focuses, a
|
|
655
|
+
modal child such as an open select sees everything first) and draws them over
|
|
656
|
+
the panel. A focused child keeps the keys it takes: Enter submits the dialog
|
|
657
|
+
only from a child whose `submitsForm` says so (a text or number input's
|
|
658
|
+
Enter, a text area's Meta or Ctrl plus Enter), so a text area's Enter is a new
|
|
659
|
+
line and a select's opens its list. They must use the same units as the dialog. `onLayout` reports the
|
|
660
|
+
panel and content boxes in spec units whenever the panel is placed (the first
|
|
661
|
+
draw, a resize), which is where to position them. A dialog is not a popup: a
|
|
662
|
+
press elsewhere in the page only blurs its children, so a window blur never
|
|
663
|
+
closes a half-filled form. Keys with Meta or Ctrl held that the form does not
|
|
664
|
+
use (reload, find, zoom) pass through to the browser. The dialog owns what it hosts: `removeDialog` and
|
|
665
|
+
`destroy` release the children's GPU resources along with the action
|
|
666
|
+
buttons, so `controls.destroy()` tears a whole form down; `removeChild`
|
|
667
|
+
first to keep a child alive.
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
const dialogs = new UIKitDialogOverlay(font, units)
|
|
671
|
+
const fields = new UIKitTextInputOverlay(font, units)
|
|
672
|
+
controls.add(dialogs) // the dialog hosts `fields`; do not add them to the layer
|
|
673
|
+
dialogs.addDialog({
|
|
674
|
+
id: 'rename',
|
|
675
|
+
title: 'Rename volume',
|
|
676
|
+
message: 'The name shows in the layer list and in exports.',
|
|
677
|
+
contentHeight: 36,
|
|
678
|
+
buttons: [
|
|
679
|
+
{ id: 'cancel', label: 'Cancel', role: 'cancel' },
|
|
680
|
+
{ id: 'ok', label: 'Rename', role: 'default' },
|
|
681
|
+
],
|
|
682
|
+
onLayout: ({ content }) => {
|
|
683
|
+
fields.updateTextInput('name', { x: content.x, y: content.y + 4 })
|
|
684
|
+
},
|
|
685
|
+
onClose: (result) => {
|
|
686
|
+
if (result === 'ok') rename(fields.getValue('name'))
|
|
687
|
+
},
|
|
688
|
+
})
|
|
689
|
+
dialogs.addChild('rename', fields)
|
|
690
|
+
fields.addTextInput({ id: 'name', x: 0, y: 0, width: 300, value: 'mni152' })
|
|
691
|
+
dialogs.open('rename')
|
|
692
|
+
dialogs.click('ok') // as the user would: closes and fires onClose('ok')
|
|
693
|
+
dialogs.close('cancel') // from code, with any result
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
`DialogStyle` holds the scrim color, the panel fill, border and radius, its
|
|
697
|
+
default `width`, `padding` and `gap`, the title and message colors and sizes,
|
|
698
|
+
the message `lineHeight`, the `buttonGap`, and `button` and `defaultButton`
|
|
699
|
+
overrides of the button style.
|
|
700
|
+
|
|
701
|
+
## Demo pages
|
|
702
|
+
|
|
703
|
+
`bunx nx dev uikit` serves the package's own demo pages with Vite; the dev
|
|
704
|
+
server also mounts `@niivue/dev-images`, so the pages can load real volumes
|
|
705
|
+
from `/volumes/...`. Each page's nav links to the others.
|
|
706
|
+
|
|
707
|
+
| Page | Shows |
|
|
708
|
+
| --- | --- |
|
|
709
|
+
| `index.html` | The ruler on a blank pane |
|
|
710
|
+
| `volume.html` | The ruler over a NiiVue volume, measuring in millimetres from the slice pick |
|
|
711
|
+
| `slide.html` | The ruler over a Deep Zoom slide in the 3D render tile, measuring in screen pixels |
|
|
712
|
+
| `slide-wsi.html` | The ruler on the standalone WSI viewer with a DICOM whole-slide image |
|
|
713
|
+
| `buttons.html` | `UIKitButtonOverlay` on its own: view buttons, a toggling label and a styled reset |
|
|
714
|
+
| `controls.html` | Every widget on one `UIKitControls` layer over a volume: a menu bar, segmented control, select, toggles, sliders, number and text inputs, a text area, a color control, a file picker and a dialog |
|
|
715
|
+
|
|
716
|
+
## Conventions for contributors
|
|
717
|
+
|
|
718
|
+
- **A widget is a model file plus an overlay file.** `foo.ts` holds the pure
|
|
719
|
+
model: the `FooSpec` and `FooStyle` interfaces, `DEFAULT_FOO_STYLE`,
|
|
720
|
+
`resolveFooStyle`, `layoutFoo` (spec to boxes), `scaleFoo` (spec and style
|
|
721
|
+
into device pixels for `cssUnits`), `fooContains` (hit test) and `buildFoo`
|
|
722
|
+
(layout and state to line, text and rect draw data). It imports no GPU code
|
|
723
|
+
and is tested in `foo.test.ts` without one. `fooOverlay.ts` holds
|
|
724
|
+
`UIKitFooOverlay`: it keeps the specs and interaction state, implements
|
|
725
|
+
`UIKitInteractive` (`hitTest`, the pointer methods, `keyDown`, `wheel` where
|
|
726
|
+
it scrolls, `blur`, `bindLayer`) and draws through the lifecycle hook.
|
|
727
|
+
`fooOverlay.test.ts` drives it through those methods with a stub renderer.
|
|
728
|
+
(The select is the exception: `select.ts` is helper functions that turn a
|
|
729
|
+
`SelectSpec` into a menu spec, and its overlay wraps a menu overlay.)
|
|
730
|
+
- **Overlays share the primitives.** Draw through `UIKitRectOverlay`,
|
|
731
|
+
`UIKitLineOverlay` and `UIKitTextOverlay` (and the `render/` renderers under
|
|
732
|
+
them); a composite widget composes another overlay (the file picker holds a
|
|
733
|
+
button overlay, the color control a slider overlay, the select a menu
|
|
734
|
+
overlay) rather than redrawing it.
|
|
735
|
+
- **Shared behaviour lives in shared models.** Row scrolling is `scroll.ts`,
|
|
736
|
+
single-line editing `textEdit.ts` and `textField.ts`, multi-line editing
|
|
737
|
+
`textArea.ts`, text wrapping `wrapText` in `dialog.ts`. Extend those before
|
|
738
|
+
adding a widget-local copy.
|
|
739
|
+
- **Browser-only steps are host bridges in `host.ts`.** A widget never touches
|
|
740
|
+
the DOM, the clipboard or a download itself: it calls a bridge type
|
|
741
|
+
(`FilePickerBridge`, `DownloadBridge`, `ClipboardBridge`) that the overlay
|
|
742
|
+
option names and `createBrowser*` implements, so tests and non-browser hosts
|
|
743
|
+
supply their own.
|
|
744
|
+
- **Callbacks follow one contract.** `onInput` fires on every user change,
|
|
745
|
+
`onChange` once per committed change, `onSubmit` on an explicit submit;
|
|
746
|
+
setters from code (`setValue`, `setChecked`, `setItemChecked`) are silent.
|
|
747
|
+
Every callback receives the widget id last.
|
|
748
|
+
- **Units.** Specs and styles are canvas pixels unless the overlay was made
|
|
749
|
+
with `cssUnits`, in which case the overlay scales them by the device pixel
|
|
750
|
+
ratio at draw time; widgets hosted by a dialog must use the dialog's units.
|
|
751
|
+
- **Export the overlay, its options type, the spec and style types and the
|
|
752
|
+
default style from `src/index.ts`** (the one permitted barrel). The pure
|
|
753
|
+
model (layout, build, hit-test and state helpers) stays package-private:
|
|
754
|
+
it is exercised by the widget's own tests and is not an API. Add the widget
|
|
755
|
+
to the table at the top of this file and to `controls.html`.
|