Skip to content

Commit e495a2a

Browse files
authored
fix: recursively-calculate-xy-position (#9)
* recursively find x,y position * demo * update readme
1 parent 7ad1f95 commit e495a2a

File tree

5 files changed

+167
-62
lines changed

5 files changed

+167
-62
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
Provides mouse support for [Ink](http://github.com/vadimdemedes/ink) components.
44

55

6-
https://github.com/zenobi-us/react-ink-mouse/assets/61225/310e5423-2efa-457a-a61f-3964657852c6
7-
6+
https://github.com/zenobi-us/ink-mouse/assets/61225/344e4b22-8c47-437a-b304-d1e455480dfe
87

98
## Features
109

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
{
22
"name": "@zenobius/ink-mouse",
33
"version": "1.0.1",
4+
"type": "module",
45
"peerDependencies": {
56
"ink": "^5.0.1",
67
"react": "^18.2.0"
78
},
89
"devDependencies": {
910
"@arethetypeswrong/cli": "^0.15.3",
11+
"@react-hookz/web": "^24.0.4",
1012
"@swc/core": "^1.6.1",
1113
"@types/is-ci": "^3",
1214
"@types/node": "18.16.9",
@@ -37,13 +39,13 @@
3739
"vite": "^5.3.1",
3840
"vitest": "^1.6.0"
3941
},
40-
"main": "./dist/ink-mouse.js",
41-
"types": "./dist/ink-mouse.d.ts",
42+
"main": "./dist/ink-mouse.cjs",
43+
"types": "./dist/ink-mouse.d.cts",
4244
"exports": {
4345
".": {
44-
"import": "./dist/ink-mouse.js",
45-
"require": "./dist/ink-mouse.js",
46-
"types": "./dist/ink-mouse.d.ts"
46+
"import": "./dist/ink-mouse.cjs",
47+
"require": "./dist/ink-mouse.cjs",
48+
"types": "./dist/ink-mouse.d.cts"
4749
}
4850
},
4951
"files": [

src/demo/index.tsx

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
22
import type { ComponentProps, PropsWithChildren } from 'react';
33
import { render, Box, Text, useStdout } from 'ink';
44
import type { DOMElement } from 'ink';
5+
import { useMap } from '@react-hookz/web';
56

67
import {
78
MouseProvider,
8-
useElementPosition,
99
useMousePosition,
1010
useMouseAction,
1111
useOnMouseHover,
1212
useOnMouseClick,
13-
} from '../..';
13+
} from '../../src/ink-mouse.ts';
1414

1515
function App() {
1616
return (
@@ -48,36 +48,14 @@ function useTerminalSize() {
4848
}
4949

5050
function View() {
51-
const [count, setCount] = useState(0);
51+
const map = useMap<'button1' | 'button2' | 'button3' | 'listitem1' | 'listitem2', number>()
5252
const size = useTerminalSize();
5353
const mouse = useMousePosition();
5454
const action = useMouseAction();
5555

56-
const ref = useRef<DOMElement>(null);
57-
const position = useElementPosition(ref);
58-
const label = useMemo(() => {
59-
// is the mouse getting close to the button?
60-
if (!position || !mouse) {
61-
return ' 🤔 ';
62-
}
63-
64-
const h = Math.pow(position.left - mouse.x, 2);
65-
const v = Math.pow(position.top - mouse.y, 2);
66-
67-
const distance = Math.sqrt(h + v);
68-
69-
if (distance < 10) {
70-
return ' 💀 ';
71-
}
72-
73-
if (distance < 20) {
74-
return ' 🤯 ';
75-
}
76-
77-
return ' 🤭 ';
78-
}, [count, position, mouse]);
7956

8057
return (
58+
<>
8159
<Box
8260
flexDirection="column"
8361
paddingY={1}
@@ -90,34 +68,95 @@ function View() {
9068
alignItems="flex-start"
9169
justifyContent="center"
9270
>
93-
<Box ref={ref}>
71+
<Box gap={1} flexDirection='column' width="20">
72+
<Box gap={1}>
9473
<Button
95-
label={label}
74+
label='button 1'
9675
onClick={() => {
97-
setCount((prev) => prev + 1);
76+
map.set('button1', (map.get('button1') ||0) + 1)
9877
}}
9978
/>
79+
<Button
80+
label='button 2'
81+
onClick={() => {
82+
map.set('button2', (map.get('button2') ||0) + 1)
83+
}}
84+
/>
85+
</Box>
86+
<Button onClick={() => {
87+
map.set('listitem1', (map.get('listitem1') ||0) + 1)
88+
89+
}}>
90+
<Box flexDirection='row' flexGrow={1}>
91+
<Box flexDirection='column'>
92+
<Text color="yellowBright" bold>listitem 1</Text>
93+
<Box flexDirection='row' gap={1}>
94+
<Text color="gray">subtitle</Text>
95+
<Text color="gray">things: 1</Text>
96+
</Box>
97+
</Box>
98+
<Box justifyContent='flex-end' flexGrow={1}>
99+
<Text color="brown">clicks: {map.get('listitem1') || 0}</Text>
100+
</Box>
101+
</Box>
102+
</Button>
103+
<Button onClick={() => {
104+
map.set('listitem2', (map.get('listitem2') ||0) + 1)
105+
}}>
106+
<Box flexDirection='row' flexGrow={1}>
107+
<Box flexDirection='column'>
108+
<Text color="yellowBright" bold>listitem 2</Text>
109+
<Box flexDirection='row' gap={1}>
110+
<Text color="gray">subtitle</Text>
111+
<Text color="gray">things: 1</Text>
112+
</Box>
113+
</Box>
114+
<Box justifyContent='flex-end' flexGrow={1}>
115+
<Text color="brown">clicks: {map.get('listitem2') || 0}</Text>
116+
</Box>
117+
</Box>
118+
</Button>
119+
100120
</Box>
101121

102-
<Box>
122+
<Box marginTop={3}>
103123
<Text>Log:</Text>
104124
<Box flexDirection="column">
105-
<Text>Count: {count}</Text>
125+
<Text>Button 1 Count: {map.get('button1')}</Text>
126+
<Text>Button 2 Count: {map.get('button2')}</Text>
127+
<Text>Button 3 Count: {map.get('button3')}</Text>
106128
<Text>
107129
Mouse: {mouse.x},{mouse.y}
108130
</Text>
109131
<Text>Action: {action}</Text>
110132
</Box>
111133
</Box>
134+
135+
136+
112137
</Box>
113-
);
138+
<Button
139+
position="absolute"
140+
marginTop={1}
141+
marginLeft={43}
142+
onClick={() => {
143+
map.set('button3', (map.get('button3') ||0) + 1)
144+
}}
145+
>
146+
<Text>Button 3</Text>
147+
</Button>
148+
149+
</>
150+
151+
);
114152
}
115153

116154
function Button({
117155
label,
118156
children,
119157
onClick,
120-
}: PropsWithChildren<{
158+
...props
159+
}: PropsWithChildren<ComponentProps<typeof Box> & {
121160
label?: string;
122161
onClick?: () => void;
123162
}>) {
@@ -147,15 +186,16 @@ function Button({
147186
}, [clicking, hovering]);
148187

149188
return (
150-
<Box
151-
gap={1}
152-
paddingX={1}
153-
ref={ref}
154-
borderStyle={border}
155-
>
156-
{!!label && <Text>{label}</Text>}
157-
{children}
158-
</Box>
189+
<Box
190+
{...props}
191+
paddingX={1}
192+
gap={1}
193+
ref={ref}
194+
borderStyle={border}
195+
>
196+
{!!label && <Text>{label}</Text>}
197+
{children}
198+
</Box>
159199
);
160200
}
161201

src/ink-mouse/useElementPosition.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { DOMElement } from 'ink';
22
import { useEffect, useState, type RefObject } from 'react';
33

4+
/**
5+
* Statefule hook to provide the position of the referenced element.
6+
*
7+
* @param ref - The reference to the element.
8+
* @returns The position of the element.
9+
*/
410
function useElementPosition(ref: RefObject<DOMElement>) {
511
const [position, setPosition] = useState<{
612
left: number;
@@ -25,6 +31,10 @@ function useElementPosition(ref: RefObject<DOMElement>) {
2531
return position;
2632
}
2733

34+
/**
35+
* Get the position of the element.
36+
*
37+
*/
2838
function getElemenetPosition(node: DOMElement | null) {
2939
if (!node) {
3040
return null;
@@ -33,22 +43,52 @@ function getElemenetPosition(node: DOMElement | null) {
3343
if (!node.yogaNode) {
3444
return null;
3545
}
36-
const parent = node.yogaNode.getParent();
46+
const elementLayout = node.yogaNode.getComputedLayout()
3747

38-
if (!parent) {
39-
return null;
40-
}
41-
const parentLayout = parent.getComputedLayout();
42-
43-
const width = node.yogaNode.getComputedWidth();
44-
const height = node.yogaNode.getComputedHeight();
48+
const parentXY = walkParentPosition(node)
4549

46-
return {
47-
top: parentLayout.top,
48-
left: parentLayout.left,
49-
width,
50-
height,
50+
const position = {
51+
left: elementLayout.left + parentXY.x,
52+
top: elementLayout.top + parentXY.y,
53+
width: elementLayout.width,
54+
height: elementLayout.height,
5155
};
56+
57+
return position
58+
}
59+
60+
/**
61+
* Walk the parent ancestory to get the position of the element.
62+
*
63+
* Since InkNodes are relative by default and because Ink does not
64+
* provide precomputed x and y values, we need to walk the parent and
65+
* accumulate the x and y values.
66+
*
67+
* I only discovered this by debugging the getElementPosition before
68+
* and after wrapping the element in a Box with padding:
69+
*
70+
* - before padding: { left: 0, top: 0, width: 10, height: 1 }
71+
* - after padding: { left: 2, top: 0, width: 10, height: 1 }
72+
*
73+
* It's still a mystery why padding on a parent results in the child
74+
* having a different top value. `#todo`
75+
*/
76+
function walkParentPosition(node: DOMElement) {
77+
let parent = node.parentNode
78+
let x = 0
79+
let y = 0
80+
while (parent) {
81+
if (!parent.yogaNode) {
82+
return { x, y}
83+
}
84+
85+
const layout = parent.yogaNode.getComputedLayout()
86+
x += layout.left
87+
y += layout.top
88+
parent = parent.parentNode
89+
}
90+
return { x, y }
91+
5292
}
5393

5494
export { useElementPosition, getElemenetPosition };

yarn.lock

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,29 @@ __metadata:
435435
languageName: node
436436
linkType: hard
437437

438+
"@react-hookz/deep-equal@npm:^1.0.4":
439+
version: 1.0.4
440+
resolution: "@react-hookz/deep-equal@npm:1.0.4"
441+
checksum: 0923e364d309e32ee54e0850471a86488faf149d7a04ee838552cf5d54f493964623a8d742880ec82410cc1105530123f056e66dfc72b7da235d4cc93fad708f
442+
languageName: node
443+
linkType: hard
444+
445+
"@react-hookz/web@npm:^24.0.4":
446+
version: 24.0.4
447+
resolution: "@react-hookz/web@npm:24.0.4"
448+
dependencies:
449+
"@react-hookz/deep-equal": "npm:^1.0.4"
450+
peerDependencies:
451+
js-cookie: ^3.0.5
452+
react: ^16.8 || ^17 || ^18
453+
react-dom: ^16.8 || ^17 || ^18
454+
peerDependenciesMeta:
455+
js-cookie:
456+
optional: true
457+
checksum: 6a841c648edbc54b11fd90de9bb61c3059255598fc4a714c508c269a03c4ca9bbf32cf017d3bd2b3a1bf7cd1d9bf4bb56028f64ad455f796079632f4a7cd4f00
458+
languageName: node
459+
linkType: hard
460+
438461
"@rollup/rollup-android-arm-eabi@npm:4.18.0":
439462
version: 4.18.0
440463
resolution: "@rollup/rollup-android-arm-eabi@npm:4.18.0"
@@ -1032,6 +1055,7 @@ __metadata:
10321055
resolution: "@zenobius/ink-mouse@workspace:."
10331056
dependencies:
10341057
"@arethetypeswrong/cli": "npm:^0.15.3"
1058+
"@react-hookz/web": "npm:^24.0.4"
10351059
"@swc/core": "npm:^1.6.1"
10361060
"@types/is-ci": "npm:^3"
10371061
"@types/node": "npm:18.16.9"

0 commit comments

Comments
 (0)