@rlabs-inc/tui 0.1.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/README.md +141 -0
- package/index.ts +45 -0
- package/package.json +59 -0
- package/src/api/index.ts +7 -0
- package/src/api/mount.ts +230 -0
- package/src/engine/arrays/core.ts +60 -0
- package/src/engine/arrays/dimensions.ts +68 -0
- package/src/engine/arrays/index.ts +166 -0
- package/src/engine/arrays/interaction.ts +112 -0
- package/src/engine/arrays/layout.ts +175 -0
- package/src/engine/arrays/spacing.ts +100 -0
- package/src/engine/arrays/text.ts +55 -0
- package/src/engine/arrays/visual.ts +140 -0
- package/src/engine/index.ts +25 -0
- package/src/engine/inheritance.ts +138 -0
- package/src/engine/registry.ts +180 -0
- package/src/pipeline/frameBuffer.ts +473 -0
- package/src/pipeline/layout/index.ts +105 -0
- package/src/pipeline/layout/titan-engine.ts +798 -0
- package/src/pipeline/layout/types.ts +194 -0
- package/src/pipeline/layout/utils/hierarchy.ts +202 -0
- package/src/pipeline/layout/utils/math.ts +134 -0
- package/src/pipeline/layout/utils/text-measure.ts +160 -0
- package/src/pipeline/layout.ts +30 -0
- package/src/primitives/box.ts +312 -0
- package/src/primitives/index.ts +12 -0
- package/src/primitives/text.ts +199 -0
- package/src/primitives/types.ts +222 -0
- package/src/primitives/utils.ts +37 -0
- package/src/renderer/ansi.ts +625 -0
- package/src/renderer/buffer.ts +667 -0
- package/src/renderer/index.ts +40 -0
- package/src/renderer/input.ts +518 -0
- package/src/renderer/output.ts +451 -0
- package/src/state/cursor.ts +176 -0
- package/src/state/focus.ts +241 -0
- package/src/state/index.ts +43 -0
- package/src/state/keyboard.ts +771 -0
- package/src/state/mouse.ts +524 -0
- package/src/state/scroll.ts +341 -0
- package/src/state/theme.ts +687 -0
- package/src/types/color.ts +401 -0
- package/src/types/index.ts +316 -0
- package/src/utils/text.ts +471 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI Framework - Mouse State Module
|
|
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
|
|
9
|
+
*
|
|
10
|
+
* 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
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { signal } from '@rlabs-inc/signals'
|
|
21
|
+
import * as interaction from '../engine/arrays/interaction'
|
|
22
|
+
import { unwrap } from '@rlabs-inc/signals'
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// TYPES
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
export type MouseAction = 'down' | 'up' | 'move' | 'drag' | 'scroll'
|
|
29
|
+
|
|
30
|
+
export enum MouseButton {
|
|
31
|
+
LEFT = 0,
|
|
32
|
+
MIDDLE = 1,
|
|
33
|
+
RIGHT = 2,
|
|
34
|
+
NONE = 3,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScrollInfo {
|
|
38
|
+
direction: 'up' | 'down' | 'left' | 'right'
|
|
39
|
+
delta: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MouseEvent {
|
|
43
|
+
/** Event type */
|
|
44
|
+
action: MouseAction
|
|
45
|
+
/** Button number (0=left, 1=middle, 2=right) */
|
|
46
|
+
button: MouseButton
|
|
47
|
+
/** X coordinate (0-based) */
|
|
48
|
+
x: number
|
|
49
|
+
/** Y coordinate (0-based) */
|
|
50
|
+
y: number
|
|
51
|
+
/** Modifier keys */
|
|
52
|
+
shiftKey: boolean
|
|
53
|
+
altKey: boolean
|
|
54
|
+
ctrlKey: boolean
|
|
55
|
+
/** Scroll info (if action is 'scroll') */
|
|
56
|
+
scroll?: ScrollInfo
|
|
57
|
+
/** Component index at (x, y) from HitGrid */
|
|
58
|
+
componentIndex: number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface MouseHandlers {
|
|
62
|
+
onMouseDown?: (event: MouseEvent) => void | boolean
|
|
63
|
+
onMouseUp?: (event: MouseEvent) => void | boolean
|
|
64
|
+
onClick?: (event: MouseEvent) => void | boolean
|
|
65
|
+
onMouseEnter?: (event: MouseEvent) => void
|
|
66
|
+
onMouseLeave?: (event: MouseEvent) => void
|
|
67
|
+
onScroll?: (event: MouseEvent) => void | boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type MouseHandler = (event: MouseEvent) => void | boolean
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// HIT GRID - O(1) Coordinate to Component Lookup
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
export class HitGrid {
|
|
77
|
+
private grid: Int16Array
|
|
78
|
+
private _width: number
|
|
79
|
+
private _height: number
|
|
80
|
+
|
|
81
|
+
constructor(width: number, height: number) {
|
|
82
|
+
this._width = width
|
|
83
|
+
this._height = height
|
|
84
|
+
this.grid = new Int16Array(width * height).fill(-1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get width(): number { return this._width }
|
|
88
|
+
get height(): number { return this._height }
|
|
89
|
+
|
|
90
|
+
/** Get component index at (x, y), or -1 if none */
|
|
91
|
+
get(x: number, y: number): number {
|
|
92
|
+
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return -1
|
|
93
|
+
return this.grid[y * this._width + x]!
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Set component index at (x, y) */
|
|
97
|
+
set(x: number, y: number, componentIndex: number): void {
|
|
98
|
+
if (x < 0 || x >= this._width || y < 0 || y >= this._height) return
|
|
99
|
+
this.grid[y * this._width + x] = componentIndex
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Fill a rectangle with component index */
|
|
103
|
+
fillRect(x: number, y: number, width: number, height: number, componentIndex: number): void {
|
|
104
|
+
const x1 = Math.max(0, x)
|
|
105
|
+
const y1 = Math.max(0, y)
|
|
106
|
+
const x2 = Math.min(this._width, x + width)
|
|
107
|
+
const y2 = Math.min(this._height, y + height)
|
|
108
|
+
|
|
109
|
+
for (let py = y1; py < y2; py++) {
|
|
110
|
+
for (let px = x1; px < x2; px++) {
|
|
111
|
+
this.grid[py * this._width + px] = componentIndex
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Clear entire grid */
|
|
117
|
+
clear(): void {
|
|
118
|
+
this.grid.fill(-1)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Resize grid (clears all data) */
|
|
122
|
+
resize(width: number, height: number): void {
|
|
123
|
+
this._width = width
|
|
124
|
+
this._height = height
|
|
125
|
+
this.grid = new Int16Array(width * height).fill(-1)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// =============================================================================
|
|
130
|
+
// MOUSE PARSER (from OpenTUI)
|
|
131
|
+
// =============================================================================
|
|
132
|
+
|
|
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
|
|
192
|
+
|
|
193
|
+
const button = (buttonByte & 3) as MouseButton
|
|
194
|
+
const isScroll = (buttonByte & 64) !== 0
|
|
195
|
+
|
|
196
|
+
const modifiers = {
|
|
197
|
+
shiftKey: (buttonByte & 4) !== 0,
|
|
198
|
+
altKey: (buttonByte & 8) !== 0,
|
|
199
|
+
ctrlKey: (buttonByte & 16) !== 0,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let action: MouseAction
|
|
203
|
+
let scrollInfo: ScrollInfo | undefined
|
|
204
|
+
|
|
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
|
+
}
|
|
214
|
+
|
|
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
|
+
}
|
|
225
|
+
|
|
226
|
+
return null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// =============================================================================
|
|
230
|
+
// MOUSE EVENT DISPATCHER
|
|
231
|
+
// =============================================================================
|
|
232
|
+
|
|
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
|
+
}
|
|
256
|
+
|
|
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
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Add global handler */
|
|
264
|
+
onMouseDown(handler: MouseHandler): () => void {
|
|
265
|
+
this.globalHandlers.onMouseDown.add(handler)
|
|
266
|
+
return () => this.globalHandlers.onMouseDown.delete(handler)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
onMouseUp(handler: MouseHandler): () => void {
|
|
270
|
+
this.globalHandlers.onMouseUp.add(handler)
|
|
271
|
+
return () => this.globalHandlers.onMouseUp.delete(handler)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
onClick(handler: MouseHandler): () => void {
|
|
275
|
+
this.globalHandlers.onClick.add(handler)
|
|
276
|
+
return () => this.globalHandlers.onClick.delete(handler)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
onScroll(handler: MouseHandler): () => void {
|
|
280
|
+
this.globalHandlers.onScroll.add(handler)
|
|
281
|
+
return () => this.globalHandlers.onScroll.delete(handler)
|
|
282
|
+
}
|
|
283
|
+
|
|
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
|
+
}
|
|
302
|
+
|
|
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
|
|
315
|
+
}
|
|
316
|
+
|
|
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
|
|
328
|
+
}
|
|
329
|
+
|
|
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
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
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
|
+
}
|
|
354
|
+
|
|
355
|
+
if (handlers?.onMouseUp && handlers.onMouseUp(event) === true) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
for (const handler of this.globalHandlers.onMouseUp) {
|
|
359
|
+
if (handler(event) === true) return true
|
|
360
|
+
}
|
|
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
|
+
}
|
|
375
|
+
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
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)
|
|
393
|
+
|
|
394
|
+
// =============================================================================
|
|
395
|
+
// ENABLE/DISABLE MOUSE TRACKING
|
|
396
|
+
// =============================================================================
|
|
397
|
+
|
|
398
|
+
let enabled = false
|
|
399
|
+
let inputHandler: ((data: Buffer) => void) | null = null
|
|
400
|
+
|
|
401
|
+
/** ANSI escape codes for mouse protocols */
|
|
402
|
+
const ENABLE_MOUSE = '\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h'
|
|
403
|
+
const DISABLE_MOUSE = '\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l'
|
|
404
|
+
|
|
405
|
+
/** Enable mouse tracking */
|
|
406
|
+
export function enable(): void {
|
|
407
|
+
if (enabled) return
|
|
408
|
+
enabled = true
|
|
409
|
+
|
|
410
|
+
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
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Disable mouse tracking */
|
|
431
|
+
export function disable(): void {
|
|
432
|
+
if (!enabled) return
|
|
433
|
+
enabled = false
|
|
434
|
+
|
|
435
|
+
process.stdout.write(DISABLE_MOUSE)
|
|
436
|
+
|
|
437
|
+
if (inputHandler) {
|
|
438
|
+
// process.stdin.removeListener('data', inputHandler)
|
|
439
|
+
inputHandler = null
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
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)
|
|
451
|
+
}
|
|
452
|
+
|
|
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)
|
|
459
|
+
|
|
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)
|
|
465
|
+
|
|
466
|
+
// Dispatch to handlers
|
|
467
|
+
dispatcher.dispatch(event)
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
|
|
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')
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// =============================================================================
|
|
478
|
+
// PUBLIC API
|
|
479
|
+
// =============================================================================
|
|
480
|
+
|
|
481
|
+
/** Register handlers for a component */
|
|
482
|
+
export function onComponent(index: number, handlers: MouseHandlers): () => void {
|
|
483
|
+
return dispatcher.register(index, handlers)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Resize the hit grid (call on terminal resize) */
|
|
487
|
+
export function resize(width: number, height: number): void {
|
|
488
|
+
hitGrid.resize(width, height)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Clear the hit grid (call before each render) */
|
|
492
|
+
export function clearHitGrid(): void {
|
|
493
|
+
hitGrid.clear()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// =============================================================================
|
|
497
|
+
// MOUSE OBJECT (convenient namespace)
|
|
498
|
+
// =============================================================================
|
|
499
|
+
|
|
500
|
+
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
|
+
// HitGrid
|
|
508
|
+
hitGrid,
|
|
509
|
+
clearHitGrid,
|
|
510
|
+
resize,
|
|
511
|
+
|
|
512
|
+
// Enable/disable
|
|
513
|
+
enable,
|
|
514
|
+
disable,
|
|
515
|
+
processInput,
|
|
516
|
+
isMouseSequence,
|
|
517
|
+
|
|
518
|
+
// 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),
|
|
523
|
+
onComponent,
|
|
524
|
+
}
|