-
-
Notifications
You must be signed in to change notification settings - Fork 180
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: handle async application initialisation
- Loading branch information
Showing
13 changed files
with
310 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { Application } from 'pixi.js'; | ||
import { createElement } from 'react'; | ||
import { ConcurrentRoot } from 'react-reconciler/constants.js'; | ||
import { ContextProvider } from '../components/Context.js'; | ||
import { isReadOnlyProperty } from '../helpers/isReadOnlyProperty.js'; | ||
import { log } from '../helpers/log.js'; | ||
import { prepareInstance } from '../helpers/prepareInstance.js'; | ||
import { reconciler } from './reconciler.js'; | ||
import { roots } from './roots.js'; | ||
|
||
/** @typedef {import('pixi.js').ApplicationOptions} ApplicationOptions */ | ||
|
||
/** @typedef {import('../typedefs/InternalState.js').InternalState} InternalState */ | ||
/** @typedef {import('../typedefs/Root.js').Root} Root */ | ||
|
||
/** | ||
* Creates a new root for a Pixi React app. | ||
* | ||
* @param {HTMLElement | HTMLCanvasElement} target The target element into which the Pixi application will be rendered. Can be any element, but if a <canvas> is passed the application will be rendered to it directly. | ||
* @param {Partial<InternalState>} [options] | ||
* @param {(app: Application) => void} [onInit] Callback to be fired when the application finishes initializing. | ||
* @returns {Root} | ||
*/ | ||
export function createRoot(target, options = {}, onInit) | ||
{ | ||
// Check against mistaken use of createRoot | ||
let root = roots.get(target); | ||
|
||
const state = /** @type {InternalState} */ (Object.assign((root?.state ?? {}), options)); | ||
|
||
if (root) | ||
{ | ||
log('warn', 'createRoot should only be called once!'); | ||
} | ||
else | ||
{ | ||
state.app = new Application(); | ||
state.rootContainer = prepareInstance(state.app.stage); | ||
} | ||
|
||
const fiber = root?.fiber ?? reconciler.createContainer( | ||
state.rootContainer, | ||
ConcurrentRoot, | ||
null, | ||
false, | ||
null, | ||
'', | ||
console.error, | ||
null, | ||
); | ||
|
||
if (!root) | ||
{ | ||
let canvas; | ||
|
||
if (target instanceof HTMLCanvasElement) | ||
{ | ||
canvas = target; | ||
} | ||
|
||
if (!canvas) | ||
{ | ||
canvas = document.createElement('canvas'); | ||
target.innerHTML = ''; | ||
target.appendChild(canvas); | ||
} | ||
|
||
/** | ||
* @param {import('react').ReactNode} children | ||
* @param {ApplicationOptions} applicationOptions | ||
* @returns {Promise<Application>} | ||
*/ | ||
const render = async (children, applicationOptions) => | ||
{ | ||
if (!state.app.renderer && !state.isInitialising) | ||
{ | ||
state.isInitialising = true; | ||
await state.app.init({ | ||
...applicationOptions, | ||
canvas, | ||
}); | ||
onInit?.(state.app); | ||
state.isInitialising = false; | ||
} | ||
|
||
Object.entries(applicationOptions).forEach(([key, value]) => | ||
{ | ||
const typedKey = /** @type {keyof ApplicationOptions} */ (key); | ||
|
||
if (isReadOnlyProperty(applicationOptions, typedKey)) | ||
{ | ||
return; | ||
} | ||
|
||
// @ts-expect-error Typescript doesn't realise it, but we're already verifying that this isn't a readonly key. | ||
state.app[typedKey] = value; | ||
}); | ||
|
||
// Update fiber and expose Pixi.js state to children | ||
reconciler.updateContainer( | ||
createElement(ContextProvider, { value: state }, children), | ||
fiber, | ||
null, | ||
() => undefined | ||
); | ||
|
||
return state.app; | ||
}; | ||
|
||
root = { | ||
fiber, | ||
render, | ||
state, | ||
}; | ||
|
||
roots.set(canvas, root); | ||
} | ||
|
||
return root; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/* eslint-disable no-empty-function */ | ||
|
||
import Reconciler from 'react-reconciler'; | ||
import { afterActiveInstanceBlur } from '../helpers/afterActiveInstanceBlur.js'; | ||
import { appendChild } from '../helpers/appendChild.js'; | ||
import { beforeActiveInstanceBlur } from '../helpers/beforeActiveInstanceBlur.js'; | ||
import { clearContainer } from '../helpers/clearContainer.js'; | ||
import { commitUpdate } from '../helpers/commitUpdate.js'; | ||
import { createInstance } from '../helpers/createInstance.js'; | ||
import { createTextInstance } from '../helpers/createTextInstance.js'; | ||
import { detachDeletedInstance } from '../helpers/detachDeletedInstance.js'; | ||
import { finalizeInitialChildren } from '../helpers/finalizeInitialChildren.js'; | ||
import { getChildHostContext } from '../helpers/getChildHostContext.js'; | ||
import { getCurrentEventPriority } from '../helpers/getCurrentEventPriority.js'; | ||
import { getInstanceFromNode } from '../helpers/getInstanceFromNode.js'; | ||
import { getInstanceFromScope } from '../helpers/getInstanceFromScope.js'; | ||
import { getPublicInstance } from '../helpers/getPublicInstance.js'; | ||
import { getRootHostContext } from '../helpers/getRootHostContext.js'; | ||
import { insertBefore } from '../helpers/insertBefore.js'; | ||
import { prepareForCommit } from '../helpers/prepareForCommit.js'; | ||
import { preparePortalMount } from '../helpers/preparePortalMount.js'; | ||
import { prepareScopeUpdate } from '../helpers/prepareScopeUpdate.js'; | ||
import { prepareUpdate } from '../helpers/prepareUpdate.js'; | ||
import { removeChild } from '../helpers/removeChild.js'; | ||
import { resetAfterCommit } from '../helpers/resetAfterCommit.js'; | ||
import { shouldSetTextContent } from '../helpers/shouldSetTextContent.js'; | ||
|
||
/** @typedef {import('../typedefs/HostConfig.js').HostConfig} HostConfig */ | ||
/** @typedef {import('../typedefs/Instance.js').Instance} Instance */ | ||
|
||
/** | ||
* @type {Reconciler.HostConfig< | ||
* HostConfig['type'], | ||
* HostConfig['props'], | ||
* HostConfig['container'], | ||
* HostConfig['instance'], | ||
* HostConfig['textInstance'], | ||
* HostConfig['suspenseInstance'], | ||
* HostConfig['hydratableInstance'], | ||
* HostConfig['publicInstance'], | ||
* HostConfig['hostContext'], | ||
* HostConfig['updatePayload'], | ||
* HostConfig['childSet'], | ||
* HostConfig['timeoutHandle'], | ||
* HostConfig['noTimeout'] | ||
* >} | ||
*/ | ||
const reconcilerConfig = { | ||
isPrimaryRenderer: false, | ||
noTimeout: -1, | ||
supportsHydration: false, | ||
supportsMutation: true, | ||
supportsPersistence: false, | ||
|
||
afterActiveInstanceBlur, | ||
appendChild, | ||
appendChildToContainer: appendChild, | ||
appendInitialChild: appendChild, | ||
beforeActiveInstanceBlur, | ||
cancelTimeout: clearTimeout, | ||
clearContainer, | ||
commitUpdate, | ||
createInstance, | ||
createTextInstance, | ||
detachDeletedInstance, | ||
finalizeInitialChildren, | ||
getChildHostContext, | ||
getCurrentEventPriority, | ||
getInstanceFromNode, | ||
getInstanceFromScope, | ||
getPublicInstance, | ||
getRootHostContext, | ||
insertBefore, | ||
insertInContainerBefore: insertBefore, | ||
prepareForCommit, | ||
preparePortalMount, | ||
prepareScopeUpdate, | ||
prepareUpdate, | ||
removeChild, | ||
removeChildFromContainer: removeChild, | ||
resetAfterCommit, | ||
scheduleTimeout: setTimeout, | ||
shouldSetTextContent, | ||
}; | ||
|
||
export const reconciler = Reconciler(reconcilerConfig); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/** | ||
* We store roots here since we can render to multiple canvases | ||
* | ||
* @type {Map<HTMLElement, import('../typedefs/Root.js').Root>} | ||
*/ | ||
export const roots = new Map(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** | ||
* @param {Record<any, any>} objectInstance | ||
* @param {string} propertyKey | ||
* @returns {boolean} | ||
*/ | ||
export function isReadOnlyProperty(objectInstance, propertyKey) | ||
{ | ||
const prototype = Object.getPrototypeOf(objectInstance); | ||
const propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, propertyKey); | ||
|
||
return !(typeof propertyDescriptor === 'undefined' || propertyDescriptor.set); | ||
} |
Oops, something went wrong.