@niivue/uikit 1.0.0-rc.2 → 1.0.0-rc.21

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.
Files changed (41) hide show
  1. package/README.md +702 -8
  2. package/dist/button.d.ts +97 -0
  3. package/dist/buttonOverlay.d.ts +121 -0
  4. package/dist/colorControl.d.ts +130 -0
  5. package/dist/colorControlOverlay.d.ts +72 -0
  6. package/dist/controls.d.ts +151 -0
  7. package/dist/dialog.d.ts +138 -0
  8. package/dist/dialogOverlay.d.ts +115 -0
  9. package/dist/filePicker.d.ts +95 -0
  10. package/dist/filePickerOverlay.d.ts +75 -0
  11. package/dist/host.d.ts +87 -0
  12. package/dist/index.d.ts +61 -0
  13. package/dist/menu.d.ts +227 -0
  14. package/dist/menuOverlay.d.ts +149 -0
  15. package/dist/numberInput.d.ts +172 -0
  16. package/dist/numberInputOverlay.d.ts +131 -0
  17. package/dist/pointLabel.d.ts +68 -0
  18. package/dist/pointLabelOverlay.d.ts +39 -0
  19. package/dist/rect.d.ts +27 -0
  20. package/dist/rectOverlay.d.ts +13 -0
  21. package/dist/render/glRectRenderer.d.ts +13 -0
  22. package/dist/render/shaders.d.ts +3 -0
  23. package/dist/render/wgpuRectRenderer.d.ts +18 -0
  24. package/dist/scroll.d.ts +70 -0
  25. package/dist/segmented.d.ts +127 -0
  26. package/dist/segmentedOverlay.d.ts +87 -0
  27. package/dist/select.d.ts +66 -0
  28. package/dist/selectOverlay.d.ts +102 -0
  29. package/dist/slider.d.ts +152 -0
  30. package/dist/sliderOverlay.d.ts +92 -0
  31. package/dist/text/layout.d.ts +8 -0
  32. package/dist/textArea.d.ts +216 -0
  33. package/dist/textAreaOverlay.d.ts +145 -0
  34. package/dist/textEdit.d.ts +72 -0
  35. package/dist/textField.d.ts +33 -0
  36. package/dist/textInput.d.ts +123 -0
  37. package/dist/textInputOverlay.d.ts +139 -0
  38. package/dist/toggle.d.ts +115 -0
  39. package/dist/toggleOverlay.d.ts +80 -0
  40. package/dist/uikit.js +6269 -221
  41. package/package.json +3 -3
@@ -0,0 +1,68 @@
1
+ import { LineData } from './line';
2
+ import { RGBA, Vec2 } from './ruler';
3
+ import { UIKitFontMetrics } from './text/font';
4
+ import { UIKitTextItem } from './textOverlay';
5
+ export interface PointLabelBounds {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+ export interface PointLabelSpec {
12
+ /** The labelled point in screen pixels. */
13
+ target: Vec2;
14
+ text: string;
15
+ /**
16
+ * Where the leader ends and the text starts, in screen pixels. Omit to place
17
+ * it `leaderLength` from the target, pointing away from the centre of
18
+ * `bounds` (or up and to the right without bounds).
19
+ */
20
+ anchor?: Vec2;
21
+ /**
22
+ * The drawable area. Used to choose the automatic anchor direction and to
23
+ * keep the text inside the frame: the text flips to the other side of the
24
+ * anchor when it would run off an edge.
25
+ */
26
+ bounds?: PointLabelBounds;
27
+ /**
28
+ * The eight projected corners of a box to outline around the target (a
29
+ * volume brick, say), in screen pixels. Corner `c` sits at the box's
30
+ * minimum on each axis whose bit of `c` is clear and at its maximum where
31
+ * it is set, so the twelve edges join corners whose indices differ in one
32
+ * bit. Omit for no box.
33
+ */
34
+ box?: readonly Vec2[];
35
+ /** Colour of the box edges. Defaults to `lineColor`. */
36
+ boxColor?: RGBA;
37
+ /** Thickness of the box edges in pixels. Defaults to `thickness`. */
38
+ boxThickness?: number;
39
+ /** Leader length for the automatic anchor, in pixels. Default 110. */
40
+ leaderLength?: number;
41
+ /** Text height in pixels. Default 28. */
42
+ sizePx?: number;
43
+ lineColor?: RGBA;
44
+ textColor?: RGBA;
45
+ /** Line thickness in pixels. Default 3. */
46
+ thickness?: number;
47
+ /** Half-size of the diamond marking the target, in pixels. Default 6. */
48
+ markerSize?: number;
49
+ /** Text outline width in pixels, 0 for none. Default 2. */
50
+ textOutlineWidthPx?: number;
51
+ /**
52
+ * Multiplies every pixel size above (given or default), so one spec reads
53
+ * the same on a 1x and a 2x display. The overlay passes the frame's device
54
+ * pixel ratio. Default 1.
55
+ */
56
+ scale?: number;
57
+ }
58
+ export interface PointLabelGeometry {
59
+ lines: LineData[];
60
+ text: UIKitTextItem[];
61
+ /** The resolved anchor (useful for tests and for chaining labels). */
62
+ anchor: Vec2;
63
+ }
64
+ /**
65
+ * Build the line + text draw data for a point label. Pure geometry; the font
66
+ * metrics are only used to measure the text so it can be kept in frame.
67
+ */
68
+ export declare function buildPointLabel(metrics: UIKitFontMetrics, spec: PointLabelSpec): PointLabelGeometry;
@@ -0,0 +1,39 @@
1
+ import { UIKitOverlayFrame, UIKitOverlayRenderer } from '@niivue/niivue';
2
+ import { PointLabelSpec } from './pointLabel';
3
+ import { UIKitFont } from './text/font';
4
+ export type MMPoint = readonly [number, number, number];
5
+ /** Maps a world-mm point to canvas backing-store pixels, or null if off screen. */
6
+ export type MMProjector = (mm: MMPoint) => [number, number] | null;
7
+ /**
8
+ * A label pinned to a world-mm point; the rest styles the callout. Pixel
9
+ * sizes are in CSS pixels: the overlay scales them by the frame's device
10
+ * pixel ratio (times any `scale` given here).
11
+ */
12
+ export type PointLabelPlacement = Omit<PointLabelSpec, 'target' | 'bounds' | 'box'> & {
13
+ mm: MMPoint;
14
+ /**
15
+ * The eight world-mm corners of a box to outline around the point (the
16
+ * volume brick holding it, say), in the corner order `PointLabelSpec.box`
17
+ * describes. The overlay projects them each frame.
18
+ */
19
+ boxMM?: readonly MMPoint[];
20
+ };
21
+ export declare class UIKitPointLabelOverlay implements UIKitOverlayRenderer {
22
+ private project;
23
+ private readonly lines;
24
+ private readonly labels;
25
+ private readonly font;
26
+ private placements;
27
+ constructor(font: UIKitFont, project: MMProjector, labels?: PointLabelPlacement[]);
28
+ /** Replace the labels. Trigger a redraw via the host. */
29
+ setLabels(labels: PointLabelPlacement[]): void;
30
+ /** Swap the projection (after a view change, say). */
31
+ setProjector(project: MMProjector): void;
32
+ /** Remove every label so nothing draws. */
33
+ clear(): void;
34
+ drawOverlay(frame: UIKitOverlayFrame): void;
35
+ /** The box's corners on the canvas, or undefined when any is off screen. */
36
+ private projectBox;
37
+ /** Release GPU resources on both backends. */
38
+ destroy(): void;
39
+ }
package/dist/rect.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export type RectData = {
2
+ data: Float32Array;
3
+ };
4
+ export declare const FLOATS_PER_RECT = 16;
5
+ export interface RectSpec {
6
+ /** Top-left corner in canvas pixels. */
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ /** Corner radius in pixels; clamped to half the shorter side. Default 0. */
12
+ radius?: number;
13
+ /** Border ring width in pixels, drawn inside the rect edge. Default 0. */
14
+ borderWidth?: number;
15
+ /** Interior color. Default an opaque mid grey. */
16
+ fill?: readonly number[];
17
+ /** Border ring color. Default the fill color (an invisible border). */
18
+ border?: readonly number[];
19
+ }
20
+ export declare function buildRect(spec: RectSpec): RectData;
21
+ /** True when canvas point (px, py) lies inside the rect's axis-aligned box. */
22
+ export declare function rectContains(rect: {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ }, px: number, py: number): boolean;
@@ -0,0 +1,13 @@
1
+ import { UIKitOverlayFrame, UIKitOverlayRenderer } from '@niivue/niivue';
2
+ import { RectData } from './rect';
3
+ export declare class UIKitRectOverlay implements UIKitOverlayRenderer {
4
+ private rects;
5
+ private readonly gl;
6
+ private readonly wgpu;
7
+ constructor(rects?: RectData[]);
8
+ /** Replace the rects drawn each frame. Trigger a redraw via the host. */
9
+ setRects(rects: RectData[]): void;
10
+ drawOverlay(frame: UIKitOverlayFrame): void;
11
+ /** Release GPU resources on both backends. */
12
+ destroy(): void;
13
+ }
@@ -0,0 +1,13 @@
1
+ import { RectData } from '../rect';
2
+ export declare class GlRectRenderer {
3
+ private gl;
4
+ private program;
5
+ private vao;
6
+ private buffer;
7
+ private uCanvasSize;
8
+ private scratch;
9
+ private ensure;
10
+ /** Draw `rects` in screen-pixel space over a `width` x `height` bounds rect. */
11
+ draw(gl: WebGL2RenderingContext, rects: RectData[], width: number, height: number): void;
12
+ destroy(): void;
13
+ }
@@ -4,3 +4,6 @@ export declare const WGSL_LINE = "\nstruct LineUniforms {\n canvasSize: vec2f
4
4
  export declare const GL_TEXT_VERT = "#version 300 es\nprecision highp float;\nuniform vec2 canvasSize;\nin vec2 pos;\nin vec2 uv;\nin vec4 color;\nout vec2 vUv;\nout vec4 vColor;\n\nvoid main() {\n vec2 ndc = (pos / canvasSize) * 2.0 - 1.0;\n gl_Position = vec4(ndc.x, -ndc.y, 0.0, 1.0);\n vUv = uv;\n vColor = color;\n}\n";
5
5
  export declare const GL_TEXT_FRAG = "#version 300 es\nprecision highp float;\nuniform highp sampler2D fontTexture;\nuniform float screenPxRange;\nin vec2 vUv;\nin vec4 vColor;\nout vec4 fragColor;\n\nfloat median(float r, float g, float b) {\n return max(min(r, g), min(max(r, g), b));\n}\n\nvoid main() {\n vec3 msd = texture(fontTexture, vUv).rgb;\n float sd = median(msd.r, msd.g, msd.b);\n float opacity = clamp(screenPxRange * (sd - 0.5) + 0.5, 0.0, 1.0);\n if (opacity <= 0.0) discard;\n fragColor = vec4(vColor.rgb, vColor.a * opacity);\n}\n";
6
6
  export declare const WGSL_TEXT = "\nstruct TextUniforms {\n canvasSize: vec2f,\n screenPxRange: f32,\n _pad: f32,\n};\n\n@group(0) @binding(0) var<uniform> u: TextUniforms;\n@group(0) @binding(1) var fontTex: texture_2d<f32>;\n@group(0) @binding(2) var fontSampler: sampler;\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n @location(1) color: vec4f,\n};\n\n@vertex\nfn vertex_main(\n @location(0) pos: vec2f,\n @location(1) uv: vec2f,\n @location(2) color: vec4f\n) -> VertexOutput {\n var out: VertexOutput;\n let ndc = (pos / u.canvasSize) * 2.0 - 1.0;\n out.position = vec4f(ndc.x, -ndc.y, 0.0, 1.0);\n out.uv = uv;\n out.color = color;\n return out;\n}\n\nfn median(r: f32, g: f32, b: f32) -> f32 {\n return max(min(r, g), min(max(r, g), b));\n}\n\n@fragment\nfn fragment_main(in: VertexOutput) -> @location(0) vec4f {\n let msd = textureSample(fontTex, fontSampler, in.uv).rgb;\n let sd = median(msd.r, msd.g, msd.b);\n let opacity = clamp(u.screenPxRange * (sd - 0.5) + 0.5, 0.0, 1.0);\n return vec4f(in.color.rgb, in.color.a * opacity);\n}\n";
7
+ export declare const GL_RECT_VERT = "#version 300 es\nprecision highp float;\nuniform vec2 canvasSize;\nin vec2 rectPos;\nin vec2 rectSize;\nin vec2 rectShape; // (cornerRadius, borderWidth)\nin vec4 rectFill;\nin vec4 rectBorder;\nout vec2 vLocal;\nout vec2 vHalfSize;\nout vec2 vShape;\nout vec4 vFill;\nout vec4 vBorder;\n\nvoid main() {\n int vIdx = gl_VertexID;\n // Corner order (0,0) (0,1) (1,0) (1,1): the strip's first triangle winds\n // counter-clockwise in clip space like the line and text quads, so the quad\n // survives when the host leaves back-face culling enabled.\n vec2 corner = vec2(float(vIdx >> 1), float(vIdx & 1));\n vec2 halfSize = rectSize * 0.5;\n vec2 centre = rectPos + halfSize;\n vec2 grown = halfSize + 1.0;\n vec2 local = (corner * 2.0 - 1.0) * grown;\n vec2 pixelPos = centre + local;\n vec2 ndc = (pixelPos / canvasSize) * 2.0 - 1.0;\n gl_Position = vec4(ndc.x, -ndc.y, 0.0, 1.0);\n vLocal = local;\n vHalfSize = halfSize;\n vShape = rectShape;\n vFill = rectFill;\n vBorder = rectBorder;\n}\n";
8
+ export declare const GL_RECT_FRAG = "#version 300 es\nprecision highp float;\nin vec2 vLocal;\nin vec2 vHalfSize;\nin vec2 vShape;\nin vec4 vFill;\nin vec4 vBorder;\nout vec4 fragColor;\n\nfloat sdRoundBox(vec2 p, vec2 halfSize, float r) {\n vec2 q = abs(p) - halfSize + r;\n return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;\n}\n\nvoid main() {\n float d = sdRoundBox(vLocal, vHalfSize, vShape.x);\n float coverage = 1.0 - smoothstep(-0.5, 0.5, d);\n // The border ring is the band within borderWidth of the outer edge.\n float ring = vShape.y > 0.0 ? smoothstep(-0.5, 0.5, d + vShape.y) : 0.0;\n vec4 color = mix(vFill, vBorder, ring);\n float alpha = color.a * coverage;\n if (alpha <= 0.0) discard;\n fragColor = vec4(color.rgb, alpha);\n}\n";
9
+ export declare const WGSL_RECT = "\nstruct RectUniforms {\n canvasSize: vec2f,\n};\n\nstruct Rect {\n pos: vec2f,\n size: vec2f,\n radius: f32,\n border: f32,\n _pad0: f32,\n _pad1: f32,\n fill: vec4f,\n borderColor: vec4f,\n};\n\n@group(0) @binding(0) var<uniform> u: RectUniforms;\n@group(0) @binding(1) var<storage, read> rects: array<Rect>;\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) local: vec2f,\n @location(1) halfSize: vec2f,\n @location(2) shape: vec2f,\n @location(3) fill: vec4f,\n @location(4) borderColor: vec4f,\n};\n\n@vertex\nfn vertex_main(\n @builtin(vertex_index) vIdx: u32,\n @builtin(instance_index) iIdx: u32\n) -> VertexOutput {\n let rect = rects[iIdx];\n let corner = vec2f(f32(vIdx >> 1u), f32(vIdx & 1u));\n let halfSize = rect.size * 0.5;\n let centre = rect.pos + halfSize;\n let grown = halfSize + 1.0;\n let local = (corner * 2.0 - 1.0) * grown;\n let pixelPos = centre + local;\n let ndc = (pixelPos / u.canvasSize) * 2.0 - 1.0;\n var out: VertexOutput;\n out.position = vec4f(ndc.x, -ndc.y, 0.0, 1.0);\n out.local = local;\n out.halfSize = halfSize;\n out.shape = vec2f(rect.radius, rect.border);\n out.fill = rect.fill;\n out.borderColor = rect.borderColor;\n return out;\n}\n\nfn sdRoundBox(p: vec2f, halfSize: vec2f, r: f32) -> f32 {\n let q = abs(p) - halfSize + r;\n return min(max(q.x, q.y), 0.0) + length(max(q, vec2f(0.0))) - r;\n}\n\n@fragment\nfn fragment_main(in: VertexOutput) -> @location(0) vec4f {\n let d = sdRoundBox(in.local, in.halfSize, in.shape.x);\n let coverage = 1.0 - smoothstep(-0.5, 0.5, d);\n var ring = 0.0;\n if (in.shape.y > 0.0) {\n ring = smoothstep(-0.5, 0.5, d + in.shape.y);\n }\n let color = mix(in.fill, in.borderColor, ring);\n return vec4f(color.rgb, color.a * coverage);\n}\n";
@@ -0,0 +1,18 @@
1
+ import { RectData } from '../rect';
2
+ export declare class WgpuRectRenderer {
3
+ private device;
4
+ private pipeline;
5
+ private bindLayout;
6
+ private paramsBuffer;
7
+ private storageBuffer;
8
+ private bindGroup;
9
+ private capacity;
10
+ private key;
11
+ private scratch;
12
+ private readonly params;
13
+ private ensurePipeline;
14
+ private ensureCapacity;
15
+ /** Append `rects` (screen-pixel space) to the open pass over a bounds rect. */
16
+ draw(device: GPUDevice, pass: GPURenderPassEncoder, colorFormat: GPUTextureFormat, sampleCount: number, depthFormat: GPUTextureFormat | undefined, rects: RectData[], width: number, height: number): void;
17
+ destroy(): void;
18
+ }
@@ -0,0 +1,70 @@
1
+ import { LineData } from './line';
2
+ import { RGBA } from './text/layout';
3
+ /** An axis-aligned box in canvas pixels. */
4
+ export interface UIKitBox {
5
+ x: number;
6
+ y: number;
7
+ width: number;
8
+ height: number;
9
+ }
10
+ /** The run of rows a viewport shows, by index. */
11
+ export interface ScrollRange {
12
+ /** Index of the first visible row. */
13
+ first: number;
14
+ /** Index after the last visible row. */
15
+ end: number;
16
+ }
17
+ /** A scroll range plus what `scrollWindow` learned while fitting it. */
18
+ export interface ScrollWindow extends ScrollRange {
19
+ /** The largest `first` at which the tail of the list still fills the viewport. */
20
+ maxFirst: number;
21
+ /** Total height of the visible rows (at most the viewport height). */
22
+ usedHeight: number;
23
+ }
24
+ /**
25
+ * The rows that fit in a viewport of `viewport` height starting at row
26
+ * `first`, after clamping `first` into [0, maxFirst] so the list never
27
+ * scrolls past its end. Rows are given by their heights in order. A row
28
+ * taller than the viewport is never shown.
29
+ */
30
+ export declare function scrollWindow(heights: readonly number[], viewport: number, first: number): ScrollWindow;
31
+ /**
32
+ * The `first` row that brings row `index` into a viewport currently showing
33
+ * `shown`, moving as little as possible: unchanged when the row is already
34
+ * visible, the row itself when it lies above, and otherwise the earliest row
35
+ * from which `index` is still the last one that fits.
36
+ */
37
+ export declare function revealRow(heights: readonly number[], viewport: number, shown: ScrollRange, index: number): number;
38
+ /** True when rows precede the shown range. */
39
+ export declare function canScrollUp(shown: ScrollRange): boolean;
40
+ /** True when rows follow the shown range. */
41
+ export declare function canScrollDown(shown: ScrollRange, rowCount: number): boolean;
42
+ /**
43
+ * Turns a stream of wheel deltas (pixels) into whole row steps, carrying the
44
+ * remainder so a trackpad's small deltas add up. `rowStep` is the number of
45
+ * pixels per row.
46
+ */
47
+ export declare class WheelAccumulator {
48
+ private readonly rowStep;
49
+ private carry;
50
+ constructor(rowStep: number);
51
+ /** Feed a delta; returns the number of rows to scroll (negative is up). */
52
+ add(delta: number): number;
53
+ /** Drop any carried remainder (when the scrolled container closes). */
54
+ reset(): void;
55
+ }
56
+ /** The look of the strips a container reserves at each end of a scrolled list. */
57
+ export interface ScrollStripStyle {
58
+ /** Height of each strip. */
59
+ height: number;
60
+ /** Width of the chevron drawn on an active strip. */
61
+ arrowSize: number;
62
+ /** Chevron stroke width. */
63
+ arrowWidth: number;
64
+ arrowColor: RGBA;
65
+ }
66
+ /**
67
+ * The chevron on a scroll strip: pointing up for `direction` -1, down for 1,
68
+ * centred in `strip`.
69
+ */
70
+ export declare function buildScrollArrow(strip: UIKitBox, direction: -1 | 1, style: ScrollStripStyle): LineData[];
@@ -0,0 +1,127 @@
1
+ import { LineData } from './line';
2
+ import { RectData } from './rect';
3
+ import { UIKitFontMetrics } from './text/font';
4
+ import { RGBA } from './text/layout';
5
+ import { UIKitTextItem } from './textOverlay';
6
+ /** Every visual knob on a segmented control. All lengths are in canvas pixels. */
7
+ export interface SegmentedStyle {
8
+ /** Control height, unless the spec fixes one. */
9
+ height: number;
10
+ /** Space between a segment's edge and its label (sets the segment width). */
11
+ paddingX: number;
12
+ /** Space between the track edge and the segment faces. */
13
+ inset: number;
14
+ /** Corner radius of the track; segment faces use what is left after the inset. */
15
+ radius: number;
16
+ trackFill: RGBA;
17
+ trackBorder: RGBA;
18
+ trackBorderWidth: number;
19
+ /** Face of the selected segment. */
20
+ selectedFill: RGBA;
21
+ /** Face of an unselected segment under the pointer. */
22
+ hoverFill: RGBA;
23
+ /** Face of a segment while the pointer holds it down. */
24
+ pressedFill: RGBA;
25
+ textColor: RGBA;
26
+ selectedTextColor: RGBA;
27
+ disabledTextColor: RGBA;
28
+ /** Label em size. */
29
+ textSizePx: number;
30
+ /** Rule between two adjacent unselected segments. */
31
+ dividerColor: RGBA;
32
+ /** Focus ring color (around the track while the control has keyboard focus). */
33
+ focusRing: RGBA;
34
+ focusRingWidth: number;
35
+ }
36
+ export declare const DEFAULT_SEGMENTED_STYLE: SegmentedStyle;
37
+ export interface SegmentSpec {
38
+ /** The value reported to `onChange` and held by the control. Unique per control. */
39
+ value: string;
40
+ label: string;
41
+ /** A disabled segment draws dimmed and cannot be chosen. Default true. */
42
+ enabled?: boolean;
43
+ }
44
+ export interface SegmentedSpec {
45
+ /** Stable key: the handle for updates, pointer tracking and the callback. */
46
+ id: string;
47
+ /** Top-left corner of the track, in canvas pixels. */
48
+ x: number;
49
+ y: number;
50
+ segments: readonly SegmentSpec[];
51
+ /** The selected segment's value, or null for none. */
52
+ value: string | null;
53
+ /** Width of every segment. Defaults to the widest label plus padding. */
54
+ segmentWidth?: number;
55
+ /** Control height. Defaults to the style's. */
56
+ height?: number;
57
+ /** A disabled control draws dimmed and ignores input. Default true. */
58
+ enabled?: boolean;
59
+ /** Per-control overrides of the overlay's default style. */
60
+ style?: Partial<SegmentedStyle>;
61
+ /** Fired when the user selects a different segment (pointer or keyboard), not by `setValue`. */
62
+ onChange?: (value: string, id: string) => void;
63
+ }
64
+ /** One segment's face, in canvas pixels. */
65
+ export interface SegmentLayout {
66
+ index: number;
67
+ x: number;
68
+ y: number;
69
+ width: number;
70
+ height: number;
71
+ }
72
+ /** The track and its segments, in canvas pixels. */
73
+ export interface SegmentedLayout {
74
+ x: number;
75
+ y: number;
76
+ width: number;
77
+ height: number;
78
+ segments: SegmentLayout[];
79
+ }
80
+ /** The transient state that changes how a segmented control looks. */
81
+ export interface SegmentedVisual {
82
+ /** Index of the selected segment, or -1. */
83
+ selected: number;
84
+ /** Index of the segment under the pointer, or -1. */
85
+ hover: number;
86
+ /** Index of the segment the pointer holds down, or -1. */
87
+ pressed: number;
88
+ focused: boolean;
89
+ enabled: boolean;
90
+ }
91
+ export declare function resolveSegmentedStyle(base: SegmentedStyle, override?: Partial<SegmentedStyle>): SegmentedStyle;
92
+ /** The track for a spec, with equal-width segment faces inside the inset. */
93
+ export declare function layoutSegmented(spec: SegmentedSpec, style: SegmentedStyle, metrics: UIKitFontMetrics): SegmentedLayout;
94
+ /** Scale every length in a spec and style by `k` (CSS pixels to canvas pixels). */
95
+ export declare function scaleSegmented(spec: SegmentedSpec, style: SegmentedStyle, k: number): {
96
+ spec: SegmentedSpec;
97
+ style: SegmentedStyle;
98
+ };
99
+ /** True when canvas point (px, py) lies inside the track. */
100
+ export declare function segmentedContains(layout: SegmentedLayout, px: number, py: number): boolean;
101
+ /**
102
+ * The index of the segment under a canvas point, or -1. The inset around the
103
+ * faces counts toward the nearest segment, so the whole track is clickable.
104
+ */
105
+ export declare function segmentAt(layout: SegmentedLayout, px: number, py: number): number;
106
+ /** The index of the segment holding `value`, or -1. */
107
+ export declare function segmentIndex(segments: readonly SegmentSpec[], value: string | null): number;
108
+ /**
109
+ * The value of the next enabled segment from the current one in `direction`,
110
+ * wrapping around like a radio group. From no value, the first step lands on
111
+ * the first (or last) enabled segment. Returns the current value when no
112
+ * segment is enabled.
113
+ */
114
+ export declare function stepSegmentValue(segments: readonly SegmentSpec[], value: string | null, direction: 1 | -1): string | null;
115
+ /** The first or last enabled segment's value, or the current one when there is none. */
116
+ export declare function endSegmentValue(segments: readonly SegmentSpec[], value: string | null, end: 'first' | 'last'): string | null;
117
+ /**
118
+ * The draw data for one segmented control in one visual state: a focus ring
119
+ * when focused, the track, a face under the selected segment and under a
120
+ * hovered or pressed one, dividers between adjacent plain segments, and the
121
+ * centred labels.
122
+ */
123
+ export declare function buildSegmented(spec: SegmentedSpec, style: SegmentedStyle, metrics: UIKitFontMetrics, layout: SegmentedLayout, visual: SegmentedVisual): {
124
+ rects: RectData[];
125
+ lines: LineData[];
126
+ text: UIKitTextItem[];
127
+ };
@@ -0,0 +1,87 @@
1
+ import { UIKitOverlayFrame, UIKitOverlayRenderer } from '@niivue/niivue';
2
+ import { UIKitInteractive, UIKitKeyEvent, UIKitRedrawSource } from './controls';
3
+ import { SegmentedLayout, SegmentedSpec, SegmentedStyle, SegmentSpec } from './segmented';
4
+ import { UIKitFont } from './text/font';
5
+ export interface UIKitSegmentedOverlayOptions {
6
+ /** Called whenever the overlay needs another frame. A control layer fills it in. */
7
+ requestRedraw?: () => void;
8
+ /** Default style for every control; a spec's `style` overrides per key. */
9
+ style?: Partial<SegmentedStyle>;
10
+ /**
11
+ * Interpret spec positions and style lengths as CSS pixels and scale them by
12
+ * the frame's device pixel ratio, instead of canvas pixels.
13
+ */
14
+ cssUnits?: boolean;
15
+ }
16
+ export declare class UIKitSegmentedOverlay implements UIKitOverlayRenderer, UIKitInteractive {
17
+ readonly hoverCursor = "pointer";
18
+ private readonly font;
19
+ private readonly rects;
20
+ private readonly lines;
21
+ private readonly labels;
22
+ private readonly entries;
23
+ private baseStyle;
24
+ private readonly cssUnits;
25
+ private requestRedraw;
26
+ private scale;
27
+ private geometryDirty;
28
+ private pressedId;
29
+ /** Index of the segment the press started on. */
30
+ private pressedIndex;
31
+ /** While pressed: whether the pointer is over the pressed segment. */
32
+ private pressInside;
33
+ private hoverId;
34
+ private focusedId;
35
+ private layer;
36
+ constructor(font: UIKitFont, options?: UIKitSegmentedOverlayOptions);
37
+ /** Replace the default style for every control (per-spec overrides still win). */
38
+ setDefaultStyle(style: Partial<SegmentedStyle>): void;
39
+ /**
40
+ * Add a control, or replace the one with the same id. A replacement takes
41
+ * the new spec's value; a value no segment holds clears the selection.
42
+ */
43
+ addSegmented(spec: SegmentedSpec): void;
44
+ /** Replace the whole control set. */
45
+ setSegmenteds(specs: readonly SegmentedSpec[]): void;
46
+ removeSegmented(id: string): void;
47
+ /** Patch one control's spec. The value is kept unless the patch sets `value`. */
48
+ updateSegmented(id: string, patch: Partial<Omit<SegmentedSpec, 'id'>>): void;
49
+ setEnabled(id: string, enabled: boolean): void;
50
+ /** Replace a control's segments. A value no longer offered clears. */
51
+ setSegments(id: string, segments: readonly SegmentSpec[]): void;
52
+ getSegments(id: string): readonly SegmentSpec[];
53
+ /** Set a control's value from code. Does not fire `onChange`. Unknown values clear it. */
54
+ setValue(id: string, value: string | null): void;
55
+ getValue(id: string): string | null;
56
+ /** Select a segment as the user would (a keyboard shortcut, say): fires `onChange`. */
57
+ select(id: string, value: string): void;
58
+ /** The ids of all controls, in draw order. */
59
+ get ids(): string[];
60
+ /** A control's track and segments in spec units (canvas pixels, or CSS pixels with `cssUnits`). */
61
+ getLayout(id: string): SegmentedLayout | null;
62
+ /** The control with keyboard focus, if any. */
63
+ get focusedSegmented(): string | null;
64
+ /** Give a control keyboard focus (arrows move the selection), or null to clear. */
65
+ focus(id: string | null): void;
66
+ hitTest(x: number, y: number): boolean;
67
+ /** Presses the segment under the pointer (disabled segments still take focus). */
68
+ pointerDown(x: number, y: number): boolean;
69
+ pointerMove(x: number, y: number): boolean;
70
+ /** Selects the pressed segment when released over it. */
71
+ pointerUp(x: number, y: number): boolean;
72
+ pointerCancel(): void;
73
+ /** Arrows move the focused control's selection (wrapping); Home and End jump to the ends. */
74
+ keyDown(e: UIKitKeyEvent): boolean;
75
+ blur(): void;
76
+ bindLayer(layer: UIKitRedrawSource): void;
77
+ drawOverlay(frame: UIKitOverlayFrame): void;
78
+ /** Release GPU resources on both backends. */
79
+ destroy(): void;
80
+ private invalidate;
81
+ private segmentEnabled;
82
+ private scaled;
83
+ private layoutOf;
84
+ /** The topmost (last added) enabled control under a canvas point. */
85
+ private hitEntry;
86
+ private rebuild;
87
+ }
@@ -0,0 +1,66 @@
1
+ import { ButtonStyle } from './button';
2
+ import { MenuButtonSpec, MenuItemSpec, MenuStyle } from './menu';
3
+ import { UIKitFontMetrics } from './text/font';
4
+ export interface SelectOption {
5
+ /** The value reported to `onChange` and held by the select. Unique per select. */
6
+ value: string;
7
+ label: string;
8
+ /** A disabled option draws dimmed and cannot be chosen. Default true. */
9
+ enabled?: boolean;
10
+ }
11
+ export interface SelectSpec {
12
+ /** Stable key: the handle for updates, open state and the callback. */
13
+ id: string;
14
+ /** Top-left corner of the button, in canvas pixels. */
15
+ x: number;
16
+ y: number;
17
+ /**
18
+ * Fixed button size. Width defaults to the widest option label (and the
19
+ * placeholder) so the button keeps its size as the value changes; height
20
+ * defaults to the label plus padding.
21
+ */
22
+ width?: number;
23
+ height?: number;
24
+ options: readonly SelectOption[];
25
+ /** The chosen option's value, or null for none (the placeholder shows). */
26
+ value: string | null;
27
+ /** Shown while nothing is chosen. Default ''. */
28
+ placeholder?: string;
29
+ /** A disabled select draws dimmed and ignores input. Default true. */
30
+ enabled?: boolean;
31
+ /** Per-select overrides of the overlay's default button style. */
32
+ buttonStyle?: Partial<ButtonStyle>;
33
+ /** Per-select overrides of the overlay's default popup style. */
34
+ style?: Partial<MenuStyle>;
35
+ /** Fired when the user chooses a different option (pointer or keyboard), not by `setValue`. */
36
+ onChange?: (value: string, id: string) => void;
37
+ }
38
+ /** The radio group every select's options share in the menu model. */
39
+ export declare const SELECT_GROUP = "select";
40
+ /** The option holding `value`, if any. */
41
+ export declare function selectOption(options: readonly SelectOption[], value: string | null): SelectOption | null;
42
+ /** The text the button shows: the chosen option's label, else the placeholder. */
43
+ export declare function selectLabel(spec: SelectSpec): string;
44
+ /**
45
+ * The button width that fits the widest option label and the placeholder,
46
+ * plus padding and the chevron. Used when the spec fixes no width.
47
+ */
48
+ export declare function selectWidth(spec: SelectSpec, buttonStyle: ButtonStyle, style: MenuStyle, metrics: UIKitFontMetrics): number;
49
+ /** The options as menu items: one radio group with the chosen option checked. */
50
+ export declare function selectItems(spec: SelectSpec): MenuItemSpec[];
51
+ /**
52
+ * The `MenuButtonSpec` a select draws and behaves as. Callbacks are left off:
53
+ * the select overlay reports value changes through its own `onChange`.
54
+ */
55
+ export declare function selectMenuSpec(spec: SelectSpec, buttonStyle: ButtonStyle, style: MenuStyle, metrics: UIKitFontMetrics): MenuButtonSpec;
56
+ /** The index of the option holding `value`, or -1. */
57
+ export declare function selectIndex(options: readonly SelectOption[], value: string | null): number;
58
+ /**
59
+ * The value `count` enabled options away from the current one in `direction`
60
+ * without wrapping (the ends stop), like a closed native select on arrow keys.
61
+ * From no value, the first step lands on the first (or last) enabled option.
62
+ * Returns the current value when nothing lies that way.
63
+ */
64
+ export declare function stepSelectValue(options: readonly SelectOption[], value: string | null, direction: 1 | -1, count?: number): string | null;
65
+ /** The first or last enabled option's value, or the current one when there is none. */
66
+ export declare function endSelectValue(options: readonly SelectOption[], value: string | null, end: 'first' | 'last'): string | null;
@@ -0,0 +1,102 @@
1
+ import { UIKitOverlayFrame, UIKitOverlayRenderer } from '@niivue/niivue';
2
+ import { ButtonLayout, ButtonStyle } from './button';
3
+ import { UIKitInteractive, UIKitKeyEvent, UIKitRedrawSource } from './controls';
4
+ import { MenuBounds, MenuPopupLayout, MenuStyle } from './menu';
5
+ import { SelectOption, SelectSpec } from './select';
6
+ import { UIKitFont } from './text/font';
7
+ export interface UIKitSelectOverlayOptions {
8
+ /** Called whenever the overlay needs another frame. A control layer fills it in. */
9
+ requestRedraw?: () => void;
10
+ /** Default button style for every select; a spec's `buttonStyle` overrides per key. */
11
+ buttonStyle?: Partial<ButtonStyle>;
12
+ /** Default popup style for every select; a spec's `style` overrides per key. */
13
+ style?: Partial<MenuStyle>;
14
+ /**
15
+ * Interpret spec positions and style lengths as CSS pixels and scale them by
16
+ * the frame's device pixel ratio, instead of canvas pixels.
17
+ */
18
+ cssUnits?: boolean;
19
+ }
20
+ export declare class UIKitSelectOverlay implements UIKitOverlayRenderer, UIKitInteractive {
21
+ readonly hoverCursor = "pointer";
22
+ private readonly font;
23
+ private readonly menu;
24
+ private readonly entries;
25
+ private readonly baseButtonStyle;
26
+ private readonly baseStyle;
27
+ constructor(font: UIKitFont, options?: UIKitSelectOverlayOptions);
28
+ /**
29
+ * Add a select, or replace the one with the same id. A replacement takes the
30
+ * new spec's value; an open replaced select closes.
31
+ */
32
+ addSelect(spec: SelectSpec): void;
33
+ /** Replace the whole select set. */
34
+ setSelects(specs: readonly SelectSpec[]): void;
35
+ removeSelect(id: string): void;
36
+ /** Patch one select's spec. The value is kept unless the patch sets `value`. */
37
+ updateSelect(id: string, patch: Partial<Omit<SelectSpec, 'id'>>): void;
38
+ setEnabled(id: string, enabled: boolean): void;
39
+ /**
40
+ * Replace a select's options (closing it if open and the values offered
41
+ * change). A value no longer offered clears.
42
+ */
43
+ setOptions(id: string, options: readonly SelectOption[]): void;
44
+ getOptions(id: string): readonly SelectOption[];
45
+ /** Set a select's value from code. Does not fire `onChange`. Unknown values clear it. */
46
+ setValue(id: string, value: string | null): void;
47
+ getValue(id: string): string | null;
48
+ /** The ids of all selects, in draw order. */
49
+ get ids(): string[];
50
+ /** A select button's box in spec units (canvas pixels, or CSS pixels with `cssUnits`). */
51
+ getLayout(id: string): ButtonLayout | null;
52
+ /** The open list's panel and rows in spec units, or null when none is open. */
53
+ getPopupLayout(): MenuPopupLayout | null;
54
+ /** The id of the select whose list is open, if any. */
55
+ get openSelect(): string | null;
56
+ isOpen(id: string): boolean;
57
+ /** The index of the highlighted option in the open list, or -1. */
58
+ get highlightedIndex(): number;
59
+ /** The select with keyboard focus, if any. */
60
+ get focusedSelect(): string | null;
61
+ /** Close an open list without choosing (the control layer's outside-press hook). */
62
+ dismiss(): void;
63
+ /** True while a list is open: the overlay then wants every event first. */
64
+ isModal(): boolean;
65
+ /** Open a select's list with the current option highlighted (closing any other). */
66
+ open(id: string): void;
67
+ /** Close the open list, if any, without choosing. */
68
+ close(): void;
69
+ /** Give a select keyboard focus, or null to clear. Disabled selects refuse. */
70
+ focus(id: string | null): void;
71
+ hitTest(x: number, y: number): boolean;
72
+ pointerDown(x: number, y: number): boolean;
73
+ pointerMove(x: number, y: number): boolean;
74
+ pointerUp(x: number, y: number): boolean;
75
+ pointerCancel(): void;
76
+ /**
77
+ * The area popups must stay inside, in canvas pixels. `drawOverlay`
78
+ * refreshes it from every frame; call it yourself when driving the overlay
79
+ * without one. A list taller than the bounds scrolls.
80
+ */
81
+ setBounds(bounds: MenuBounds | null): void;
82
+ /** An open popup scrolls with the wheel; a closed select ignores it. */
83
+ wheel(x: number, y: number, deltaX: number, deltaY: number): boolean;
84
+ /**
85
+ * Open: the list handles the key (arrows move, Enter or Space choose, Escape
86
+ * closes). Closed and focused: ArrowUp/Down step the value (Shift or
87
+ * PageUp/PageDown by ten), Home and End jump to the ends, Enter, Space or
88
+ * Alt+ArrowDown open the list on the current option.
89
+ */
90
+ keyDown(e: UIKitKeyEvent): boolean;
91
+ blur(): void;
92
+ bindLayer(layer: UIKitRedrawSource): void;
93
+ drawOverlay(frame: UIKitOverlayFrame): void;
94
+ /** Draw the open list; a control layer calls this after every widget. */
95
+ drawPopup(frame: UIKitOverlayFrame): void;
96
+ /** Release GPU resources on both backends. */
97
+ destroy(): void;
98
+ /** Apply a user choice: update the value, the button label, and fire `onChange`. */
99
+ private choose;
100
+ /** Push an entry's current spec and value into the menu model. */
101
+ private sync;
102
+ }