Make studio.ui available before the UI is loaded

This was a regression introduced by b83164f26f
This commit is contained in:
Aria Minaei 2023-07-17 12:25:20 +02:00 committed by Aria
parent bcfb91fbb7
commit 35fe1c375c
5 changed files with 104 additions and 67 deletions

View file

@ -0,0 +1,11 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Theatre.js Playground</title>
<script src="./index.tsx" type="module"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -1,6 +1,6 @@
import Scrub from '@theatre/studio/Scrub'
import type {StudioHistoricState} from '@theatre/studio/store/types/historic'
import type UI from '@theatre/studio/UI'
import UI from '@theatre/studio/UI/UI'
import type {Pointer, Ticker} from '@theatre/dataverse'
import {Atom, PointerProxy, pointerToPrism} from '@theatre/dataverse'
import type {
@ -32,9 +32,6 @@ const DEFAULT_PERSISTENCE_KEY = 'theatre-0.4'
export type CoreExports = typeof _coreExports
const UIConstructorModule =
typeof window !== 'undefined' ? import('./UI').then((M) => M.default) : null
const STUDIO_NOT_INITIALIZED_MESSAGE = `You seem to have imported '@theatre/studio' but haven't initialized it. You can initialize the studio by:
\`\`\`
import studio from '@theatre/studio'
@ -64,16 +61,9 @@ studio.initialize()
`
export class Studio {
protected _ui: UI | null = null
readonly ui: UI
// this._uiInitDeferred.promise will resolve once this._ui is set
private _uiInitDeferred = defer()
get ui() {
if (!this._ui) {
debugger
throw new Error(`Studio.ui called before UI is initialized`)
}
return this._ui
}
readonly publicApi: IStudio
readonly address: {studioId: string}
readonly _projectsProxy: PointerProxy<Record<ProjectId, Project>> =
@ -130,17 +120,7 @@ export class Studio {
this.address = {studioId: nanoid(10)}
this.publicApi = new TheatreStudio(this)
// initialize UI if we're in the browser
if (process.env.NODE_ENV !== 'test' && typeof window !== 'undefined') {
UIConstructorModule!
.then((M) => {
this._ui = new M(this)
this._uiInitDeferred.resolve(null)
})
.catch((error) => {
console.error(`Failed initializing the UI at @theatre/studio.`, error)
})
}
this.ui = new UI(this)
this._attachToIncomingProjects()
this.paneManager = new PaneManager(this)
@ -215,13 +195,15 @@ export class Studio {
return
}
if (process.env.NODE_ENV !== 'test' && typeof window !== 'undefined') {
await this.ui.ready
}
this._initializedDeferred.resolve()
if (process.env.NODE_ENV !== 'test') {
this._uiInitDeferred.promise.then(() => {
this.ui.render()
checkForUpdates()
})
this.ui.render()
checkForUpdates()
}
}

View file

@ -0,0 +1,70 @@
import type {Studio} from '@theatre/studio/Studio'
import {val} from '@theatre/dataverse'
const NonSSRBitsClass =
typeof window !== 'undefined'
? import('./UINonSSRBits').then((M) => M.default)
: null
export default class UI {
private _rendered = false
private _nonSSRBits = NonSSRBitsClass
? NonSSRBitsClass.then((NonSSRBitsClass) => new NonSSRBitsClass())
: Promise.reject()
readonly ready: Promise<void> = this._nonSSRBits.then(
() => undefined,
() => undefined,
)
constructor(readonly studio: Studio) {}
render() {
if (this._rendered) {
return
}
this._rendered = true
this._nonSSRBits.then((b) => {
b.render()
})
}
hide() {
this.studio.transaction(({drafts}) => {
drafts.ahistoric.visibilityState = 'everythingIsHidden'
})
}
restore() {
this.render()
this.studio.transaction(({drafts}) => {
drafts.ahistoric.visibilityState = 'everythingIsVisible'
})
}
get isHidden() {
return (
val(this.studio.atomP.ahistoric.visibilityState) === 'everythingIsHidden'
)
}
renderToolset(toolsetId: string, htmlNode: HTMLElement) {
let shouldUnmount = false
let unmount: null | (() => void) = null
this._nonSSRBits.then((nonSSRBits) => {
if (shouldUnmount) return // unmount requested before the toolset is mounted, so, abort
unmount = nonSSRBits.renderToolset(toolsetId, htmlNode)
})
return () => {
if (unmount) {
unmount()
return
}
if (shouldUnmount) return
shouldUnmount = true
}
}
}

View file

@ -2,20 +2,17 @@ import UIRoot from '@theatre/studio/UIRoot/UIRoot'
import type {$IntentionalAny} from '@theatre/shared/utils/types'
import React from 'react'
import ReactDOM from 'react-dom'
import type {Studio} from './Studio'
import {val} from '@theatre/dataverse'
import {getMounter} from './utils/renderInPortalInContext'
import {withStyledShadow} from './css'
import ExtensionToolbar from './toolbars/ExtensionToolbar/ExtensionToolbar'
import {getMounter} from '@theatre/studio/utils/renderInPortalInContext'
import {withStyledShadow} from '@theatre/studio/css'
import ExtensionToolbar from '@theatre/studio/toolbars/ExtensionToolbar/ExtensionToolbar'
export default class UI {
export default class UINonSSRBits {
readonly containerEl = document.createElement('div')
private _rendered = false
private _renderTimeout: NodeJS.Timer | undefined = undefined
private _documentBodyUIIsRenderedIn: HTMLElement | undefined = undefined
readonly containerShadow: ShadowRoot & HTMLElement
constructor(readonly studio: Studio) {
constructor() {
// @todo we can't bootstrap Theatre.js (as in, to design Theatre.js using theatre), if we rely on IDed elements
this.containerEl.id = 'theatrejs-studio-root'
@ -49,15 +46,6 @@ export default class UI {
}
render() {
if (this._rendered) {
return
}
this._rendered = true
this._render()
}
protected _render() {
const renderCallback = () => {
if (!document.body) {
this._renderTimeout = setTimeout(renderCallback, 5)
@ -66,30 +54,14 @@ export default class UI {
this._renderTimeout = undefined
this._documentBodyUIIsRenderedIn = document.body
this._documentBodyUIIsRenderedIn.appendChild(this.containerEl)
ReactDOM.render(React.createElement(UIRoot), this.containerShadow)
ReactDOM.render(
React.createElement(UIRoot, {containerShadow: this.containerShadow}),
this.containerShadow,
)
}
this._renderTimeout = setTimeout(renderCallback, 10)
}
hide() {
this.studio.transaction(({drafts}) => {
drafts.ahistoric.visibilityState = 'everythingIsHidden'
})
}
restore() {
this.render()
this.studio.transaction(({drafts}) => {
drafts.ahistoric.visibilityState = 'everythingIsVisible'
})
}
get isHidden() {
return (
val(this.studio.atomP.ahistoric.visibilityState) === 'everythingIsHidden'
)
}
renderToolset(toolsetId: string, htmlNode: HTMLElement) {
const s = getMounter()

View file

@ -46,7 +46,9 @@ const INTERNAL_LOGGING = /Playground.+Theatre\.js/.test(
(typeof document !== 'undefined' ? document?.title : null) ?? '',
)
export default function UIRoot() {
export default function UIRoot(props: {
containerShadow: ShadowRoot & HTMLElement
}) {
const studio = getStudio()
const [portalLayerRef, portalLayer] = useRefAndState<HTMLDivElement>(
undefined as $IntentionalAny,
@ -87,7 +89,7 @@ export default function UIRoot() {
target={
window.__IS_VISUAL_REGRESSION_TESTING === true
? undefined
: getStudio()!.ui.containerShadow
: props.containerShadow
}
>
<>