@rlabs-inc/tui 0.1.0 → 0.2.1

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 (39) hide show
  1. package/README.md +126 -13
  2. package/index.ts +11 -5
  3. package/package.json +2 -2
  4. package/src/api/history.ts +451 -0
  5. package/src/api/mount.ts +66 -31
  6. package/src/engine/arrays/core.ts +13 -21
  7. package/src/engine/arrays/dimensions.ts +22 -32
  8. package/src/engine/arrays/index.ts +88 -86
  9. package/src/engine/arrays/interaction.ts +34 -48
  10. package/src/engine/arrays/layout.ts +67 -92
  11. package/src/engine/arrays/spacing.ts +37 -52
  12. package/src/engine/arrays/text.ts +23 -31
  13. package/src/engine/arrays/visual.ts +56 -75
  14. package/src/engine/inheritance.ts +18 -18
  15. package/src/engine/registry.ts +15 -0
  16. package/src/pipeline/frameBuffer.ts +26 -26
  17. package/src/pipeline/layout/index.ts +2 -2
  18. package/src/pipeline/layout/titan-engine.ts +112 -84
  19. package/src/primitives/animation.ts +194 -0
  20. package/src/primitives/box.ts +74 -86
  21. package/src/primitives/each.ts +87 -0
  22. package/src/primitives/index.ts +7 -0
  23. package/src/primitives/scope.ts +215 -0
  24. package/src/primitives/show.ts +77 -0
  25. package/src/primitives/text.ts +63 -59
  26. package/src/primitives/types.ts +1 -1
  27. package/src/primitives/when.ts +102 -0
  28. package/src/renderer/append-region.ts +159 -0
  29. package/src/renderer/index.ts +4 -2
  30. package/src/renderer/output.ts +11 -34
  31. package/src/state/focus.ts +16 -5
  32. package/src/state/global-keys.ts +184 -0
  33. package/src/state/index.ts +44 -8
  34. package/src/state/input.ts +534 -0
  35. package/src/state/keyboard.ts +98 -674
  36. package/src/state/mouse.ts +163 -340
  37. package/src/state/scroll.ts +7 -9
  38. package/src/types/index.ts +23 -2
  39. package/src/renderer/input.ts +0 -518
@@ -1,25 +1,25 @@
1
1
  /**
2
- * TUI Framework - Mouse State Module
2
+ * TUI Framework - Mouse Module
3
3
  *
4
- * Robust mouse handling based on terminalKit/OpenTUI patterns:
5
- * - SGR mouse protocol (extended coordinates > 223)
6
- * - Basic X10 mouse protocol fallback
7
- * - HitGrid for O(1) coordinate-to-component lookup
8
- * - Event dispatching with hover/press tracking
4
+ * HitGrid for coordinate-to-component lookup.
5
+ * State and handler registry for mouse events.
6
+ * Does NOT own stdin (that's input.ts).
7
+ * Does NOT handle global shortcuts (that's global-keys.ts).
9
8
  *
10
9
  * API:
11
- * mouse.enable() - Enable mouse tracking
12
- * mouse.disable() - Disable mouse tracking
13
- * mouse.onMouseDown(fn) - Global mouse down handler
14
- * mouse.onMouseUp(fn) - Global mouse up handler
15
- * mouse.onClick(fn) - Global click handler
16
- * mouse.onScroll(fn) - Global scroll handler
17
- * mouse.onComponent(idx, handlers) - Per-component handlers
10
+ * lastEvent - Reactive last mouse event
11
+ * x, y - Reactive cursor position
12
+ * isDown - Reactive mouse button state
13
+ * hitGrid - O(1) coordinate lookup
14
+ * onMouseDown(fn) - Subscribe to mouse down
15
+ * onMouseUp(fn) - Subscribe to mouse up
16
+ * onClick(fn) - Subscribe to clicks
17
+ * onScroll(fn) - Subscribe to scroll
18
+ * onComponent(i,h) - Per-component handlers
18
19
  */
19
20
 
20
21
  import { signal } from '@rlabs-inc/signals'
21
22
  import * as interaction from '../engine/arrays/interaction'
22
- import { unwrap } from '@rlabs-inc/signals'
23
23
 
24
24
  // =============================================================================
25
25
  // TYPES
@@ -40,21 +40,14 @@ export interface ScrollInfo {
40
40
  }
41
41
 
42
42
  export interface MouseEvent {
43
- /** Event type */
44
43
  action: MouseAction
45
- /** Button number (0=left, 1=middle, 2=right) */
46
- button: MouseButton
47
- /** X coordinate (0-based) */
44
+ button: MouseButton | number
48
45
  x: number
49
- /** Y coordinate (0-based) */
50
46
  y: number
51
- /** Modifier keys */
52
47
  shiftKey: boolean
53
48
  altKey: boolean
54
49
  ctrlKey: boolean
55
- /** Scroll info (if action is 'scroll') */
56
50
  scroll?: ScrollInfo
57
- /** Component index at (x, y) from HitGrid */
58
51
  componentIndex: number
59
52
  }
60
53
 
@@ -87,19 +80,16 @@ export class HitGrid {
87
80
  get width(): number { return this._width }
88
81
  get height(): number { return this._height }
89
82
 
90
- /** Get component index at (x, y), or -1 if none */
91
83
  get(x: number, y: number): number {
92
84
  if (x < 0 || x >= this._width || y < 0 || y >= this._height) return -1
93
85
  return this.grid[y * this._width + x]!
94
86
  }
95
87
 
96
- /** Set component index at (x, y) */
97
88
  set(x: number, y: number, componentIndex: number): void {
98
89
  if (x < 0 || x >= this._width || y < 0 || y >= this._height) return
99
90
  this.grid[y * this._width + x] = componentIndex
100
91
  }
101
92
 
102
- /** Fill a rectangle with component index */
103
93
  fillRect(x: number, y: number, width: number, height: number, componentIndex: number): void {
104
94
  const x1 = Math.max(0, x)
105
95
  const y1 = Math.max(0, y)
@@ -113,12 +103,10 @@ export class HitGrid {
113
103
  }
114
104
  }
115
105
 
116
- /** Clear entire grid */
117
106
  clear(): void {
118
107
  this.grid.fill(-1)
119
108
  }
120
109
 
121
- /** Resize grid (clears all data) */
122
110
  resize(width: number, height: number): void {
123
111
  this._width = width
124
112
  this._height = height
@@ -127,398 +115,233 @@ export class HitGrid {
127
115
  }
128
116
 
129
117
  // =============================================================================
130
- // MOUSE PARSER (from OpenTUI)
118
+ // STATE
131
119
  // =============================================================================
132
120
 
133
- const SCROLL_DIRECTIONS: Record<number, 'up' | 'down' | 'left' | 'right'> = {
134
- 0: 'up',
135
- 1: 'down',
136
- 2: 'left',
137
- 3: 'right',
138
- }
139
-
140
- function parseMouseEvent(data: Buffer, hitGrid: HitGrid): MouseEvent | null {
141
- const str = data.toString()
142
-
143
- // Parse SGR mouse mode: \x1b[<b;x;yM or \x1b[<b;x;ym
144
- const sgrMatch = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/)
145
- if (sgrMatch) {
146
- const [, buttonCode, xStr, yStr, pressRelease] = sgrMatch
147
- const rawButtonCode = parseInt(buttonCode!)
148
- const x = parseInt(xStr!) - 1 // Convert to 0-based
149
- const y = parseInt(yStr!) - 1
150
-
151
- const button = (rawButtonCode & 3) as MouseButton
152
- const isScroll = (rawButtonCode & 64) !== 0
153
- const isMotion = (rawButtonCode & 32) !== 0
154
-
155
- const modifiers = {
156
- shiftKey: (rawButtonCode & 4) !== 0,
157
- altKey: (rawButtonCode & 8) !== 0,
158
- ctrlKey: (rawButtonCode & 16) !== 0,
159
- }
160
-
161
- let action: MouseAction
162
- let scrollInfo: ScrollInfo | undefined
163
-
164
- if (isScroll && pressRelease === 'M') {
165
- action = 'scroll'
166
- scrollInfo = {
167
- direction: SCROLL_DIRECTIONS[button] ?? 'up',
168
- delta: 1,
169
- }
170
- } else if (isMotion) {
171
- action = button === MouseButton.NONE ? 'move' : 'drag'
172
- } else {
173
- action = pressRelease === 'M' ? 'down' : 'up'
174
- }
175
-
176
- return {
177
- action,
178
- button: button === MouseButton.NONE ? MouseButton.LEFT : button,
179
- x,
180
- y,
181
- ...modifiers,
182
- scroll: scrollInfo,
183
- componentIndex: hitGrid.get(x, y),
184
- }
185
- }
186
-
187
- // Parse basic X10 mouse mode: \x1b[M followed by 3 bytes
188
- if (str.startsWith('\x1b[M') && str.length >= 6) {
189
- const buttonByte = str.charCodeAt(3) - 32
190
- const x = str.charCodeAt(4) - 33 // Convert to 0-based
191
- const y = str.charCodeAt(5) - 33
121
+ export const hitGrid = new HitGrid(80, 24)
192
122
 
193
- const button = (buttonByte & 3) as MouseButton
194
- const isScroll = (buttonByte & 64) !== 0
123
+ /** Last mouse event (reactive signal) */
124
+ export const lastMouseEvent = signal<MouseEvent | null>(null)
195
125
 
196
- const modifiers = {
197
- shiftKey: (buttonByte & 4) !== 0,
198
- altKey: (buttonByte & 8) !== 0,
199
- ctrlKey: (buttonByte & 16) !== 0,
200
- }
126
+ /** Mouse X position (reactive signal) */
127
+ export const mouseX = signal(0)
201
128
 
202
- let action: MouseAction
203
- let scrollInfo: ScrollInfo | undefined
129
+ /** Mouse Y position (reactive signal) */
130
+ export const mouseY = signal(0)
204
131
 
205
- if (isScroll) {
206
- action = 'scroll'
207
- scrollInfo = {
208
- direction: SCROLL_DIRECTIONS[button] ?? 'up',
209
- delta: 1,
210
- }
211
- } else {
212
- action = button === MouseButton.NONE ? 'up' : 'down'
213
- }
132
+ /** Is mouse button down (reactive signal) */
133
+ export const isMouseDown = signal(false)
214
134
 
215
- return {
216
- action,
217
- button: button === MouseButton.NONE ? MouseButton.LEFT : button,
218
- x,
219
- y,
220
- ...modifiers,
221
- scroll: scrollInfo,
222
- componentIndex: hitGrid.get(x, y),
223
- }
224
- }
135
+ // =============================================================================
136
+ // HANDLER REGISTRY
137
+ // =============================================================================
225
138
 
226
- return null
139
+ const componentHandlers = new Map<number, MouseHandlers>()
140
+ const globalHandlers = {
141
+ onMouseDown: new Set<MouseHandler>(),
142
+ onMouseUp: new Set<MouseHandler>(),
143
+ onClick: new Set<MouseHandler>(),
144
+ onScroll: new Set<MouseHandler>(),
227
145
  }
228
146
 
147
+ // Tracking state for hover and click detection
148
+ let hoveredComponent = -1
149
+ let pressedComponent = -1
150
+ let pressedButton = MouseButton.NONE
151
+
229
152
  // =============================================================================
230
- // MOUSE EVENT DISPATCHER
153
+ // EVENT DISPATCH (called by global-keys.ts)
231
154
  // =============================================================================
232
155
 
233
- class MouseEventDispatcher {
234
- private hitGrid: HitGrid
235
- private handlers = new Map<number, MouseHandlers>()
236
- private globalHandlers: {
237
- onMouseDown: Set<MouseHandler>
238
- onMouseUp: Set<MouseHandler>
239
- onClick: Set<MouseHandler>
240
- onScroll: Set<MouseHandler>
241
- } = {
242
- onMouseDown: new Set(),
243
- onMouseUp: new Set(),
244
- onClick: new Set(),
245
- onScroll: new Set(),
246
- }
247
-
248
- // Track state for hover and click detection
249
- private hoveredComponent = -1
250
- private pressedComponent = -1
251
- private pressedButton = MouseButton.NONE
252
-
253
- constructor(hitGrid: HitGrid) {
254
- this.hitGrid = hitGrid
255
- }
156
+ /**
157
+ * Dispatch a mouse event to all registered handlers.
158
+ * Updates reactive state and handles hover/click tracking.
159
+ */
160
+ export function dispatch(event: MouseEvent): boolean {
161
+ // Fill componentIndex from HitGrid
162
+ event.componentIndex = hitGrid.get(event.x, event.y)
256
163
 
257
- /** Register handlers for a component */
258
- register(index: number, handlers: MouseHandlers): () => void {
259
- this.handlers.set(index, handlers)
260
- return () => this.handlers.delete(index)
261
- }
164
+ // Update reactive state
165
+ lastMouseEvent.value = event
166
+ mouseX.value = event.x
167
+ mouseY.value = event.y
168
+ isMouseDown.value = event.action === 'down' || (event.action !== 'up' && isMouseDown.value)
262
169
 
263
- /** Add global handler */
264
- onMouseDown(handler: MouseHandler): () => void {
265
- this.globalHandlers.onMouseDown.add(handler)
266
- return () => this.globalHandlers.onMouseDown.delete(handler)
267
- }
170
+ const componentIndex = event.componentIndex
171
+ const handlers = componentIndex >= 0 ? componentHandlers.get(componentIndex) : undefined
172
+
173
+ // Handle hover (enter/leave)
174
+ if (componentIndex !== hoveredComponent) {
175
+ // Leave previous
176
+ if (hoveredComponent >= 0) {
177
+ const prevHandlers = componentHandlers.get(hoveredComponent)
178
+ prevHandlers?.onMouseLeave?.({ ...event, componentIndex: hoveredComponent })
179
+ if (interaction.hovered[hoveredComponent]) {
180
+ interaction.hovered.setValue(hoveredComponent, 0)
181
+ }
182
+ }
268
183
 
269
- onMouseUp(handler: MouseHandler): () => void {
270
- this.globalHandlers.onMouseUp.add(handler)
271
- return () => this.globalHandlers.onMouseUp.delete(handler)
272
- }
184
+ // Enter new
185
+ if (componentIndex >= 0) {
186
+ handlers?.onMouseEnter?.(event)
187
+ if (interaction.hovered[componentIndex]) {
188
+ interaction.hovered.setValue(componentIndex, 1)
189
+ }
190
+ }
273
191
 
274
- onClick(handler: MouseHandler): () => void {
275
- this.globalHandlers.onClick.add(handler)
276
- return () => this.globalHandlers.onClick.delete(handler)
192
+ hoveredComponent = componentIndex
277
193
  }
278
194
 
279
- onScroll(handler: MouseHandler): () => void {
280
- this.globalHandlers.onScroll.add(handler)
281
- return () => this.globalHandlers.onScroll.delete(handler)
195
+ // Handle scroll
196
+ if (event.action === 'scroll') {
197
+ if (handlers?.onScroll?.(event) === true) return true
198
+ for (const handler of globalHandlers.onScroll) {
199
+ if (handler(event) === true) return true
200
+ }
201
+ return false
282
202
  }
283
203
 
284
- /** Dispatch a mouse event */
285
- dispatch(event: MouseEvent): boolean {
286
- const componentIndex = event.componentIndex
287
- const handlers = componentIndex >= 0 ? this.handlers.get(componentIndex) : undefined
288
-
289
- // Handle hover (enter/leave)
290
- if (componentIndex !== this.hoveredComponent) {
291
- // Leave previous
292
- if (this.hoveredComponent >= 0) {
293
- const prevHandlers = this.handlers.get(this.hoveredComponent)
294
- if (prevHandlers?.onMouseLeave) {
295
- prevHandlers.onMouseLeave({ ...event, componentIndex: this.hoveredComponent })
296
- }
297
- // Update hovered array
298
- if (interaction.hovered[this.hoveredComponent]) {
299
- interaction.hovered[this.hoveredComponent]!.value = 0
300
- }
301
- }
204
+ // Handle down
205
+ if (event.action === 'down') {
206
+ pressedComponent = componentIndex
207
+ pressedButton = event.button as MouseButton
302
208
 
303
- // Enter new
304
- if (componentIndex >= 0) {
305
- if (handlers?.onMouseEnter) {
306
- handlers.onMouseEnter(event)
307
- }
308
- // Update hovered array
309
- if (interaction.hovered[componentIndex]) {
310
- interaction.hovered[componentIndex]!.value = 1
311
- }
312
- }
313
-
314
- this.hoveredComponent = componentIndex
209
+ if (componentIndex >= 0 && interaction.pressed[componentIndex]) {
210
+ interaction.pressed.setValue(componentIndex, 1)
315
211
  }
316
212
 
317
- // Handle scroll
318
- if (event.action === 'scroll') {
319
- // First try component handler
320
- if (handlers?.onScroll && handlers.onScroll(event) === true) {
321
- return true
322
- }
323
- // Then global handlers
324
- for (const handler of this.globalHandlers.onScroll) {
325
- if (handler(event) === true) return true
326
- }
327
- return false
213
+ if (handlers?.onMouseDown?.(event) === true) return true
214
+ for (const handler of globalHandlers.onMouseDown) {
215
+ if (handler(event) === true) return true
328
216
  }
217
+ }
329
218
 
330
- // Handle down
331
- if (event.action === 'down') {
332
- this.pressedComponent = componentIndex
333
- this.pressedButton = event.button
334
-
335
- // Update pressed array
336
- if (componentIndex >= 0 && interaction.pressed[componentIndex]) {
337
- interaction.pressed[componentIndex]!.value = 1
338
- }
339
-
340
- if (handlers?.onMouseDown && handlers.onMouseDown(event) === true) {
341
- return true
342
- }
343
- for (const handler of this.globalHandlers.onMouseDown) {
344
- if (handler(event) === true) return true
345
- }
219
+ // Handle up
220
+ if (event.action === 'up') {
221
+ if (pressedComponent >= 0 && interaction.pressed[pressedComponent]) {
222
+ interaction.pressed.setValue(pressedComponent, 0)
346
223
  }
347
224
 
348
- // Handle up
349
- if (event.action === 'up') {
350
- // Clear pressed state
351
- if (this.pressedComponent >= 0 && interaction.pressed[this.pressedComponent]) {
352
- interaction.pressed[this.pressedComponent]!.value = 0
353
- }
225
+ if (handlers?.onMouseUp?.(event) === true) return true
226
+ for (const handler of globalHandlers.onMouseUp) {
227
+ if (handler(event) === true) return true
228
+ }
354
229
 
355
- if (handlers?.onMouseUp && handlers.onMouseUp(event) === true) {
356
- return true
357
- }
358
- for (const handler of this.globalHandlers.onMouseUp) {
230
+ // Detect click (press and release on same component)
231
+ if (pressedComponent === componentIndex && pressedButton === event.button) {
232
+ if (handlers?.onClick?.(event) === true) return true
233
+ for (const handler of globalHandlers.onClick) {
359
234
  if (handler(event) === true) return true
360
235
  }
361
-
362
- // Detect click (press and release on same component)
363
- if (this.pressedComponent === componentIndex && this.pressedButton === event.button) {
364
- if (handlers?.onClick && handlers.onClick(event) === true) {
365
- return true
366
- }
367
- for (const handler of this.globalHandlers.onClick) {
368
- if (handler(event) === true) return true
369
- }
370
- }
371
-
372
- this.pressedComponent = -1
373
- this.pressedButton = MouseButton.NONE
374
236
  }
375
237
 
376
- return false
238
+ pressedComponent = -1
239
+ pressedButton = MouseButton.NONE
377
240
  }
378
- }
379
241
 
380
- // =============================================================================
381
- // SINGLETON INSTANCES
382
- // =============================================================================
383
-
384
- // Default to 80x24, will be resized on mount
385
- export const hitGrid = new HitGrid(80, 24)
386
- const dispatcher = new MouseEventDispatcher(hitGrid)
387
-
388
- // Reactive state
389
- export const lastMouseEvent = signal<MouseEvent | null>(null)
390
- export const mouseX = signal(0)
391
- export const mouseY = signal(0)
392
- export const isMouseDown = signal(false)
242
+ return false
243
+ }
393
244
 
394
245
  // =============================================================================
395
- // ENABLE/DISABLE MOUSE TRACKING
246
+ // MOUSE TRACKING (ANSI escape codes)
396
247
  // =============================================================================
397
248
 
398
- let enabled = false
399
- let inputHandler: ((data: Buffer) => void) | null = null
400
-
401
- /** ANSI escape codes for mouse protocols */
402
249
  const ENABLE_MOUSE = '\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h'
403
250
  const DISABLE_MOUSE = '\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l'
404
251
 
405
- /** Enable mouse tracking */
406
- export function enable(): void {
407
- if (enabled) return
408
- enabled = true
252
+ let trackingEnabled = false
409
253
 
254
+ export function enableTracking(): void {
255
+ if (trackingEnabled) return
256
+ trackingEnabled = true
410
257
  process.stdout.write(ENABLE_MOUSE)
411
-
412
- inputHandler = (data: Buffer) => {
413
- const event = parseMouseEvent(data, hitGrid)
414
- if (!event) return
415
-
416
- // Update reactive state
417
- lastMouseEvent.value = event
418
- mouseX.value = event.x
419
- mouseY.value = event.y
420
- isMouseDown.value = event.action === 'down' || (event.action !== 'up' && isMouseDown.value)
421
-
422
- // Dispatch to handlers
423
- dispatcher.dispatch(event)
424
- }
425
-
426
- // Note: This will be hooked up by the mount system
427
- // process.stdin.on('data', inputHandler)
428
258
  }
429
259
 
430
- /** Disable mouse tracking */
431
- export function disable(): void {
432
- if (!enabled) return
433
- enabled = false
434
-
260
+ export function disableTracking(): void {
261
+ if (!trackingEnabled) return
262
+ trackingEnabled = false
435
263
  process.stdout.write(DISABLE_MOUSE)
436
-
437
- if (inputHandler) {
438
- // process.stdin.removeListener('data', inputHandler)
439
- inputHandler = null
440
- }
441
264
  }
442
265
 
443
- /** Process raw input data (called by keyboard module which owns stdin) - LEGACY */
444
- export function processInput(data: Buffer): boolean {
445
- if (!enabled || !inputHandler) return false
446
-
447
- const event = parseMouseEvent(data, hitGrid)
448
- if (!event) return false
449
-
450
- return processMouseEvent(event)
266
+ export function isTrackingEnabled(): boolean {
267
+ return trackingEnabled
451
268
  }
452
269
 
453
- /** Process a parsed mouse event (called by unified keyboard input buffer) */
454
- export function processMouseEvent(event: MouseEvent): boolean {
455
- if (!enabled) return false
456
-
457
- // Fill in componentIndex from HitGrid
458
- event.componentIndex = hitGrid.get(event.x, event.y)
270
+ // =============================================================================
271
+ // PUBLIC API
272
+ // =============================================================================
459
273
 
460
- // Update reactive state
461
- lastMouseEvent.value = event
462
- mouseX.value = event.x
463
- mouseY.value = event.y
464
- isMouseDown.value = event.action === 'down' || (event.action !== 'up' && isMouseDown.value)
274
+ export function onMouseDown(handler: MouseHandler): () => void {
275
+ globalHandlers.onMouseDown.add(handler)
276
+ return () => globalHandlers.onMouseDown.delete(handler)
277
+ }
465
278
 
466
- // Dispatch to handlers
467
- dispatcher.dispatch(event)
468
- return true
279
+ export function onMouseUp(handler: MouseHandler): () => void {
280
+ globalHandlers.onMouseUp.add(handler)
281
+ return () => globalHandlers.onMouseUp.delete(handler)
469
282
  }
470
283
 
471
- /** Check if data is a mouse sequence - LEGACY, no longer needed with unified parsing */
472
- export function isMouseSequence(data: Buffer): boolean {
473
- const str = data.toString()
474
- return str.startsWith('\x1b[<') || str.startsWith('\x1b[M')
284
+ export function onClick(handler: MouseHandler): () => void {
285
+ globalHandlers.onClick.add(handler)
286
+ return () => globalHandlers.onClick.delete(handler)
475
287
  }
476
288
 
477
- // =============================================================================
478
- // PUBLIC API
479
- // =============================================================================
289
+ export function onScroll(handler: MouseHandler): () => void {
290
+ globalHandlers.onScroll.add(handler)
291
+ return () => globalHandlers.onScroll.delete(handler)
292
+ }
480
293
 
481
- /** Register handlers for a component */
482
294
  export function onComponent(index: number, handlers: MouseHandlers): () => void {
483
- return dispatcher.register(index, handlers)
295
+ componentHandlers.set(index, handlers)
296
+ return () => componentHandlers.delete(index)
484
297
  }
485
298
 
486
- /** Resize the hit grid (call on terminal resize) */
487
299
  export function resize(width: number, height: number): void {
488
300
  hitGrid.resize(width, height)
489
301
  }
490
302
 
491
- /** Clear the hit grid (call before each render) */
492
303
  export function clearHitGrid(): void {
493
304
  hitGrid.clear()
494
305
  }
495
306
 
307
+ export function cleanup(): void {
308
+ disableTracking()
309
+ componentHandlers.clear()
310
+ globalHandlers.onMouseDown.clear()
311
+ globalHandlers.onMouseUp.clear()
312
+ globalHandlers.onClick.clear()
313
+ globalHandlers.onScroll.clear()
314
+ hoveredComponent = -1
315
+ pressedComponent = -1
316
+ pressedButton = MouseButton.NONE
317
+ lastMouseEvent.value = null
318
+ mouseX.value = 0
319
+ mouseY.value = 0
320
+ isMouseDown.value = false
321
+ }
322
+
496
323
  // =============================================================================
497
- // MOUSE OBJECT (convenient namespace)
324
+ // MOUSE OBJECT - Functions only, no state getters
498
325
  // =============================================================================
499
326
 
500
327
  export const mouse = {
501
- // State
502
- get lastEvent() { return lastMouseEvent.value },
503
- get x() { return mouseX.value },
504
- get y() { return mouseY.value },
505
- get isDown() { return isMouseDown.value },
506
-
507
328
  // HitGrid
508
329
  hitGrid,
509
330
  clearHitGrid,
510
331
  resize,
511
332
 
512
- // Enable/disable
513
- enable,
514
- disable,
515
- processInput,
516
- isMouseSequence,
333
+ // Tracking
334
+ enableTracking,
335
+ disableTracking,
336
+ isTrackingEnabled,
517
337
 
518
338
  // Handlers
519
- onMouseDown: (handler: MouseHandler) => dispatcher.onMouseDown(handler),
520
- onMouseUp: (handler: MouseHandler) => dispatcher.onMouseUp(handler),
521
- onClick: (handler: MouseHandler) => dispatcher.onClick(handler),
522
- onScroll: (handler: MouseHandler) => dispatcher.onScroll(handler),
339
+ onMouseDown,
340
+ onMouseUp,
341
+ onClick,
342
+ onScroll,
523
343
  onComponent,
344
+
345
+ // Cleanup
346
+ cleanup,
524
347
  }