@trendr/core 0.4.12 → 0.4.13
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 +33 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/src/diff-view.js +5 -3
- package/src/hooks.js +12 -0
- package/src/layout.js +4 -2
- package/src/renderer.js +20 -0
package/README.md
CHANGED
|
@@ -313,7 +313,39 @@ useMouse((event) => {
|
|
|
313
313
|
})
|
|
314
314
|
```
|
|
315
315
|
|
|
316
|
-
Mouse is enabled automatically. Passive pointer movement is reported as `move
|
|
316
|
+
Mouse is enabled automatically. Passive pointer movement is reported as `move`; combine it with `useHitTest()` to derive hover state. Built-in components support click, scroll wheel, and scrollbar dragging.
|
|
317
|
+
|
|
318
|
+
### useHitTest
|
|
319
|
+
|
|
320
|
+
Mouse events carry terminal coordinates, but `useLayout()` returns logical content-space coordinates - the two disagree inside any scrolled or clipped container. `useHitTest()` returns a function that tests terminal coordinates against the component's final painted, clipped rectangle, with ancestor scroll offsets applied.
|
|
321
|
+
|
|
322
|
+
```jsx
|
|
323
|
+
function Hoverable({ children, action, onAction }) {
|
|
324
|
+
const hitTest = useHitTest()
|
|
325
|
+
const [hovered, setHovered] = createSignal(false)
|
|
326
|
+
|
|
327
|
+
useMouse((event) => {
|
|
328
|
+
if (event.action === 'move') setHovered(hitTest(event.x, event.y))
|
|
329
|
+
if (event.action === 'press' && event.button === 'left' && hovered() && hitTest(event.x, event.y)) {
|
|
330
|
+
onAction()
|
|
331
|
+
event.stopPropagation()
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<box>
|
|
337
|
+
{children}
|
|
338
|
+
{hovered() && (
|
|
339
|
+
<box style={{ position: 'absolute', top: 0, right: 1 }}>
|
|
340
|
+
<text style={{ inverse: true }}>{` ${action} `}</text>
|
|
341
|
+
</box>
|
|
342
|
+
)}
|
|
343
|
+
</box>
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The returned function stays accurate across scrolling, resizing, and relayout, and returns `false` once the component unmounts. It answers geometric containment only - focus and z-order stay with the input system. Never bounds-check mouse events against `useLayout()`; inside a scrolled viewport that either misses real hits or, worse, captures events belonging to other components.
|
|
317
349
|
|
|
318
350
|
### useStdout
|
|
319
351
|
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { mount, registerOverlay } from './src/renderer.js'
|
|
2
2
|
export { createSignal } from './src/signal.js'
|
|
3
3
|
export { createEffect, createMemo, batch, untrack, onCleanup } from './src/signal.js'
|
|
4
|
-
export { useInput, useMouse, useResize, useInterval, useTimeout, useLayout, useStdout, useRepaint, useTitle, useTheme, useCursor, useFrameStats, useAsync } from './src/hooks.js'
|
|
4
|
+
export { useInput, useMouse, useResize, useInterval, useTimeout, useLayout, useHitTest, useStdout, useRepaint, useTitle, useTheme, useCursor, useFrameStats, useAsync } from './src/hooks.js'
|
|
5
5
|
export { Box, Text, Spacer } from './src/components.js'
|
|
6
6
|
export { TextInput } from './src/text-input.js'
|
|
7
7
|
export { TextArea } from './src/text-area.js'
|
package/package.json
CHANGED
package/src/diff-view.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from '../jsx-runtime.js'
|
|
2
2
|
import { createSignal } from './signal.js'
|
|
3
|
-
import { useInput, useMouse, useLayout, useScrollDrag } from './hooks.js'
|
|
3
|
+
import { useInput, useMouse, useLayout, useHitTest, useScrollDrag } from './hooks.js'
|
|
4
4
|
import { sliceVisibleRange } from './wrap.js'
|
|
5
5
|
import { computeDiff } from './diff-engine.js'
|
|
6
6
|
|
|
@@ -141,10 +141,12 @@ export function Diff({
|
|
|
141
141
|
else if (key === 'end' || key === 'G') setOffset(maxOffset)
|
|
142
142
|
})
|
|
143
143
|
|
|
144
|
+
const hitTest = useHitTest()
|
|
144
145
|
useMouse((event) => {
|
|
145
146
|
if (event.action !== 'scroll' || rows.length <= h) return
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
// painted-geometry bounds: logical layout coords are wrong for a diff
|
|
148
|
+
// inside a scrolled container and would swallow wheel events meant for it
|
|
149
|
+
if (!hitTest(event.x, event.y)) return
|
|
148
150
|
if (event.direction !== 'up' && event.direction !== 'down') return
|
|
149
151
|
setOffset(clamp(clamped + (event.direction === 'up' ? -3 : 3)))
|
|
150
152
|
event.stopPropagation()
|
package/src/hooks.js
CHANGED
|
@@ -60,6 +60,18 @@ export function useLayout() {
|
|
|
60
60
|
return getInstanceLayout()
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// terminal-space hit testing: tests mouse coordinates against the component's
|
|
64
|
+
// final painted, clipped rectangle - unlike useLayout, which is logical
|
|
65
|
+
// content space and knows nothing of ancestor scroll offsets or clipping
|
|
66
|
+
export function useHitTest() {
|
|
67
|
+
const state = registerHook(() => ({ owner: getCurrentHookOwner() }))
|
|
68
|
+
return (x, y) => {
|
|
69
|
+
const rect = state.owner?._paintedRect
|
|
70
|
+
if (!rect) return false
|
|
71
|
+
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
export function useTheme() {
|
|
64
76
|
return getTheme()
|
|
65
77
|
}
|
package/src/layout.js
CHANGED
|
@@ -314,8 +314,10 @@ function measureIntrinsic(node, availW, availH) {
|
|
|
314
314
|
const innerW = availW - chrome.x
|
|
315
315
|
const innerH = availH - chrome.y
|
|
316
316
|
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
// absolute children are excluded from flow layout, so they must not
|
|
318
|
+
// contribute to intrinsic size either, or a hover overlay grows its parent
|
|
319
|
+
const children = (node._resolvedChildren || []).filter((c) => childStyle(c).position !== 'absolute')
|
|
320
|
+
if (children.length === 0) {
|
|
319
321
|
return { width: chrome.x, height: chrome.y }
|
|
320
322
|
}
|
|
321
323
|
|
package/src/renderer.js
CHANGED
|
@@ -319,6 +319,23 @@ function paintTree(node, buf, clip, offset, prevBuf) {
|
|
|
319
319
|
|
|
320
320
|
if (node._resolved) {
|
|
321
321
|
const inst = node._instance
|
|
322
|
+
// painted terminal-space geometry for useHitTest: logical layout plus
|
|
323
|
+
// accumulated ancestor paint offset, intersected with the active clip.
|
|
324
|
+
// recorded before the blit fast-path so clean subtrees stay hit-testable
|
|
325
|
+
if (inst) {
|
|
326
|
+
const lay = node._resolved?._layout ?? node._layout
|
|
327
|
+
if (lay && lay.width > 0 && lay.height > 0) {
|
|
328
|
+
const painted = offset
|
|
329
|
+
? { x: lay.x + offset.x, y: lay.y + offset.y, width: lay.width, height: lay.height }
|
|
330
|
+
: lay
|
|
331
|
+
const vis = clip ? clipRect(painted, clip) : painted
|
|
332
|
+
inst._paintedRect = vis.width > 0 && vis.height > 0
|
|
333
|
+
? { x: vis.x, y: vis.y, width: vis.width, height: vis.height }
|
|
334
|
+
: null
|
|
335
|
+
} else {
|
|
336
|
+
inst._paintedRect = null
|
|
337
|
+
}
|
|
338
|
+
}
|
|
322
339
|
if (prevBuf && inst && !inst._subtreeDirty) {
|
|
323
340
|
const layout = node._resolved?._layout ?? node._layout
|
|
324
341
|
if (layout && layoutEqual(layout, inst._lastLayout)) {
|
|
@@ -913,6 +930,7 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
913
930
|
|
|
914
931
|
for (const [key, inst] of instances) {
|
|
915
932
|
if (!visited.has(key)) {
|
|
933
|
+
inst._paintedRect = null
|
|
916
934
|
disposeScope(inst.scope)
|
|
917
935
|
instances.delete(key)
|
|
918
936
|
}
|
|
@@ -1060,6 +1078,7 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
1060
1078
|
|
|
1061
1079
|
for (const [key, inst] of instances) {
|
|
1062
1080
|
if (!visited.has(key)) {
|
|
1081
|
+
inst._paintedRect = null
|
|
1063
1082
|
disposeScope(inst.scope)
|
|
1064
1083
|
instances.delete(key)
|
|
1065
1084
|
}
|
|
@@ -1173,6 +1192,7 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
|
|
|
1173
1192
|
process.off('SIGHUP', onSighup)
|
|
1174
1193
|
|
|
1175
1194
|
for (const inst of instances.values()) {
|
|
1195
|
+
inst._paintedRect = null
|
|
1176
1196
|
disposeScope(inst.scope)
|
|
1177
1197
|
}
|
|
1178
1198
|
instances.clear()
|