Skip to content

Commit

Permalink
feat: ref.current.setStyle
Browse files Browse the repository at this point in the history
  • Loading branch information
bbohlender committed Apr 5, 2024
1 parent 3a40374 commit a0ee4ab
Show file tree
Hide file tree
Showing 42 changed files with 455 additions and 259 deletions.
9 changes: 7 additions & 2 deletions examples/uikit/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Suspense, useMemo, useState } from 'react'
import { Suspense, useMemo, useRef, useState } from 'react'
import { Canvas } from '@react-three/fiber'
import { Gltf, Box, PerspectiveCamera, RenderTexture } from '@react-three/drei'
import { signal } from '@preact/signals-core'
Expand All @@ -14,6 +14,8 @@ import {
SuspendingImage,
Input,
FontFamilyProvider,
ComponentInternals,
ImageProperties,
} from '@react-three/uikit'
import { Texture } from 'three'
import { Skeleton } from '../../../packages/kits/default/skeleton.js'
Expand All @@ -24,6 +26,7 @@ export default function App() {
const s = useMemo(() => signal(5), [])
const x = useMemo(() => signal<string | undefined>('red'), [])
const t = useMemo(() => signal('X X\nX X'), [])
const ref = useRef<ComponentInternals<ImageProperties>>(null)
return (
<Canvas style={{ height: '100dvh', touchAction: 'none' }} gl={{ localClippingEnabled: true }}>
<FontFamilyProvider inter={{ normal: 'inter-normal.json' }}>
Expand Down Expand Up @@ -118,9 +121,11 @@ export default function App() {
<Suspense fallback={<Skeleton width={300} aspectRatio={2 / 3} />}>
<SuspendingImage
flexShrink={0}
hover={{ padding: 30, border: 0, marginLeft: -30, opacity: 1 }}
hover={{ padding: 30, marginLeft: -30, opacity: 1 }}
fit="cover"
border={20}
ref={ref}
onHoverChange={(hovered) => ref.current?.setStyle({ borderOpacity: hovered ? 10 : 0.2 })}
borderOpacity={0.2}
borderRadius={10}
flexDirection="column"
Expand Down
12 changes: 10 additions & 2 deletions packages/react/src/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ export const Container: (
children?: ReactNode
} & ContainerProperties &
EventHandlers &
RefAttributes<ComponentInternals>,
RefAttributes<ComponentInternals<ContainerProperties>>,
) => ReactNode = forwardRef((properties, ref) => {
const parent = useParent()
const outerRef = useRef<Object3D>(null)
const innerRef = useRef<Object3D>(null)
const propertySignals = usePropertySignals(properties)
const internals = useMemo(
() => createContainer(parent, propertySignals.properties, propertySignals.default, outerRef, innerRef),
() =>
createContainer(
parent,
propertySignals.style,
propertySignals.properties,
propertySignals.default,
outerRef,
innerRef,
),
[parent, propertySignals],
)
useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals])
Expand Down
4 changes: 2 additions & 2 deletions packages/react/src/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ export const Content: (
children?: ReactNode
} & ContentProperties &
EventHandlers &
RefAttributes<ComponentInternals>,
RefAttributes<ComponentInternals<ContentProperties>>,
) => ReactNode = forwardRef((properties, ref) => {
const parent = useParent()
const outerRef = useRef<Object3D>(null)
const innerRef = useRef<Object3D>(null)
const propertySignals = usePropertySignals(properties)
const internals = useMemo(
() => createContent(parent, propertySignals.properties, propertySignals.default, outerRef),
() => createContent(parent, propertySignals.style, propertySignals.properties, propertySignals.default, outerRef),
[parent, propertySignals],
)
useEffect(() => {
Expand Down
11 changes: 9 additions & 2 deletions packages/react/src/custom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,21 @@ export const CustomContainer: (
customDistanceMaterial?: Material
} & CustomContainerProperties &
EventHandlers &
RefAttributes<ComponentInternals>,
RefAttributes<ComponentInternals<CustomContainerProperties>>,
) => ReactNode = forwardRef((properties, ref) => {
const parent = useParent()
const outerRef = useRef<Object3D>(null)
const innerRef = useRef<Mesh>(null)
const propertySignals = usePropertySignals(properties)
const internals = useMemo(
() => createCustomContainer(parent, propertySignals.properties, propertySignals.default, outerRef),
() =>
createCustomContainer(
parent,
propertySignals.style,
propertySignals.properties,
propertySignals.default,
outerRef,
),
[parent, propertySignals],
)
useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/fullscreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Fullscreen: (
children?: ReactNode
attachCamera?: boolean
} & EventHandlers &
RefAttributes<ComponentInternals>,
RefAttributes<ComponentInternals<RootProperties>>,
) => ReactNode = forwardRef((properties, ref) => {
const store = useStore()
const pixelSize = properties.pixelSize ?? 0.01
Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import type { EventHandlers } from '@react-three/fiber/dist/declarations/src/cor
export const Icon: (
props: IconProperties &
EventHandlers &
RefAttributes<ComponentInternals> & { text: string; svgWidth: number; svgHeight: number; children?: ReactNode },
RefAttributes<ComponentInternals<IconProperties>> & {
text: string
svgWidth: number
svgHeight: number
children?: ReactNode
},
) => ReactNode = forwardRef((properties, ref) => {
const parent = useParent()
const outerRef = useRef<Object3D>(null)
Expand All @@ -21,6 +26,7 @@ export const Icon: (
properties.text,
properties.svgWidth,
properties.svgHeight,
propertySignals.style,
propertySignals.properties,
propertySignals.default,
outerRef,
Expand Down
13 changes: 11 additions & 2 deletions packages/react/src/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Signal, signal } from '@preact/signals-core'
export const Image: (
props: ImageProperties &
EventHandlers &
RefAttributes<ComponentInternals> & {
RefAttributes<ComponentInternals<ImageProperties>> & {
src?: Signal<string | undefined> | string | Texture | Signal<Texture | undefined>
children?: ReactNode
},
Expand All @@ -25,7 +25,16 @@ export const Image: (
)
srcSignal.value = properties.src
const internals = useMemo(
() => createImage(parent, srcSignal, propertySignals.properties, propertySignals.default, outerRef, innerRef),
() =>
createImage(
parent,
srcSignal,
propertySignals.style,
propertySignals.properties,
propertySignals.default,
outerRef,
innerRef,
),
[parent, propertySignals, srcSignal],
)
useEffect(() => () => unsubscribeSubscriptions(internals.subscriptions), [internals])
Expand Down
9 changes: 9 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ export {
type ViewportListeners,
type Listeners,
type AllOptionalProperties,
type ImageProperties,
type ContainerProperties,
type ContentProperties,
type CustomContainerProperties,
type IconProperties,
type InputProperties,
type RootProperties,
type SvgProperties,
type TextProperties,
} from '@pmndrs/uikit'
export { DefaultProperties } from './default.js'
export type { ComponentInternals } from './ref.js'
Expand Down
9 changes: 7 additions & 2 deletions packages/react/src/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { ComponentInternals, useComponentInternals } from './ref.js'
import { computed, ReadonlySignal, Signal, signal } from '@preact/signals-core'
import { useFontFamilies } from './font.js'

export type InputInternals = ComponentInternals & { current: ReadonlySignal<string>; focus: () => void }
export type InputInternals = ComponentInternals<InputProperties> & {
current: ReadonlySignal<string>
focus: () => void
}

export const Input: (
props: {
Expand Down Expand Up @@ -45,10 +48,12 @@ export const Input: (
if (!controlled.current) {
valueSignal.value = newValue
}
propertySignals.properties.peek().onValueChange?.(newValue)
propertySignals.style.peek()?.onValueChange?.(newValue)
propertySignals.properties.peek()?.onValueChange?.(newValue)
},
properties.multiline ?? false,
fontFamilies,
propertySignals.style,
propertySignals.properties,
propertySignals.default,
outerRef,
Expand Down
129 changes: 65 additions & 64 deletions packages/react/src/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,76 +17,77 @@ import type { DomEvent, EventHandlers } from '@react-three/fiber/dist/declaratio
import type { ImageProperties } from '@pmndrs/uikit/internals'
import type { ComponentInternals } from './ref.js'

export type PortalProperties = {
frames?: number
renderPriority?: number
eventPriority?: number
resolution?: number
children?: ReactNode
} & Omit<ComponentPropsWithoutRef<typeof Image>, 'src' | 'fit'>

export const Portal: (
props: Omit<ImageProperties, 'fit'> &
props: PortalProperties &
EventHandlers &
RefAttributes<ComponentInternals> & {
RefAttributes<ComponentInternals<PortalProperties>> & {
children?: ReactNode
},
) => ReactNode = forwardRef<
ComponentInternals,
{
frames?: number
renderPriority?: number
eventPriority?: number
resolution?: number
children?: ReactNode
} & Omit<ComponentPropsWithoutRef<typeof Image>, 'src' | 'fit'>
>(({ children, resolution = 1, frames = Infinity, renderPriority = 0, eventPriority = 0, ...props }, ref) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const fbo = useMemo(
() =>
new WebGLRenderTarget(1, 1, {
minFilter: LinearFilter,
magFilter: LinearFilter,
type: HalfFloatType,
) => ReactNode = forwardRef(
({ children, resolution = 1, frames = Infinity, renderPriority = 0, eventPriority = 0, ...props }, ref) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const fbo = useMemo(
() =>
new WebGLRenderTarget(1, 1, {
minFilter: LinearFilter,
magFilter: LinearFilter,
type: HalfFloatType,
}),
[],
)
const imageRef = useRef<ComponentInternals<ImageProperties>>(null)
const injectState = useMemo<InjectState>(
() => ({
events: { compute: uvCompute.bind(null, imageRef), priority: eventPriority },
size: { width: 1, height: 1, left: 0, top: 0 },
}),
[],
)
const imageRef = useRef<ComponentInternals>(null)
const injectState = useMemo<InjectState>(
() => ({
events: { compute: uvCompute.bind(null, imageRef), priority: eventPriority },
size: { width: 1, height: 1, left: 0, top: 0 },
}),
[eventPriority],
)
useEffect(() => {
if (imageRef.current == null) {
return
}
const { size } = imageRef.current
const unsubscribeSetSize = effect(() => {
const [width, height] = size.value
fbo.setSize(width, height)
injectState.size!.width = width
injectState.size!.height = height
})
return () => {
unsubscribeSetSize()
fbo.dispose()
}
}, [fbo, injectState])
useImperativeHandle(ref, () => imageRef.current!, [])
const vScene = useMemo(() => new Scene(), [])
return (
<>
{createPortal(
<ChildrenToFBO imageRef={imageRef} renderPriority={renderPriority} frames={frames} fbo={fbo}>
{children}
{/* Without an element that receives pointer events state.pointer will always be 0/0 */}
<group onPointerOver={() => null} />
</ChildrenToFBO>,
vScene,
injectState,
)}
<Image {...props} src={fbo.texture} fit="fill" keepAspectRatio={false} ref={imageRef} />
</>
)
})
[eventPriority],
)
useEffect(() => {
if (imageRef.current == null) {
return
}
const { size } = imageRef.current
const unsubscribeSetSize = effect(() => {
const [width, height] = size.value
fbo.setSize(width, height)
injectState.size!.width = width
injectState.size!.height = height
})
return () => {
unsubscribeSetSize()
fbo.dispose()
}
}, [fbo, injectState])
useImperativeHandle(ref, () => imageRef.current!, [])
const vScene = useMemo(() => new Scene(), [])
return (
<>
{createPortal(
<ChildrenToFBO imageRef={imageRef} renderPriority={renderPriority} frames={frames} fbo={fbo}>
{children}
{/* Without an element that receives pointer events state.pointer will always be 0/0 */}
<group onPointerOver={() => null} />
</ChildrenToFBO>,
vScene,
injectState,
)}
<Image {...props} src={fbo.texture} fit="fill" keepAspectRatio={false} ref={imageRef} />
</>
)
},
)

function uvCompute(
{ current }: RefObject<ComponentInternals>,
{ current }: RefObject<ComponentInternals<ImageProperties>>,
event: DomEvent,
state: RootState,
previous?: RootState,
Expand Down Expand Up @@ -116,7 +117,7 @@ function ChildrenToFBO({
renderPriority: number
children: ReactNode
fbo: WebGLRenderTarget
imageRef: RefObject<ComponentInternals>
imageRef: RefObject<ComponentInternals<ImageProperties>>
}) {
const store = useStore()
useEffect(() => {
Expand Down
9 changes: 5 additions & 4 deletions packages/react/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { ForwardedRef, RefObject, useImperativeHandle } from 'react'
import { Vector2Tuple, Mesh } from 'three'

export type ComponentInternals = {
export type ComponentInternals<T> = {
pixelSize: number
size: ReadonlySignal<Vector2Tuple>
center: ReadonlySignal<Vector2Tuple>
Expand All @@ -21,11 +21,12 @@ export type ComponentInternals = {
scrollPosition?: Signal<Vector2Tuple>
maxScrollPosition?: Signal<Partial<Vector2Tuple>>
interactionPanel: Mesh
setStyle(style: T | undefined): void
}

export function useComponentInternals<T, O = {}>(
ref: ForwardedRef<ComponentInternals & O>,
styleSignal: Signal<T>,
ref: ForwardedRef<ComponentInternals<T> & O>,
styleSignal: Signal<T | undefined>,
internals: ReturnType<
| typeof createContainer
| typeof createImage
Expand All @@ -45,7 +46,7 @@ export function useComponentInternals<T, O = {}>(
() => {
const { scrollPosition, node, root } = internals
return {
setStyle: (style: T) => (styleSignal.value = style),
setStyle: (style: T | undefined) => (styleSignal.value = style),
pixelSize: root.pixelSize,
borderInset: node.borderInset,
paddingInset: node.paddingInset,
Expand Down
Loading

0 comments on commit a0ee4ab

Please sign in to comment.