Implement studio.globalToolbar

This commit is contained in:
Aria Minaei 2021-07-13 16:13:15 +02:00
parent 86547aa4cb
commit 921bc44270
18 changed files with 433 additions and 924 deletions

View file

@ -43,6 +43,7 @@
"@theatre/core": "workspace:*", "@theatre/core": "workspace:*",
"@theatre/studio": "workspace:*", "@theatre/studio": "workspace:*",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"polished": "^4.1.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-icons": "^4.2.0", "react-icons": "^4.2.0",

View file

@ -6,11 +6,10 @@ import {useEditorStore} from '../store'
import {OrbitControls} from '@react-three/drei' import {OrbitControls} from '@react-three/drei'
import shallow from 'zustand/shallow' import shallow from 'zustand/shallow'
import root from 'react-shadow/styled-components' import root from 'react-shadow/styled-components'
import Toolbar from './Toolbar/Toolbar'
import ProxyManager from './ProxyManager' import ProxyManager from './ProxyManager'
import studio from '@theatre/studio' import studio from '@theatre/studio'
import {useVal} from '@theatre/dataverse-react' import {useVal} from '@theatre/dataverse-react'
import styled, {createGlobalStyle} from 'styled-components' import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
const GlobalStyle = createGlobalStyle` const GlobalStyle = createGlobalStyle`
:host { :host {
@ -52,12 +51,13 @@ const EditorScene = () => {
return ( return (
<> <>
{showGrid && <gridHelper args={[1000, 1000, 0x444444, 0x888888]} />} {showGrid && <gridHelper args={[20, 20, '#6e6e6e', '#4a4b4b']} />}
{showAxes && <axesHelper args={[500]} />} {showAxes && <axesHelper args={[500]} />}
{/* @ts-ignore */} {/* @ts-ignore */}
<OrbitControls ref={orbitControlsRef} enableDamping={false} /> <OrbitControls ref={orbitControlsRef} enableDamping={false} />
<primitive object={helpersRoot}></primitive> <primitive object={helpersRoot}></primitive>
<ProxyManager orbitControlsRef={orbitControlsRef} /> <ProxyManager orbitControlsRef={orbitControlsRef} />
<color attach="background" args={[0.24, 0.24, 0.24]} />
</> </>
) )
} }
@ -106,29 +106,36 @@ const Editor: VFC = () => {
return ( return (
<root.div> <root.div>
<GlobalStyle /> <StyleSheetManager disableVendorPrefixes>
<Wrapper id="theatre-plugin-r3f-root" visible={true}> <>
<Toolbar /> <GlobalStyle />
{sceneSnapshot ? ( <Wrapper id="theatre-plugin-r3f-root" visible={true}>
<> {/* <Toolbar /> */}
<CanvasWrapper> {sceneSnapshot ? (
<Canvas <>
// @ts-ignore <CanvasWrapper>
colorManagement <Canvas
camera={initialEditorCamera} // @ts-ignore
onCreated={({gl}) => { colorManagement
gl.setClearColor('white') camera={initialEditorCamera}
}} onCreated={({gl}) => {
shadowMap gl.setClearColor('white')
pixelRatio={window.devicePixelRatio} }}
onPointerMissed={() => studio.__experimental_setSelection([])} shadowMap
> dpr={[1, 2]}
<EditorScene /> fog={'red'}
</Canvas> onPointerMissed={() =>
</CanvasWrapper> studio.__experimental_setSelection([])
</> }
) : null} >
</Wrapper> <EditorScene />
</Canvas>
</CanvasWrapper>
</>
) : null}
</Wrapper>
</>
</StyleSheetManager>
</root.div> </root.div>
) )
} }

View file

@ -1,5 +1,4 @@
import type {VFC} from 'react' import type {VFC} from 'react'
import {useState} from 'react'
import React from 'react' import React from 'react'
import TransformControlsModeSelect from './TransformControlsModeSelect' import TransformControlsModeSelect from './TransformControlsModeSelect'
import {useEditorStore} from '../../store' import {useEditorStore} from '../../store'
@ -13,35 +12,8 @@ import studio from '@theatre/studio'
import {getSelected} from '../useSelected' import {getSelected} from '../useSelected'
import {useVal} from '@theatre/dataverse-react' import {useVal} from '@theatre/dataverse-react'
import IconButton from './utils/IconButton' import IconButton from './utils/IconButton'
import {PortalContext} from 'reakit'
import styled from 'styled-components' import styled from 'styled-components'
const Container = styled.div`
z-index: 50;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
pointer-events: none;
`
const TopRow = styled.div`
position: relative;
margin: 1.25rem;
height: 100%;
display: flex;
flex: 1 1 0%;
justify-content: space-between;
align-items: flex-start;
`
const Tools = styled.div`
display: flex;
gap: 1rem;
`
const ToolGroup = styled.div` const ToolGroup = styled.div`
pointer-events: auto; pointer-events: auto;
` `
@ -59,111 +31,102 @@ const Toolbar: VFC = () => {
const viewportShading = const viewportShading =
useVal(editorObject?.props.viewport.shading) ?? 'rendered' useVal(editorObject?.props.viewport.shading) ?? 'rendered'
const [wrapper, setWrapper] = useState<null | HTMLDivElement>(null)
if (!editorObject) return <></> if (!editorObject) return <></>
return ( return (
<PortalContext.Provider value={wrapper}> <>
<Container ref={setWrapper}> <ToolGroup>
<TopRow> <TransformControlsModeSelect
<Tools> value={transformControlsMode}
<ToolGroup> onChange={(value) =>
<TransformControlsModeSelect studio.transaction(({set}) =>
value={transformControlsMode} set(editorObject!.props.transformControls.mode, value),
onChange={(value) => )
studio.transaction(({set}) => }
set(editorObject!.props.transformControls.mode, value), />
) </ToolGroup>
} <ToolGroup>
/> <TransformControlsSpaceSelect
</ToolGroup> value={transformControlsSpace}
<ToolGroup> onChange={(space) => {
<TransformControlsSpaceSelect studio.transaction(({set}) => {
value={transformControlsSpace} set(editorObject.props.transformControls.space, space)
onChange={(space) => { })
studio.transaction(({set}) => { }}
set(editorObject.props.transformControls.space, space) />
}) </ToolGroup>
}} <ToolGroup>
/> <ViewportShadingSelect
</ToolGroup> value={viewportShading}
<ToolGroup> onChange={(shading) => {
<ViewportShadingSelect studio.transaction(({set}) => {
value={viewportShading} set(editorObject.props.viewport.shading, shading)
onChange={(shading) => { })
studio.transaction(({set}) => { }}
set(editorObject.props.viewport.shading, shading) />
}) </ToolGroup>
}} <ToolGroup>
/> <IconButton
</ToolGroup> label="Focus on selected"
<ToolGroup> icon={<RiFocus3Line />}
<IconButton onClick={() => {
label="Focus on selected" const orbitControls =
icon={<RiFocus3Line />} useEditorStore.getState().orbitControlsRef?.current
onClick={() => { const selected = getSelected()
const orbitControls =
useEditorStore.getState().orbitControlsRef?.current
const selected = getSelected()
let focusObject let focusObject
if (selected) { if (selected) {
focusObject = focusObject =
useEditorStore.getState().editablesSnapshot![selected] useEditorStore.getState().editablesSnapshot![selected]
.proxyObject .proxyObject
} }
if (orbitControls && focusObject) { if (orbitControls && focusObject) {
focusObject.getWorldPosition( focusObject.getWorldPosition(
// @ts-ignore TODO // @ts-ignore TODO
orbitControls.target as Vector3, orbitControls.target as Vector3,
) )
} }
}} }}
/> />
</ToolGroup> </ToolGroup>
<ToolGroup> <ToolGroup>
<IconButton <IconButton
label="Align object to view" label="Align object to view"
icon={<GiPocketBow />} icon={<GiPocketBow />}
onClick={() => { onClick={() => {
const camera = ( const camera = (
useEditorStore.getState().orbitControlsRef useEditorStore.getState().orbitControlsRef?.current as $FixMe
?.current as $FixMe )?.object
)?.object
const selected = getSelected() const selected = getSelected()
let proxyObject let proxyObject
if (selected) { if (selected) {
proxyObject = proxyObject =
useEditorStore.getState().editablesSnapshot![selected] useEditorStore.getState().editablesSnapshot![selected]
.proxyObject .proxyObject
if (proxyObject && camera) { if (proxyObject && camera) {
const direction = new Vector3() const direction = new Vector3()
const position = camera.position.clone() const position = camera.position.clone()
camera.getWorldDirection(direction) camera.getWorldDirection(direction)
proxyObject.position.set(0, 0, 0) proxyObject.position.set(0, 0, 0)
proxyObject.lookAt(direction) proxyObject.lookAt(direction)
proxyObject.parent!.worldToLocal(position) proxyObject.parent!.worldToLocal(position)
proxyObject.position.copy(position) proxyObject.position.copy(position)
proxyObject.updateMatrix() proxyObject.updateMatrix()
} }
} }
}} }}
/> />
</ToolGroup> </ToolGroup>
</Tools> </>
</TopRow>
</Container>
</PortalContext.Provider>
) )
} }

View file

@ -13,42 +13,38 @@ interface OptionButtonProps<Option> {
onClick: () => void onClick: () => void
} }
const _TooltipRef = styled(TooltipReference)<{selected: boolean}>` const TheButton = styled(TooltipReference)<{selected: boolean}>`
display: flex; display: flex;
position: relative; position: relative;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
vertical-align: middle; vertical-align: middle;
width: auto; font-size: 11px;
font-size: 0.875rem; line-height: 1.25em;
line-height: 1.25rem;
font-weight: 600; font-weight: 600;
height: 1.75rem; height: 24px;
padding-left: 0.5rem; padding-left: 0.5em;
padding-right: 0.5rem; padding-right: 0.5em;
border: 0 transparent; border: 1px solid #22222238;
&:first-child { border-radius: 2px;
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
&:last-child {
border-top-right-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
}
&:focus { &:focus {
outline: none; outline: none;
} }
color: ${({selected}) => (selected ? 'white' : 'rgba(55, 65, 81, 1)')}; color: #e6e6e5;
background-color: ${({selected}) => background-color: rgba(0, 0, 0, 0.1);
selected ? 'rgba(6, 95, 70, 1)' : 'rgba(243, 244, 246, 1);'};
&:hover { &:hover {
background-color: ${({selected}) => background-color: rgba(0, 0, 0, 0.5);
selected ? 'rgba(6, 78, 59, 1)' : 'rgba(229, 231, 235, 1);'}; color: white;
}
&.selected,
&.selected:hover {
color: white;
background-color: rgba(0, 0, 0, 0.5);
} }
` `
@ -62,15 +58,16 @@ function OptionButton<Option>({
const tooltip = useTooltipState() const tooltip = useTooltipState()
return ( return (
<> <>
<_TooltipRef <TheButton
{...tooltip} {...tooltip}
forwardedAs={Button} forwardedAs={Button}
selected={option === value} selected={option === value}
className={option === value ? 'selected' : undefined}
aria-label={label} aria-label={label}
onClick={onClick} onClick={onClick}
> >
{icon} {icon}
</_TooltipRef> </TheButton>
<Tooltip {...tooltip}>{label}</Tooltip> <Tooltip {...tooltip}>{label}</Tooltip>
</> </>
) )
@ -89,6 +86,7 @@ interface CompactModeSelectProps<Option> {
const Container = styled(Group)` const Container = styled(Group)`
display: flex; display: flex;
gap: 2px;
` `
const CompactModeSelect = <Option extends string | number>({ const CompactModeSelect = <Option extends string | number>({

View file

@ -5,45 +5,48 @@ import {Button} from 'reakit'
import type {IconType} from 'react-icons' import type {IconType} from 'react-icons'
import {Tooltip, TooltipReference, useTooltipState} from './Tooltip' import {Tooltip, TooltipReference, useTooltipState} from './Tooltip'
import styled from 'styled-components' import styled from 'styled-components'
import {transparentize} from 'polished'
export interface IconButtonProps extends Exclude<ButtonProps, 'children'> { export interface IconButtonProps extends Exclude<ButtonProps, 'children'> {
icon: ReactElement<IconType> icon: ReactElement<IconType>
label: string label: string
} }
const _TooltipRef = styled(TooltipReference)` const TheButton = styled(TooltipReference)`
display: flex; display: flex;
position: relative; position: relative;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
vertical-align: middle; vertical-align: middle;
width: auto; font-size: 11px;
font-size: 0.875rem; line-height: 1.25em;
line-height: 1.25rem;
font-weight: 600; font-weight: 600;
height: 1.75rem; height: 24px;
padding-left: 0.5rem; padding-left: 0.5em;
padding-right: 0.5rem; padding-right: 0.5em;
color: #e6e6e5;
background-color: #313131ba;
border: 0 transparent;
&:first-child { &:first-child {
border-top-left-radius: 0.25rem; border-top-left-radius: 3px;
border-bottom-left-radius: 0.25rem; border-bottom-left-radius: 3px;
} }
&:last-child { &:last-child {
border-top-right-radius: 0.25rem; border-top-right-radius: 3px;
border-bottom-right-radius: 0.25rem; border-bottom-right-radius: 3px;
} }
&:focus { &:focus {
outline: none; outline: none;
} }
color: rgba(55, 65, 81, 1); color: #e6e6e5;
background-color: rgba(243, 244, 246, 1); background-color: #313131;
&:hover { &:hover {
background-color: rgba(229, 231, 235, 1); background-color: ${transparentize(0.5, '#313131')};
} }
border: 0 transparent; border: 0 transparent;
@ -53,14 +56,14 @@ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
const tooltip = useTooltipState() const tooltip = useTooltipState()
return ( return (
<> <>
<_TooltipRef <TheButton
{...props} {...props}
{...tooltip} {...tooltip}
forwardedAs={Button} forwardedAs={Button}
aria-label={label} aria-label={label}
> >
{icon} {icon}
</_TooltipRef> </TheButton>
<Tooltip {...tooltip}>{label}</Tooltip> <Tooltip {...tooltip}>{label}</Tooltip>
</> </>
) )

View file

@ -1,24 +1,22 @@
import type {VFC} from 'react' import type {VFC} from 'react'
import React from 'react' import React from 'react'
import {Tooltip as TooltipImpl, TooltipReference, useTooltipState} from 'reakit' import {Tooltip as TooltipImpl, TooltipReference, useTooltipState} from 'reakit'
import {transparentize} from 'polished'
import type {TooltipProps} from 'reakit' import type {TooltipProps} from 'reakit'
import styled from 'styled-components' import styled from 'styled-components'
export {TooltipReference, useTooltipState} export {TooltipReference, useTooltipState}
const Container = styled(TooltipImpl)` const Container = styled(TooltipImpl)`
padding-left: 0.5rem; padding: 3px 5px;
padding-right: 0.5rem;
padding-top: 0.25rem;
padding-bottom: 0.25rem;
font-size: 0.875rem; font-size: 11px;
line-height: 1.25rem; line-height: 1.25em;
border-radius: 0.125rem; border-radius: 2px;
background-color: rgba(55, 65, 81, 1); background-color: ${transparentize(0.5, '#313131')};
color: white; color: white;
pointer-events: none; pointer-events: none;
font-weight: 500;
` `
export const Tooltip: VFC<TooltipProps> = ({className, ...props}) => ( export const Tooltip: VFC<TooltipProps> = ({className, ...props}) => (

View file

@ -8,8 +8,16 @@ export type {EditorHelperProps} from './components/EditorHelper'
export {default as editable} from './components/editable' export {default as editable} from './components/editable'
export {bindToCanvas} from './store' export {bindToCanvas} from './store'
export type {EditableState, BindFunction} from './store' export type {EditableState, BindFunction} from './store'
import studio from '@theatre/studio'
import Toolbar from './components/Toolbar/Toolbar'
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
studio.extend({
id: '@theatre/plugin-r3f',
globalToolbar: {
component: Toolbar,
},
})
const editorRoot = document.createElement('div') const editorRoot = document.createElement('div')
document.body.appendChild(editorRoot) document.body.appendChild(editorRoot)

View file

@ -306,6 +306,23 @@ const editorSheetObjectConfig = types.compound({
}, },
{as: 'menu', label: 'Shading'}, {as: 'menu', label: 'Shading'},
), ),
mode: types.stringLiteral(
'translate',
{
translate: 'Translate',
rotate: 'Rotate',
scale: 'Scale',
},
{as: 'switch', label: 'Mode'},
),
space: types.stringLiteral(
'world',
{
local: 'Local',
world: 'World',
},
{as: 'switch', label: 'Space'},
),
}, },
{label: 'Viewport Config'}, {label: 'Viewport Config'},
), ),

View file

@ -124,6 +124,25 @@ export const compound = <Props extends IValidCompoundProps>(
} }
} }
export interface PropTypeConfig_CSSRGBA
extends IBasePropType<{r: number; g: number; b: number; a: number}> {
type: 'cssrgba'
default: {r: number; g: number; b: number; a: number}
}
export const rgba = (
defaultValue: {r: number; b: number; g: number; a: number},
extras?: PropTypeConfigExtras,
): PropTypeConfig_CSSRGBA => {
return {
type: 'cssrgba',
valueType: null as $IntentionalAny,
[s]: 'TheatrePropType',
label: extras?.label,
default: defaultValue,
}
}
export interface PropTypeConfig_Enum extends IBasePropType<{}> { export interface PropTypeConfig_Enum extends IBasePropType<{}> {
type: 'enum' type: 'enum'
cases: Record<string, PropTypeConfig> cases: Record<string, PropTypeConfig>
@ -135,6 +154,7 @@ export type PropTypeConfig_AllPrimitives =
| PropTypeConfig_Boolean | PropTypeConfig_Boolean
| PropTypeConfig_String | PropTypeConfig_String
| PropTypeConfig_StringLiteral<$IntentionalAny> | PropTypeConfig_StringLiteral<$IntentionalAny>
| PropTypeConfig_CSSRGBA
export type PropTypeConfig = export type PropTypeConfig =
| PropTypeConfig_AllPrimitives | PropTypeConfig_AllPrimitives

View file

@ -63,7 +63,6 @@
"json-touch-patch": "^0.11.2", "json-touch-patch": "^0.11.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"microbundle": "^0.13.0",
"nanoid": "^3.1.23", "nanoid": "^3.1.23",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"null-loader": "^4.0.1", "null-loader": "^4.0.1",
@ -77,6 +76,7 @@
"react-shadow": "^19.0.2", "react-shadow": "^19.0.2",
"react-use": "^17.2.4", "react-use": "^17.2.4",
"react-use-gesture": "^9.1.3", "react-use-gesture": "^9.1.3",
"reakit": "^1.3.8",
"redux": "^3.7.2", "redux": "^3.7.2",
"redux-actions": "^2.6.5", "redux-actions": "^2.6.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",

View file

@ -9,7 +9,7 @@ import type {
ITransactionPrivateApi, ITransactionPrivateApi,
} from './StudioStore/StudioStore' } from './StudioStore/StudioStore'
import StudioStore from './StudioStore/StudioStore' import StudioStore from './StudioStore/StudioStore'
import type {IStudio} from './TheatreStudio' import type {IExtension, IStudio} from './TheatreStudio'
import TheatreStudio from './TheatreStudio' import TheatreStudio from './TheatreStudio'
import {nanoid} from 'nanoid/non-secure' import {nanoid} from 'nanoid/non-secure'
import type Project from '@theatre/core/projects/Project' import type Project from '@theatre/core/projects/Project'
@ -30,6 +30,11 @@ export class Studio {
private readonly _store = new StudioStore() private readonly _store = new StudioStore()
private _corePrivateApi: typeof privateAPI | undefined private _corePrivateApi: typeof privateAPI | undefined
private _extensions: Atom<{byId: Record<string, IExtension>}> = new Atom({
byId: {},
})
readonly extensionsP = this._extensions.pointer.byId
constructor() { constructor() {
this.address = {studioId: nanoid(10)} this.address = {studioId: nanoid(10)}
this.publicApi = new TheatreStudio(this) this.publicApi = new TheatreStudio(this)
@ -90,4 +95,22 @@ export class Studio {
get corePrivateAPI() { get corePrivateAPI() {
return this._corePrivateApi return this._corePrivateApi
} }
extend(extension: IExtension) {
if (!extension || typeof extension !== 'object') {
throw new Error(`Extensions must be JS objects`)
}
if (typeof extension.id !== 'string') {
throw new Error(`extension.id must be a string`)
}
if (this._extensions.getState().byId[extension.id]) {
throw new Error(
`An extension with the id of ${extension.id} already exists`,
)
}
this._extensions.setIn(['byId', extension.id], extension)
}
} }

View file

@ -3,7 +3,7 @@ import studioTicker from '@theatre/studio/studioTicker'
import type {IDerivation, Pointer} from '@theatre/dataverse' import type {IDerivation, Pointer} from '@theatre/dataverse'
import {prism} from '@theatre/dataverse' import {prism} from '@theatre/dataverse'
import SimpleCache from '@theatre/shared/utils/SimpleCache' import SimpleCache from '@theatre/shared/utils/SimpleCache'
import type {VoidFn} from '@theatre/shared/utils/types' import type {$IntentionalAny, VoidFn} from '@theatre/shared/utils/types'
import type {IScrub} from '@theatre/studio/Scrub' import type {IScrub} from '@theatre/studio/Scrub'
import type {Studio} from '@theatre/studio/Studio' import type {Studio} from '@theatre/studio/Studio'
@ -11,12 +11,39 @@ import {isSheetObjectPublicAPI} from '@theatre/shared/instanceTypes'
import {getOutlineSelection} from './selectors' import {getOutlineSelection} from './selectors'
import type SheetObject from '@theatre/core/sheetObjects/SheetObject' import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
import getStudio from './getStudio' import getStudio from './getStudio'
import type React from 'react'
import type {
PropTypeConfig_Boolean,
PropTypeConfig_Compound,
} from '@theatre/core/propTypes'
export interface ITransactionAPI { export interface ITransactionAPI {
set<V>(pointer: Pointer<V>, value: V): void set<V>(pointer: Pointer<V>, value: V): void
unset<V>(pointer: Pointer<V>): void unset<V>(pointer: Pointer<V>): void
} }
export interface IPanelType<DataType extends PropTypeConfig_Compound<{}>> {
sheetName: string
dataType: DataType
component: React.ComponentType<{
id: string
object: ISheetObject<
PropTypeConfig_Compound<{
visible: PropTypeConfig_Boolean
data: DataType
}>
>
}>
}
export type IExtension = {
id: string
globalToolbar?: {
component: React.ComponentType<{}>
}
panes?: Record<string, IPanelType<$IntentionalAny>>
}
export interface IStudio { export interface IStudio {
readonly ui: { readonly ui: {
show(): void show(): void
@ -34,6 +61,8 @@ export interface IStudio {
): VoidFunction ): VoidFunction
readonly selection: Array<ISheetObject> readonly selection: Array<ISheetObject>
extend(extension: IExtension): void
} }
export default class TheatreStudio implements IStudio { export default class TheatreStudio implements IStudio {
@ -62,6 +91,10 @@ export default class TheatreStudio implements IStudio {
*/ */
constructor(internals: Studio) {} constructor(internals: Studio) {}
extend(extension: IExtension): void {
getStudio().extend(extension)
}
transaction(fn: (api: ITransactionAPI) => void): void { transaction(fn: (api: ITransactionAPI) => void): void {
return getStudio().transaction(({set, unset}) => { return getStudio().transaction(({set, unset}) => {
return fn({set, unset}) return fn({set, unset})

View file

@ -1,4 +1,4 @@
import UIRootWrapper from '@theatre/studio/UIRoot/UIRootWrapper' import UIRoot from '@theatre/studio/UIRoot/UIRoot'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
@ -54,10 +54,7 @@ export default class UI {
this._renderTimeout = undefined this._renderTimeout = undefined
this._documentBodyUIIsRenderedIn = document.body this._documentBodyUIIsRenderedIn = document.body
this._documentBodyUIIsRenderedIn.appendChild(this.containerEl) this._documentBodyUIIsRenderedIn.appendChild(this.containerEl)
ReactDOM.render( ReactDOM.render(React.createElement(UIRoot), this.containerShadow)
React.createElement(UIRootWrapper, {studio: this.studio}),
this.containerShadow,
)
} }
this._renderTimeout = setTimeout(renderCallback, 10) this._renderTimeout = setTimeout(renderCallback, 10)
} }

View file

@ -1,13 +1,15 @@
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import type {Studio} from '@theatre/studio/Studio'
import {usePrism} from '@theatre/dataverse-react' import {usePrism} from '@theatre/dataverse-react'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import React from 'react' import React from 'react'
import {createGlobalStyle, StyleSheetManager} from 'styled-components' import styled, {createGlobalStyle, StyleSheetManager} from 'styled-components'
import EnsureProjectsDontHaveErrors from './EnsureProjectsDontHaveErrors'
import PanelsRoot from './PanelsRoot' import PanelsRoot from './PanelsRoot'
import ProvideTheme from './ProvideTheme' import ProvideTheme from './ProvideTheme'
import TheTrigger from './TheTrigger' import TheTrigger from './TheTrigger'
import GlobalToolbar from '@theatre/studio/toolbars/GlobalToolbar/GlobalToolbar'
import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {PortalContext} from 'reakit'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
const GlobalStyle = createGlobalStyle` const GlobalStyle = createGlobalStyle`
:host { :host {
@ -28,13 +30,28 @@ const GlobalStyle = createGlobalStyle`
} }
` `
export default function UIRoot({studio}: {studio: Studio}) { const Container = styled.div`
z-index: 50;
position: fixed;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
pointer-events: none;
`
export default function UIRoot() {
const studio = getStudio()
const [containerRef, container] = useRefAndState<HTMLDivElement>(
undefined as $IntentionalAny,
)
const inside = usePrism(() => { const inside = usePrism(() => {
const visiblityState = val(studio.atomP.ahistoric.visibilityState) const visiblityState = val(studio.atomP.ahistoric.visibilityState)
const initialised = val(studio.atomP.ephemeral.initialised) const initialised = val(studio.atomP.ephemeral.initialised)
const shouldShowTrigger = visiblityState === 'onlyTriggerIsVisible' const shouldShowTrigger = visiblityState === 'onlyTriggerIsVisible'
const shouldShowPanels = visiblityState === 'everythingIsVisible' const shouldShowPanels = visiblityState === 'everythingIsVisible'
const shouldShowGlobalToolbar = visiblityState !== 'everythingIsHidden'
return !initialised ? null : ( return !initialised ? null : (
<StyleSheetManager <StyleSheetManager
@ -44,13 +61,18 @@ export default function UIRoot({studio}: {studio: Studio}) {
<> <>
<GlobalStyle /> <GlobalStyle />
<ProvideTheme> <ProvideTheme>
{shouldShowTrigger && <TheTrigger />} <PortalContext.Provider value={container}>
{shouldShowPanels && <PanelsRoot />} <Container ref={containerRef}>
{shouldShowGlobalToolbar && <GlobalToolbar />}
{shouldShowTrigger && <TheTrigger />}
{shouldShowPanels && <PanelsRoot />}
</Container>
</PortalContext.Provider>
</ProvideTheme> </ProvideTheme>
</> </>
</StyleSheetManager> </StyleSheetManager>
) )
}, [studio]) }, [studio, containerRef, container])
return <EnsureProjectsDontHaveErrors>{inside}</EnsureProjectsDontHaveErrors> return inside
} }

View file

@ -1,28 +0,0 @@
import type {Studio} from '@theatre/studio/Studio'
import React from 'react'
import UIRoot from './UIRoot'
export default class UIRootWrapper extends React.Component<{
studio: Studio
}> {
state = {UIRoot}
componentDidMount() {
const self = this
if (
process.env.NODE_ENV !== 'production' &&
typeof module === 'object' &&
module &&
module.hot
) {
module.hot.accept('./UIRoot', () => {
const UIRoot = require('./UIRoot').default
self.setState({UIRoot})
})
}
}
render() {
const UIRoot = this.state.UIRoot
const rootEl = <UIRoot studio={this.props.studio} />
return rootEl
}
}

View file

@ -68,6 +68,7 @@ const propEditorByPropType: {
enum: () => <></>, enum: () => <></>,
boolean: BooleanPropEditor, boolean: BooleanPropEditor,
stringLiteral: StringLiteralPropEditor, stringLiteral: StringLiteralPropEditor,
cssrgba: () => <></>,
} }
const DeterminePropEditor: React.FC<{ const DeterminePropEditor: React.FC<{

View file

@ -0,0 +1,35 @@
import {useVal} from '@theatre/dataverse-react'
import getStudio from '@theatre/studio/getStudio'
import React from 'react'
import styled from 'styled-components'
const Container = styled.div`
position: fixed;
z-index: 50;
top: 12px;
right: 12px;
left: 12px;
pointer-events: none;
display: flex;
gap: 1rem;
`
const GlobalToolbar: React.FC<{}> = (props) => {
const groups: Array<React.ReactNode> = []
const extensions = useVal(getStudio().extensionsP)
for (const [, extension] of Object.entries(extensions)) {
if (extension.globalToolbar) {
groups.push(
<extension.globalToolbar.component
key={'extensionToolbar-' + extension.id}
/>,
)
}
}
return <Container>{groups}</Container>
}
export default GlobalToolbar

781
yarn.lock

File diff suppressed because it is too large Load diff