@rlabs-inc/tui 0.1.0 → 0.2.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 +126 -13
- package/index.ts +11 -5
- package/package.json +2 -2
- package/src/api/mount.ts +42 -27
- package/src/engine/arrays/core.ts +13 -21
- package/src/engine/arrays/dimensions.ts +22 -32
- package/src/engine/arrays/index.ts +88 -86
- package/src/engine/arrays/interaction.ts +34 -48
- package/src/engine/arrays/layout.ts +67 -92
- package/src/engine/arrays/spacing.ts +37 -52
- package/src/engine/arrays/text.ts +23 -31
- package/src/engine/arrays/visual.ts +56 -75
- package/src/engine/inheritance.ts +18 -18
- package/src/engine/registry.ts +15 -0
- package/src/pipeline/frameBuffer.ts +26 -26
- package/src/pipeline/layout/index.ts +2 -2
- package/src/pipeline/layout/titan-engine.ts +112 -84
- package/src/primitives/animation.ts +194 -0
- package/src/primitives/box.ts +74 -86
- package/src/primitives/each.ts +87 -0
- package/src/primitives/index.ts +7 -0
- package/src/primitives/scope.ts +215 -0
- package/src/primitives/show.ts +77 -0
- package/src/primitives/text.ts +63 -59
- package/src/primitives/types.ts +1 -1
- package/src/primitives/when.ts +102 -0
- package/src/renderer/append-region.ts +303 -0
- package/src/renderer/index.ts +4 -2
- package/src/renderer/output.ts +11 -34
- package/src/state/focus.ts +16 -5
- package/src/state/global-keys.ts +184 -0
- package/src/state/index.ts +44 -8
- package/src/state/input.ts +534 -0
- package/src/state/keyboard.ts +98 -674
- package/src/state/mouse.ts +163 -340
- package/src/state/scroll.ts +7 -9
- package/src/types/index.ts +6 -0
- package/src/renderer/input.ts +0 -518
package/src/state/mouse.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TUI Framework - Mouse
|
|
2
|
+
* TUI Framework - Mouse Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
118
|
+
// STATE
|
|
131
119
|
// =============================================================================
|
|
132
120
|
|
|
133
|
-
const
|
|
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
|
-
|
|
194
|
-
|
|
123
|
+
/** Last mouse event (reactive signal) */
|
|
124
|
+
export const lastMouseEvent = signal<MouseEvent | null>(null)
|
|
195
125
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
129
|
+
/** Mouse Y position (reactive signal) */
|
|
130
|
+
export const mouseY = signal(0)
|
|
204
131
|
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
153
|
+
// EVENT DISPATCH (called by global-keys.ts)
|
|
231
154
|
// =============================================================================
|
|
232
155
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
275
|
-
this.globalHandlers.onClick.add(handler)
|
|
276
|
-
return () => this.globalHandlers.onClick.delete(handler)
|
|
192
|
+
hoveredComponent = componentIndex
|
|
277
193
|
}
|
|
278
194
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
for (const handler of
|
|
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
|
-
|
|
238
|
+
pressedComponent = -1
|
|
239
|
+
pressedButton = MouseButton.NONE
|
|
377
240
|
}
|
|
378
|
-
}
|
|
379
241
|
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// Fill in componentIndex from HitGrid
|
|
458
|
-
event.componentIndex = hitGrid.get(event.x, event.y)
|
|
270
|
+
// =============================================================================
|
|
271
|
+
// PUBLIC API
|
|
272
|
+
// =============================================================================
|
|
459
273
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
279
|
+
export function onMouseUp(handler: MouseHandler): () => void {
|
|
280
|
+
globalHandlers.onMouseUp.add(handler)
|
|
281
|
+
return () => globalHandlers.onMouseUp.delete(handler)
|
|
469
282
|
}
|
|
470
283
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
isMouseSequence,
|
|
333
|
+
// Tracking
|
|
334
|
+
enableTracking,
|
|
335
|
+
disableTracking,
|
|
336
|
+
isTrackingEnabled,
|
|
517
337
|
|
|
518
338
|
// Handlers
|
|
519
|
-
onMouseDown
|
|
520
|
-
onMouseUp
|
|
521
|
-
onClick
|
|
522
|
-
onScroll
|
|
339
|
+
onMouseDown,
|
|
340
|
+
onMouseUp,
|
|
341
|
+
onClick,
|
|
342
|
+
onScroll,
|
|
523
343
|
onComponent,
|
|
344
|
+
|
|
345
|
+
// Cleanup
|
|
346
|
+
cleanup,
|
|
524
347
|
}
|