Skip to content

Commit cad1156

Browse files
committed
Access caret transformation, selection transformations, and range for the input component fixes #114
1 parent 319a4fb commit cad1156

File tree

9 files changed

+100
-51
lines changed

9 files changed

+100
-51
lines changed

docs/getting-started/components-and-properties.md

+17
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,23 @@ The `Input` component extends the `Text` component and allows the user to change
291291
</Root>
292292
```
293293

294+
The `Input` component also exposes a ref that provides access to various properties and methods for controlling the input programmatically. This ref can be used to focus or blur the input, access the current value, and get information about the selection and caret position.
295+
296+
<details>
297+
<summary>View all properties exposed in the Input ref</summary>
298+
299+
| Property | Description |
300+
| ------------------------ | ---------------------------------------------------------------- |
301+
| current | A signal containing the current value of the input |
302+
| focus | Method to programmatically focus the input |
303+
| blur | Method to programmatically remove focus from the input |
304+
| element | A signal containing the underlying HTML element |
305+
| selectionRange | A signal containing the current selection range [start, end] |
306+
| caretTransformation | A signal containing information about the caret's transformation (position and height) |
307+
| selectionTransformations | A signal containing the transformations for all selection (boxes) |
308+
309+
</details>
310+
294311
<details>
295312
<summary>View all properties specific to the `Input` component</summary>
296313

packages/react/src/input.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { forwardRef, ReactNode, RefAttributes, useEffect, useMemo, useRef } from 'react'
2-
import { Object3D } from 'three'
2+
import { Object3D, Vector2Tuple } from 'three'
33
import { useParent } from './context.js'
44
import { AddHandlers, R3FEventMap, usePropertySignals } from './utils.js'
55
import {
@@ -9,6 +9,8 @@ import {
99
createInput,
1010
initialize,
1111
unsubscribeSubscriptions,
12+
CaretTransformation,
13+
SelectionTransformation,
1214
} from '@pmndrs/uikit/internals'
1315
import { ComponentInternals, useComponentInternals } from './ref.js'
1416
import { ReadonlySignal, signal } from '@preact/signals-core'
@@ -21,6 +23,9 @@ export type InputInternals = ComponentInternals<BaseInputProperties<R3FEventMap>
2123
focus: () => void
2224
blur: () => void
2325
element: ReadonlySignal<HTMLInputElement | HTMLTextAreaElement | undefined>
26+
selectionRange: ReadonlySignal<Vector2Tuple | undefined>
27+
caretTransformation: ReadonlySignal<CaretTransformation | undefined>
28+
selectionTransformations: ReadonlySignal<Array<SelectionTransformation>>
2429
}
2530

2631
export type InputProperties = BaseInputProperties<R3FEventMap> & {
@@ -68,8 +73,19 @@ export const Input: (props: InputProperties & RefAttributes<InputRef>) => ReactN
6873
blur: internals.blur,
6974
current: internals.valueSignal,
7075
element: internals.element,
76+
selectionRange: internals.selectionRange,
77+
caretTransformation: internals.caretTransformation,
78+
selectionTransformations: internals.selectionTransformations,
7179
}),
72-
[internals.focus, internals.blur, internals.valueSignal, internals.element],
80+
[
81+
internals.focus,
82+
internals.blur,
83+
internals.valueSignal,
84+
internals.element,
85+
internals.caretTransformation,
86+
internals.selectionRange,
87+
internals.selectionTransformations,
88+
],
7389
),
7490
)
7591

packages/react/src/portal.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Raycaster,
1010
Vector2,
1111
Vector3,
12+
OrthographicCamera,
1213
} from 'three'
1314
import { Image } from './image.js'
1415
import { InjectState, RootState, reconciler, useFrame, useStore, context } from '@react-three/fiber'
@@ -32,9 +33,9 @@ export const privateKeys = [
3233
'viewport',
3334
]
3435

35-
type Camera = THREE.OrthographicCamera | THREE.PerspectiveCamera
36-
const isOrthographicCamera = (def: Camera): def is THREE.OrthographicCamera =>
37-
def && (def as THREE.OrthographicCamera).isOrthographicCamera
36+
type Camera = OrthographicCamera | PerspectiveCamera
37+
const isOrthographicCamera = (def: Camera): def is OrthographicCamera =>
38+
def && (def as OrthographicCamera).isOrthographicCamera
3839

3940
type BasePortalProperties = Omit<ImageProperties<R3FEventMap>, 'src' | 'objectFit'>
4041

packages/uikit/src/caret.ts

+16-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Signal, computed, effect, signal } from '@preact/signals-core'
2-
import { Matrix4, Vector3Tuple } from 'three'
2+
import { Matrix4, Vector2Tuple } from 'three'
33
import { ClippingRect } from './clipping.js'
44
import { ElementType, OrderInfo, computedOrderInfo } from './order.js'
55
import { PanelProperties, createInstancedPanel } from './panel/instanced-panel.js'
@@ -12,6 +12,11 @@ import {
1212
} from './panel/index.js'
1313
import { MergedProperties, computedInheritableProperty } from './properties/index.js'
1414

15+
export type CaretTransformation = {
16+
position: Vector2Tuple
17+
height: number
18+
}
19+
1520
export type CaretWidthProperties = {
1621
caretWidth?: number
1722
}
@@ -66,7 +71,7 @@ function getCaretMaterialConfig() {
6671
export function createCaret(
6772
propertiesSignal: Signal<MergedProperties>,
6873
matrix: Signal<Matrix4 | undefined>,
69-
caretPosition: Signal<Vector3Tuple | undefined>,
74+
caretTransformation: Signal<CaretTransformation | undefined>,
7075
isVisible: Signal<boolean>,
7176
parentOrderInfo: Signal<OrderInfo | undefined>,
7277
parentClippingRect: Signal<ClippingRect | undefined> | undefined,
@@ -80,16 +85,16 @@ export function createCaret(
8085
defaultPanelDependencies,
8186
parentOrderInfo,
8287
)
83-
const blinkingCaretPosition = signal<Vector3Tuple | undefined>(undefined)
88+
const blinkingCaretTransformation = signal<CaretTransformation | undefined>(undefined)
8489
initializers.push(() =>
8590
effect(() => {
86-
const pos = caretPosition.value
91+
const pos = caretTransformation.value
8792
if (pos == null) {
88-
blinkingCaretPosition.value = undefined
93+
blinkingCaretTransformation.value = undefined
8994
}
90-
blinkingCaretPosition.value = pos
95+
blinkingCaretTransformation.value = pos
9196
const ref = setInterval(
92-
() => (blinkingCaretPosition.value = blinkingCaretPosition.peek() == null ? pos : undefined),
97+
() => (blinkingCaretTransformation.value = blinkingCaretTransformation.peek() == null ? pos : undefined),
9398
500,
9499
)
95100
return () => clearInterval(ref)
@@ -106,14 +111,14 @@ export function createCaret(
106111
panelGroupManager,
107112
matrix,
108113
computed(() => {
109-
const size = blinkingCaretPosition.value
110-
if (size == null) {
114+
const height = blinkingCaretTransformation.value?.height
115+
if (height == null) {
111116
return [0, 0]
112117
}
113-
return [caretWidth.value, size[2]]
118+
return [caretWidth.value, height]
114119
}),
115120
computed(() => {
116-
const position = blinkingCaretPosition.value
121+
const position = blinkingCaretTransformation.value?.position
117122
if (position == null) {
118123
return [0, 0]
119124
}

packages/uikit/src/components/input.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import { PanelGroupProperties, computedPanelGroupDependencies } from '../panel/i
3737
import { createInteractionPanel } from '../panel/instanced-panel-mesh.js'
3838
import { EventHandlers, ThreeEventMap, ThreePointerEvent } from '../events.js'
3939
import { Vector2Tuple, Vector2, Vector3Tuple } from 'three'
40-
import { CaretProperties, createCaret } from '../caret.js'
41-
import { SelectionBoxes, SelectionProperties, createSelection } from '../selection.js'
40+
import { CaretProperties, CaretTransformation, createCaret } from '../caret.js'
41+
import { SelectionTransformation, SelectionProperties, createSelection } from '../selection.js'
4242
import { WithFocus, createFocusPropertyTransformers } from '../focus.js'
4343
import {
4444
FontFamilies,
@@ -185,13 +185,13 @@ export function createInput<EM extends ThreeEventMap = ThreeEventMap>(
185185
)
186186

187187
const instancedTextRef: { current?: InstancedText } = {}
188-
const selectionBoxes = signal<SelectionBoxes>([])
189-
const caretPosition = signal<Vector3Tuple | undefined>(undefined)
188+
const selectionTransformations = signal<Array<SelectionTransformation>>([])
189+
const caretTransformation = signal<CaretTransformation | undefined>(undefined)
190190
const selectionRange = signal<Vector2Tuple | undefined>(undefined)
191191
createCaret(
192192
mergedProperties,
193193
globalMatrix,
194-
caretPosition,
194+
caretTransformation,
195195
isVisible,
196196
backgroundOrderInfo,
197197
parentCtx.clippingRect,
@@ -201,7 +201,7 @@ export function createInput<EM extends ThreeEventMap = ThreeEventMap>(
201201
const selectionOrderInfo = createSelection(
202202
mergedProperties,
203203
globalMatrix,
204-
selectionBoxes,
204+
selectionTransformations,
205205
isVisible,
206206
backgroundOrderInfo,
207207
parentCtx.clippingRect,
@@ -240,8 +240,8 @@ export function createInput<EM extends ThreeEventMap = ThreeEventMap>(
240240
fontSignal,
241241
parentCtx.root.gylphGroupManager,
242242
selectionRange,
243-
selectionBoxes,
244-
caretPosition,
243+
selectionTransformations,
244+
caretTransformation,
245245
instancedTextRef,
246246
initializers,
247247
multiline ? 'break-word' : 'keep-all',
@@ -335,6 +335,9 @@ export function createInput<EM extends ThreeEventMap = ThreeEventMap>(
335335
interactionPanel,
336336
handlers,
337337
initializers,
338+
selectionRange,
339+
caretTransformation,
340+
selectionTransformations,
338341
})
339342
}
340343

packages/uikit/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ export type {
2626
export * from './vanilla/index.js'
2727
export { htmlToCode } from './convert/html/index.js'
2828
export type { ColorRepresentation } from './utils.js'
29+
export type { CaretTransformation } from './caret.js'
30+
export type { SelectionTransformation } from './selection.js'

packages/uikit/src/internals.ts

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export * from './panel/index.js'
1414
export * from './scroll.js'
1515
export * from './components/index.js'
1616
export * from './convert/html/internals.js'
17+
export * from './caret.js'
18+
export * from './selection.js'
1719

1820
export type * from './events.js'
1921
export type * from './context.js'

packages/uikit/src/selection.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from './panel/index.js'
1919
import { MergedProperties } from './properties/index.js'
2020

21-
export type SelectionBoxes = Array<{ size: Vector2Tuple; position: Vector2Tuple }>
21+
export type SelectionTransformation = { size: Vector2Tuple; position: Vector2Tuple }
2222

2323
export type SelectionBorderSizeProperties = {
2424
selectionBorderRightWidth?: number
@@ -69,7 +69,7 @@ function getSelectionMaterialConfig() {
6969
export function createSelection(
7070
propertiesSignal: Signal<MergedProperties>,
7171
matrix: Signal<Matrix4 | undefined>,
72-
selectionBoxes: Signal<SelectionBoxes>,
72+
selectionTransformations: Signal<Array<SelectionTransformation>>,
7373
isVisible: Signal<boolean>,
7474
prevOrderInfo: Signal<OrderInfo | undefined>,
7575
parentClippingRect: Signal<ClippingRect | undefined> | undefined,
@@ -93,7 +93,7 @@ export function createSelection(
9393
initializers.push(
9494
() =>
9595
effect(() => {
96-
const selections = selectionBoxes.value
96+
const selections = selectionTransformations.value
9797
const selectionsLength = selections.length
9898
for (let i = 0; i < selectionsLength; i++) {
9999
let panelData = panels[i]

packages/uikit/src/text/render/instanced-text.ts

+27-24
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import {
1212
} from '../utils.js'
1313
import { GlyphGroupManager, InstancedGlyphGroup } from './instanced-glyph-group.js'
1414
import { GlyphLayout, GlyphLayoutProperties, buildGlyphLayout, computedCustomLayouting } from '../layout.js'
15-
import { SelectionBoxes } from '../../selection.js'
15+
import { SelectionTransformation } from '../../selection.js'
1616
import { OrderInfo } from '../../order.js'
1717
import { Font } from '../font.js'
1818
import { MergedProperties, computedInheritableProperty } from '../../properties/index.js'
1919
import { FlexNode, FlexNodeState } from '../../flex/index.js'
20+
import { CaretTransformation } from '../../caret.js'
2021

2122
export type TextAlignProperties = {
2223
textAlign?: keyof typeof alignmentXMap | 'block'
@@ -43,8 +44,8 @@ export function createInstancedText(
4344
fontSignal: Signal<Font | undefined>,
4445
glyphGroupManager: GlyphGroupManager,
4546
selectionRange: Signal<Vector2Tuple | undefined> | undefined,
46-
selectionBoxes: Signal<SelectionBoxes> | undefined,
47-
caretPosition: Signal<Vector3Tuple | undefined> | undefined,
47+
selectionTransformations: Signal<Array<SelectionTransformation>> | undefined,
48+
caretTransformation: Signal<CaretTransformation | undefined> | undefined,
4849
instancedTextRef: { current?: InstancedText } | undefined,
4950
initializers: Initializers,
5051
defaultWordBreak: GlyphLayoutProperties['wordBreak'],
@@ -108,8 +109,8 @@ export function createInstancedText(
108109
isVisible,
109110
parentClippingRect,
110111
selectionRange,
111-
selectionBoxes,
112-
caretPosition,
112+
selectionTransformations,
113+
caretTransformation,
113114
)
114115
if (instancedTextRef != null) {
115116
instancedTextRef.current = instancedText
@@ -121,7 +122,7 @@ export function createInstancedText(
121122
return customLayouting
122123
}
123124

124-
const noSelectionBoxes: SelectionBoxes = []
125+
const noSelectionTransformations: Array<SelectionTransformation> = []
125126

126127
export class InstancedText {
127128
private glyphLines: Array<Array<InstancedGlyph | number>> = []
@@ -142,8 +143,8 @@ export class InstancedText {
142143
isVisible: Signal<boolean>,
143144
private parentClippingRect: Signal<ClippingRect | undefined> | undefined,
144145
private selectionRange: Signal<Vector2Tuple | undefined> | undefined,
145-
private selectionBoxes: Signal<SelectionBoxes> | undefined,
146-
private caretPosition: Signal<Vector3Tuple | undefined> | undefined,
146+
private selectionTransformations: Signal<Array<SelectionTransformation>> | undefined,
147+
private caretTransformation: Signal<CaretTransformation | undefined> | undefined,
147148
) {
148149
this.unsubscribeInitialList = [
149150
effect(() => {
@@ -194,12 +195,12 @@ export class InstancedText {
194195
verticalAlign: keyof typeof alignmentYMap,
195196
textAlign: keyof typeof alignmentXMap | 'block',
196197
): void {
197-
if (this.caretPosition == null || this.selectionBoxes == null) {
198+
if (this.caretTransformation == null || this.selectionTransformations == null) {
198199
return
199200
}
200201
if (range == null || layout == null || layout.lines.length === 0) {
201-
this.caretPosition.value = undefined
202-
this.selectionBoxes.value = noSelectionBoxes
202+
this.caretTransformation.value = undefined
203+
this.selectionTransformations.value = noSelectionTransformations
203204
return
204205
}
205206
const whitespaceWidth = layout.font.getGlyphInfo(' ').xadvance * layout.fontSize
@@ -212,39 +213,41 @@ export class InstancedText {
212213
lineIndex * getOffsetToNextLine(layout.lineHeight, layout.fontSize) +
213214
getGlyphOffsetY(layout.fontSize, layout.lineHeight)
214215
)
215-
this.caretPosition.value = [x, y - layout.fontSize / 2, layout.fontSize]
216-
this.selectionBoxes.value = []
216+
this.caretTransformation.value = { position: [x, y - layout.fontSize / 2], height: layout.fontSize }
217+
this.selectionTransformations.value = []
217218
return
218219
}
219-
this.caretPosition.value = undefined
220+
this.caretTransformation.value = undefined
220221
const start = this.getGlyphLineAndX(layout, startCharIndexIncl, true, whitespaceWidth, textAlign)
221222
const end = this.getGlyphLineAndX(layout, endCharIndexExcl - 1, false, whitespaceWidth, textAlign)
222223
if (start.lineIndex === end.lineIndex) {
223-
this.selectionBoxes.value = [
224-
this.computeSelectionBox(start.lineIndex, start.x, end.x, layout, verticalAlign, whitespaceWidth),
224+
this.selectionTransformations.value = [
225+
this.computeSelectionTransformation(start.lineIndex, start.x, end.x, layout, verticalAlign, whitespaceWidth),
225226
]
226227
return
227228
}
228-
const newSelectionBoxes: SelectionBoxes = [
229-
this.computeSelectionBox(start.lineIndex, start.x, undefined, layout, verticalAlign, whitespaceWidth),
229+
const newSelectionTransformations: Array<SelectionTransformation> = [
230+
this.computeSelectionTransformation(start.lineIndex, start.x, undefined, layout, verticalAlign, whitespaceWidth),
230231
]
231232
for (let i = start.lineIndex + 1; i < end.lineIndex; i++) {
232-
newSelectionBoxes.push(this.computeSelectionBox(i, undefined, undefined, layout, verticalAlign, whitespaceWidth))
233+
newSelectionTransformations.push(
234+
this.computeSelectionTransformation(i, undefined, undefined, layout, verticalAlign, whitespaceWidth),
235+
)
233236
}
234-
newSelectionBoxes.push(
235-
this.computeSelectionBox(end.lineIndex, undefined, end.x, layout, verticalAlign, whitespaceWidth),
237+
newSelectionTransformations.push(
238+
this.computeSelectionTransformation(end.lineIndex, undefined, end.x, layout, verticalAlign, whitespaceWidth),
236239
)
237-
this.selectionBoxes.value = newSelectionBoxes
240+
this.selectionTransformations.value = newSelectionTransformations
238241
}
239242

240-
private computeSelectionBox(
243+
private computeSelectionTransformation(
241244
lineIndex: number,
242245
startX: number | undefined,
243246
endX: number | undefined,
244247
layout: GlyphLayout,
245248
verticalAlign: keyof typeof alignmentYMap,
246249
whitespaceWidth: number,
247-
): SelectionBoxes[number] {
250+
): SelectionTransformation {
248251
const lineGlyphs = this.glyphLines[lineIndex]
249252
if (startX == null) {
250253
startX = this.getGlyphX(lineGlyphs[0], 0, whitespaceWidth)

0 commit comments

Comments
 (0)