@rlabs-inc/tui 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +14 -2
- package/package.json +1 -1
- package/src/api/mount.ts +17 -7
- package/src/pipeline/frameBuffer.ts +2 -2
- package/src/pipeline/layout/titan-engine.ts +8 -7
- package/src/pipeline/layout/utils/text-measure.ts +3 -2
- package/src/primitives/box.ts +1 -22
- package/src/primitives/scope.ts +1 -2
- package/src/primitives/show.ts +3 -2
- package/src/primitives/text.ts +1 -24
- package/src/primitives/types.ts +0 -35
- package/src/primitives/utils.ts +26 -0
- package/src/state/drawnCursor.ts +5 -0
package/index.ts
CHANGED
|
@@ -21,8 +21,20 @@ export type { Context } from './src/state/context'
|
|
|
21
21
|
|
|
22
22
|
// State modules - Input handling
|
|
23
23
|
export { keyboard, lastKey, lastEvent } from './src/state/keyboard'
|
|
24
|
-
export {
|
|
25
|
-
|
|
24
|
+
export {
|
|
25
|
+
mouse,
|
|
26
|
+
hitGrid,
|
|
27
|
+
lastMouseEvent,
|
|
28
|
+
mouseX,
|
|
29
|
+
mouseY,
|
|
30
|
+
isMouseDown,
|
|
31
|
+
onMouseDown,
|
|
32
|
+
onMouseUp,
|
|
33
|
+
onClick,
|
|
34
|
+
onScroll,
|
|
35
|
+
onComponent,
|
|
36
|
+
} from './src/state/mouse'
|
|
37
|
+
export { focusManager, focusedIndex, pushFocusTrap, popFocusTrap, isFocusTrapped, getFocusTrapContainer } from './src/state/focus'
|
|
26
38
|
export { scroll } from './src/state/scroll'
|
|
27
39
|
export { globalKeys } from './src/state/global-keys'
|
|
28
40
|
export { cursor } from './src/state/cursor'
|
package/package.json
CHANGED
package/src/api/mount.ts
CHANGED
|
@@ -37,6 +37,13 @@ import { resetRegistry } from '../engine/registry'
|
|
|
37
37
|
import { hitGrid, clearHitGrid, mouse } from '../state/mouse'
|
|
38
38
|
import { globalKeys } from '../state/global-keys'
|
|
39
39
|
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// MODULE STATE
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
// Track if global error handlers have been registered (only once per process)
|
|
45
|
+
let globalErrorHandlersRegistered = false
|
|
46
|
+
|
|
40
47
|
// =============================================================================
|
|
41
48
|
// MOUNT
|
|
42
49
|
// =============================================================================
|
|
@@ -149,13 +156,16 @@ export async function mount(
|
|
|
149
156
|
// Create the component tree
|
|
150
157
|
root()
|
|
151
158
|
|
|
152
|
-
// Global error handlers for debugging
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
// Global error handlers for debugging (register only once per process)
|
|
160
|
+
if (!globalErrorHandlersRegistered) {
|
|
161
|
+
globalErrorHandlersRegistered = true
|
|
162
|
+
process.on('uncaughtException', (err) => {
|
|
163
|
+
console.error('[TUI] Uncaught exception:', err)
|
|
164
|
+
})
|
|
165
|
+
process.on('unhandledRejection', (err) => {
|
|
166
|
+
console.error('[TUI] Unhandled rejection:', err)
|
|
167
|
+
})
|
|
168
|
+
}
|
|
159
169
|
|
|
160
170
|
// THE ONE RENDER EFFECT
|
|
161
171
|
// This is where the magic happens - reactive rendering!
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
getInheritedBg,
|
|
40
40
|
getBorderColors,
|
|
41
41
|
getBorderStyles,
|
|
42
|
-
hasBorder,
|
|
43
42
|
getEffectiveOpacity,
|
|
44
43
|
} from '../engine/inheritance'
|
|
45
44
|
|
|
@@ -222,7 +221,8 @@ function renderComponent(
|
|
|
222
221
|
// Get border configuration
|
|
223
222
|
const borderStyles = getBorderStyles(index)
|
|
224
223
|
const borderColors = getBorderColors(index)
|
|
225
|
-
|
|
224
|
+
// Inline hasAnyBorder check - avoids redundant getBorderStyles call (was 12 array reads, now 8)
|
|
225
|
+
const hasAnyBorder = borderStyles.top > 0 || borderStyles.right > 0 || borderStyles.bottom > 0 || borderStyles.left > 0
|
|
226
226
|
|
|
227
227
|
// Draw borders
|
|
228
228
|
if (hasAnyBorder && w >= 2 && h >= 2) {
|
|
@@ -217,7 +217,8 @@ export function computeLayoutTitan(
|
|
|
217
217
|
if (firstChild[parent] === -1) {
|
|
218
218
|
firstChild[parent] = i
|
|
219
219
|
} else {
|
|
220
|
-
|
|
220
|
+
const last = lastChild[parent] ?? -1
|
|
221
|
+
if (last !== -1) nextSibling[last] = i
|
|
221
222
|
}
|
|
222
223
|
lastChild[parent] = i
|
|
223
224
|
} else {
|
|
@@ -231,10 +232,10 @@ export function computeLayoutTitan(
|
|
|
231
232
|
let head = 0
|
|
232
233
|
while (head < bfsQueue.length) {
|
|
233
234
|
const parent = bfsQueue[head++]!
|
|
234
|
-
let child = firstChild[parent]
|
|
235
|
+
let child = firstChild[parent] ?? -1
|
|
235
236
|
while (child !== -1) {
|
|
236
237
|
bfsQueue.push(child)
|
|
237
|
-
child = nextSibling[child]
|
|
238
|
+
child = nextSibling[child] ?? -1
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
241
|
|
|
@@ -319,7 +320,7 @@ export function computeLayoutTitan(
|
|
|
319
320
|
const overflow = layout.overflow[i] ?? Overflow.VISIBLE
|
|
320
321
|
const isScrollable = overflow === Overflow.SCROLL || overflow === Overflow.AUTO
|
|
321
322
|
|
|
322
|
-
let kid = firstChild[i]
|
|
323
|
+
let kid = firstChild[i] ?? -1
|
|
323
324
|
if (kid !== -1 && !isScrollable) {
|
|
324
325
|
// Normal containers: intrinsic size includes all children
|
|
325
326
|
const dir = layout.flexDirection[i] ?? FLEX_COLUMN
|
|
@@ -368,7 +369,7 @@ export function computeLayoutTitan(
|
|
|
368
369
|
sumMain += kidH + kidMarginMain + gap
|
|
369
370
|
maxCross = Math.max(maxCross, kidW)
|
|
370
371
|
}
|
|
371
|
-
kid = nextSibling[kid]
|
|
372
|
+
kid = nextSibling[kid] ?? -1
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
if (childCount > 0) sumMain -= gap
|
|
@@ -429,12 +430,12 @@ export function computeLayoutTitan(
|
|
|
429
430
|
let childrenMaxCross = 0
|
|
430
431
|
|
|
431
432
|
// Collect flow children
|
|
432
|
-
let kid = firstChild[parent]
|
|
433
|
+
let kid = firstChild[parent] ?? -1
|
|
433
434
|
while (kid !== -1) {
|
|
434
435
|
if ((layout.position[kid] ?? POS_RELATIVE) !== POS_ABSOLUTE) {
|
|
435
436
|
flowKids.push(kid)
|
|
436
437
|
}
|
|
437
|
-
kid = nextSibling[kid]
|
|
438
|
+
kid = nextSibling[kid] ?? -1
|
|
438
439
|
}
|
|
439
440
|
|
|
440
441
|
if (flowKids.length === 0) return
|
|
@@ -33,7 +33,8 @@ export function measureTextHeight(content: string, maxWidth: number): number {
|
|
|
33
33
|
// Split by existing newlines first
|
|
34
34
|
const paragraphs = content.split('\n')
|
|
35
35
|
|
|
36
|
-
for (
|
|
36
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
37
|
+
const paragraph = paragraphs[i]!
|
|
37
38
|
if (paragraph === '') {
|
|
38
39
|
lines++
|
|
39
40
|
continue
|
|
@@ -53,7 +54,7 @@ export function measureTextHeight(content: string, maxWidth: number): number {
|
|
|
53
54
|
|
|
54
55
|
// Reset for next paragraph
|
|
55
56
|
currentLineWidth = 0
|
|
56
|
-
if (
|
|
57
|
+
if (i < paragraphs.length - 1) {
|
|
57
58
|
lines++
|
|
58
59
|
}
|
|
59
60
|
}
|
package/src/primitives/box.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { cleanupIndex as cleanupKeyboardListeners } from '../state/keyboard'
|
|
39
39
|
import { getVariantStyle } from '../state/theme'
|
|
40
40
|
import { getActiveScope } from './scope'
|
|
41
|
+
import { enumSource } from './utils'
|
|
41
42
|
|
|
42
43
|
// Import arrays
|
|
43
44
|
import * as core from '../engine/arrays/core'
|
|
@@ -118,28 +119,6 @@ function alignSelfToNum(align: string | undefined): number {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
/**
|
|
122
|
-
* Create a slot source for enum props - returns getter for reactive, value for static.
|
|
123
|
-
* For use with slotArray.setSource()
|
|
124
|
-
* Handles: static values, signals/bindings ({ value: T }), and getter functions (() => T)
|
|
125
|
-
*/
|
|
126
|
-
function enumSource<T extends string>(
|
|
127
|
-
prop: T | { value: T } | (() => T) | undefined,
|
|
128
|
-
converter: (val: T | undefined) => number
|
|
129
|
-
): number | (() => number) {
|
|
130
|
-
// Handle getter function (inline derived)
|
|
131
|
-
if (typeof prop === 'function') {
|
|
132
|
-
return () => converter(prop())
|
|
133
|
-
}
|
|
134
|
-
// Handle object with .value (signal/binding/derived)
|
|
135
|
-
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
136
|
-
const reactiveSource = prop as { value: T }
|
|
137
|
-
return () => converter(reactiveSource.value)
|
|
138
|
-
}
|
|
139
|
-
// Static value
|
|
140
|
-
return converter(prop as T | undefined)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
122
|
/** Get static boolean for visible prop */
|
|
144
123
|
function getStaticBool(prop: unknown, defaultVal: boolean): boolean {
|
|
145
124
|
if (prop === undefined) return defaultVal
|
package/src/primitives/scope.ts
CHANGED
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { effectScope, onScopeDispose } from '@rlabs-inc/signals'
|
|
24
24
|
import type { EffectScope } from '@rlabs-inc/signals'
|
|
25
|
-
|
|
26
|
-
export type Cleanup = () => void
|
|
25
|
+
import type { Cleanup } from './types'
|
|
27
26
|
|
|
28
27
|
// =============================================================================
|
|
29
28
|
// ACTIVE SCOPE TRACKING
|
package/src/primitives/show.ts
CHANGED
|
@@ -58,14 +58,15 @@ export function show(
|
|
|
58
58
|
// Initial render
|
|
59
59
|
update(conditionGetter())
|
|
60
60
|
|
|
61
|
-
// Effect for updates -
|
|
61
|
+
// Effect for updates - reads condition to establish dependency
|
|
62
|
+
// but skips the update on first run since we already rendered above
|
|
62
63
|
let initialized = false
|
|
63
64
|
effect(() => {
|
|
65
|
+
const condition = conditionGetter() // Must read to track dependency!
|
|
64
66
|
if (!initialized) {
|
|
65
67
|
initialized = true
|
|
66
68
|
return
|
|
67
69
|
}
|
|
68
|
-
const condition = conditionGetter()
|
|
69
70
|
update(condition)
|
|
70
71
|
})
|
|
71
72
|
|
package/src/primitives/text.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { cleanupIndex as cleanupKeyboardListeners } from '../state/keyboard'
|
|
35
35
|
import { getVariantStyle } from '../state/theme'
|
|
36
36
|
import { getActiveScope } from './scope'
|
|
37
|
+
import { enumSource } from './utils'
|
|
37
38
|
|
|
38
39
|
// Import arrays
|
|
39
40
|
import * as core from '../engine/arrays/core'
|
|
@@ -79,8 +80,6 @@ function alignSelfToNum(alignSelf: string | undefined): number {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
// bindEnumProp removed - using enumSource instead
|
|
83
|
-
|
|
84
83
|
/**
|
|
85
84
|
* Convert content prop (string | number) to string source for setSource.
|
|
86
85
|
* Handles: static values, signals, and getters.
|
|
@@ -101,28 +100,6 @@ function contentToStringSource(
|
|
|
101
100
|
return String(content)
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
/**
|
|
105
|
-
* Create a slot source for enum props - returns getter for reactive, value for static.
|
|
106
|
-
* For use with slotArray.setSource()
|
|
107
|
-
* Handles: static values, signals/bindings ({ value: T }), and getter functions (() => T)
|
|
108
|
-
*/
|
|
109
|
-
function enumSource<T extends string>(
|
|
110
|
-
prop: T | { value: T } | (() => T) | undefined,
|
|
111
|
-
converter: (val: T | undefined) => number
|
|
112
|
-
): number | (() => number) {
|
|
113
|
-
// Handle getter function (inline derived)
|
|
114
|
-
if (typeof prop === 'function') {
|
|
115
|
-
return () => converter(prop())
|
|
116
|
-
}
|
|
117
|
-
// Handle object with .value (signal/binding/derived)
|
|
118
|
-
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
119
|
-
const reactiveSource = prop as { value: T }
|
|
120
|
-
return () => converter(reactiveSource.value)
|
|
121
|
-
}
|
|
122
|
-
// Static value
|
|
123
|
-
return converter(prop as T | undefined)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
103
|
// =============================================================================
|
|
127
104
|
// TEXT COMPONENT
|
|
128
105
|
// =============================================================================
|
package/src/primitives/types.ts
CHANGED
|
@@ -232,41 +232,6 @@ export interface InputProps extends StyleProps, BorderProps, DimensionProps, Spa
|
|
|
232
232
|
onBlur?: () => void
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// =============================================================================
|
|
236
|
-
// SELECT PROPS
|
|
237
|
-
// =============================================================================
|
|
238
|
-
|
|
239
|
-
export interface SelectOption {
|
|
240
|
-
value: string
|
|
241
|
-
label: string
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export interface SelectProps extends StyleProps, BorderProps, DimensionProps {
|
|
245
|
-
/** Selected value (two-way bound) */
|
|
246
|
-
value: WritableSignal<string> | Binding<string>
|
|
247
|
-
/** Available options */
|
|
248
|
-
options: SelectOption[]
|
|
249
|
-
/** Placeholder when nothing selected */
|
|
250
|
-
placeholder?: string
|
|
251
|
-
/** Is visible */
|
|
252
|
-
visible?: Reactive<boolean>
|
|
253
|
-
/** Called when selection changes */
|
|
254
|
-
onChange?: (value: string) => void
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// =============================================================================
|
|
258
|
-
// PROGRESS PROPS
|
|
259
|
-
// =============================================================================
|
|
260
|
-
|
|
261
|
-
export interface ProgressProps extends StyleProps, DimensionProps {
|
|
262
|
-
/** Progress value 0-1 */
|
|
263
|
-
value: Reactive<number>
|
|
264
|
-
/** Show percentage text */
|
|
265
|
-
showPercent?: boolean
|
|
266
|
-
/** Is visible */
|
|
267
|
-
visible?: Reactive<boolean>
|
|
268
|
-
}
|
|
269
|
-
|
|
270
235
|
// =============================================================================
|
|
271
236
|
// COMPONENT RETURN TYPE
|
|
272
237
|
// =============================================================================
|
package/src/primitives/utils.ts
CHANGED
|
@@ -35,3 +35,29 @@ export function getValue<T>(prop: T | { value: T } | (() => T) | undefined, defa
|
|
|
35
35
|
if (typeof prop === 'object' && prop !== null && 'value' in prop) return (prop as { value: T }).value
|
|
36
36
|
return prop as T
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// ENUM SOURCE
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a slot source for enum props - returns getter for reactive, value for static.
|
|
45
|
+
* For use with slotArray.setSource()
|
|
46
|
+
* Handles: static values, signals/bindings ({ value: T }), and getter functions (() => T)
|
|
47
|
+
*/
|
|
48
|
+
export function enumSource<T extends string>(
|
|
49
|
+
prop: T | { value: T } | (() => T) | undefined,
|
|
50
|
+
converter: (val: T | undefined) => number
|
|
51
|
+
): number | (() => number) {
|
|
52
|
+
// Handle getter function (inline derived)
|
|
53
|
+
if (typeof prop === 'function') {
|
|
54
|
+
return () => converter(prop())
|
|
55
|
+
}
|
|
56
|
+
// Handle object with .value (signal/binding/derived)
|
|
57
|
+
if (prop !== undefined && typeof prop === 'object' && prop !== null && 'value' in prop) {
|
|
58
|
+
const reactiveSource = prop as { value: T }
|
|
59
|
+
return () => converter(reactiveSource.value)
|
|
60
|
+
}
|
|
61
|
+
// Static value
|
|
62
|
+
return converter(prop as T | undefined)
|
|
63
|
+
}
|
package/src/state/drawnCursor.ts
CHANGED
|
@@ -111,6 +111,11 @@ function getBlinkClock(fps: number): BlinkRegistry {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
function subscribeToBlink(fps: number): () => void {
|
|
114
|
+
// Guard against invalid fps (0 or negative would cause Infinity interval)
|
|
115
|
+
if (fps <= 0) {
|
|
116
|
+
return () => {} // No-op unsubscribe
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
const registry = getBlinkClock(fps)
|
|
115
120
|
registry.subscribers++
|
|
116
121
|
|