-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(core) Support multiple event managers (multi-canvas prep) #10375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,7 @@ | |
| // Copyright (c) vis.gl contributors | ||
|
|
||
| import LayerManager from './layer-manager'; | ||
| import ViewManager from './view-manager'; | ||
| import ViewManager, {DEFAULT_CANVAS_ID} from './view-manager'; | ||
| import MapView from '../views/map-view'; | ||
| import EffectManager from './effect-manager'; | ||
| import DeckRenderer from './deck-renderer'; | ||
|
|
@@ -317,6 +317,7 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
| protected deckRenderer: DeckRenderer | null = null; | ||
| protected deckPicker: DeckPicker | null = null; | ||
| protected eventManager: EventManager | null = null; | ||
| protected eventManagers: Record<string, EventManager> = {}; | ||
| protected widgetManager: WidgetManager | null = null; | ||
| protected tooltip: TooltipWidget | null = null; | ||
| protected animationLoop: AnimationLoop | null = null; | ||
|
|
@@ -465,6 +466,7 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
|
|
||
| this.eventManager?.destroy(); | ||
| this.eventManager = null; | ||
| this.eventManagers = {}; | ||
|
|
||
| this.widgetManager?.finalize(); | ||
| this.widgetManager = null; | ||
|
|
@@ -509,12 +511,14 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
| height: number; | ||
| views: View[]; | ||
| viewState: ViewStateObject<ViewsT> | null; | ||
| eventManagers: Record<string, EventManager>; | ||
| } = Object.create(this.props); | ||
| Object.assign(resolvedProps, { | ||
| views: this._getViews(), | ||
| width: this.width, | ||
| height: this.height, | ||
| viewState: this._getViewState() | ||
| viewState: this._getViewState(), | ||
| eventManagers: this.eventManagers | ||
| }); | ||
|
|
||
| if (props.device && props.device.id !== this.device?.id) { | ||
|
|
@@ -526,6 +530,8 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
| // DOM here might be a bit unexpected but it should be ok for most users. | ||
| this.canvas?.remove(); | ||
| this.eventManager?.destroy(); | ||
| this.eventManager = null; | ||
| this.eventManagers = {}; | ||
|
|
||
| // ensure we will re-attach ourselves after createDevice callbacks | ||
| this.canvas = null; | ||
|
|
@@ -658,6 +664,18 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
| return this.canvas; | ||
| } | ||
|
|
||
| /** | ||
| * Get the event manager associated with a view or the default Deck canvas. | ||
| */ | ||
| getEventManager(viewId?: string): EventManager | null { | ||
| if (!viewId || !this.viewManager) { | ||
| return this.eventManager; | ||
| } | ||
|
|
||
| const canvasId = this.viewManager.getCanvasId(viewId) || DEFAULT_CANVAS_ID; | ||
| return this.eventManagers[canvasId] || this.eventManager; | ||
| } | ||
|
|
||
| /** Query the object rendered on top at a given point */ | ||
| async pickObjectAsync(opts: { | ||
| /** x position in pixels */ | ||
|
|
@@ -1046,6 +1064,35 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
| return canvas; | ||
| } | ||
|
|
||
| private _createEventManager(root: HTMLElement): EventManager { | ||
| const eventManager = new EventManager(root, { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @chrisgervang does it make sense to refactor my recent mobile tip+tricks work: within |
||
| touchAction: this.props.touchAction, | ||
| recognizers: Object.keys(RECOGNIZERS).map((eventName: string) => { | ||
| // Resolve recognizer settings | ||
| const [RecognizerConstructor, defaultOptions, recognizeWith, requireFailure] = | ||
| RECOGNIZERS[eventName]; | ||
| const optionsOverride = this.props.eventRecognizerOptions?.[eventName]; | ||
| const options = {...defaultOptions, ...optionsOverride, event: eventName}; | ||
| return { | ||
| recognizer: new RecognizerConstructor(options), | ||
| recognizeWith, | ||
| requireFailure | ||
| }; | ||
| }), | ||
| events: { | ||
| pointerdown: this._onPointerDown, | ||
| pointermove: this._onPointerMove, | ||
| pointerleave: this._onPointerMove | ||
| } | ||
| }); | ||
|
|
||
| for (const eventType in EVENT_HANDLERS) { | ||
| eventManager.on(eventType, this._onEvent); | ||
| } | ||
|
|
||
| return eventManager; | ||
| } | ||
|
|
||
| private _setCanvasContext(canvasContext: CanvasContext): void { | ||
| this._canvasContext = canvasContext; | ||
|
|
||
|
|
@@ -1377,33 +1424,14 @@ export default class Deck<ViewsT extends ViewOrViews = null> { | |
|
|
||
| const eventRoot = | ||
| this.props.parent?.querySelector<HTMLDivElement>('.deck-events-root') || this.canvas; | ||
| this.eventManager = new EventManager(eventRoot, { | ||
| touchAction: this.props.touchAction, | ||
| recognizers: Object.keys(RECOGNIZERS).map((eventName: string) => { | ||
| // Resolve recognizer settings | ||
| const [RecognizerConstructor, defaultOptions, recognizeWith, requireFailure] = | ||
| RECOGNIZERS[eventName]; | ||
| const optionsOverride = this.props.eventRecognizerOptions?.[eventName]; | ||
| const options = {...defaultOptions, ...optionsOverride, event: eventName}; | ||
| return { | ||
| recognizer: new RecognizerConstructor(options), | ||
| recognizeWith, | ||
| requireFailure | ||
| }; | ||
| }), | ||
| events: { | ||
| pointerdown: this._onPointerDown, | ||
| pointermove: this._onPointerMove, | ||
| pointerleave: this._onPointerMove | ||
| } | ||
| }); | ||
| for (const eventType in EVENT_HANDLERS) { | ||
| this.eventManager.on(eventType, this._onEvent); | ||
| } | ||
| assert(eventRoot); | ||
| this.eventManager = this._createEventManager(eventRoot); | ||
| this.eventManagers = {[DEFAULT_CANVAS_ID]: this.eventManager}; | ||
|
|
||
| this.viewManager = new ViewManager({ | ||
| timeline, | ||
| eventManager: this.eventManager, | ||
| eventManagers: this.eventManagers, | ||
| onViewStateChange: this._onViewStateChange.bind(this), | ||
| onInteractionStateChange: this._onInteractionStateChange.bind(this), | ||
| pickPosition: this._pickPositionForController.bind(this), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,14 @@ export type ViewStateObject<ViewsT extends ViewOrViews> = | |
| | AnyViewStateOf<ViewsT> | ||
| | {[viewId: string]: AnyViewStateOf<ViewsT>}; | ||
|
|
||
| /** Canvas id used by views that do not declare a presentation canvas. */ | ||
| export const DEFAULT_CANVAS_ID = 'default-canvas'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| type ViewEventManager = { | ||
| canvasId: string; | ||
| eventManager: EventManager; | ||
| }; | ||
|
|
||
| /** ViewManager props directly supplied by the user */ | ||
| type ViewManagerProps<ViewsT extends ViewOrViews> = { | ||
| views: ViewsT; | ||
|
|
@@ -46,6 +54,8 @@ type ViewManagerProps<ViewsT extends ViewOrViews> = { | |
| pickPosition?: (x: number, y: number) => {coordinate?: number[]} | null; | ||
| width?: number; | ||
| height?: number; | ||
| /** Event managers keyed by presentation canvas id. */ | ||
| eventManagers?: Record<string, EventManager>; | ||
| }; | ||
|
|
||
| export default class ViewManager<ViewsT extends View[]> { | ||
|
|
@@ -62,6 +72,8 @@ export default class ViewManager<ViewsT extends View[]> { | |
| private _needsRedraw: string | false; | ||
| private _needsUpdate: string | false; | ||
| private _eventManager: EventManager; | ||
| private _eventManagers: Record<string, EventManager>; | ||
| private _viewEventManagers: {[viewId: string]: ViewEventManager}; | ||
| private _eventCallbacks: { | ||
| onViewStateChange?: (params: ViewStateChangeParameters) => void; | ||
| onInteractionStateChange?: (state: InteractionState) => void; | ||
|
|
@@ -90,6 +102,8 @@ export default class ViewManager<ViewsT extends View[]> { | |
| this._needsUpdate = 'Initialize'; | ||
|
|
||
| this._eventManager = props.eventManager; | ||
| this._eventManagers = props.eventManagers || {}; | ||
| this._viewEventManagers = {}; | ||
| this._eventCallbacks = { | ||
| onViewStateChange: props.onViewStateChange, | ||
| onInteractionStateChange: props.onInteractionStateChange | ||
|
|
@@ -188,6 +202,14 @@ export default class ViewManager<ViewsT extends View[]> { | |
| return this._viewportMap[viewId]; | ||
| } | ||
|
|
||
| /** Return the presentation canvas id assigned to a view. */ | ||
| getCanvasId(viewOrViewId: string | View): string | undefined { | ||
| const view = typeof viewOrViewId === 'string' ? this.getView(viewOrViewId) : viewOrViewId; | ||
| return view | ||
| ? this._viewEventManagers[view.id]?.canvasId || view.props.canvasId || DEFAULT_CANVAS_ID | ||
| : undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Unproject pixel coordinates on screen onto world coordinates, | ||
| * (possibly [lon, lat]) on map. | ||
|
|
@@ -231,6 +253,10 @@ export default class ViewManager<ViewsT extends View[]> { | |
| this._pickPosition = props.pickPosition; | ||
| } | ||
|
|
||
| if ('eventManagers' in props) { | ||
| this._setEventManagers(props.eventManagers || {}); | ||
| } | ||
|
|
||
| // Important: avoid invoking _update() inside itself | ||
| // Nested updates result in unexpected side effects inside _rebuildViewports() | ||
| // when using auto control in pure-js | ||
|
|
@@ -298,6 +324,49 @@ export default class ViewManager<ViewsT extends View[]> { | |
| } | ||
| } | ||
|
|
||
| private _setEventManagers(eventManagers: Record<string, EventManager>): void { | ||
| if (this._eventManagers !== eventManagers) { | ||
| this._eventManagers = eventManagers; | ||
| this.setNeedsUpdate('eventManagers changed'); | ||
| } | ||
| } | ||
|
|
||
| private _getViewEventManager(view: View): ViewEventManager { | ||
| const canvasId = this.getCanvasId(view) || DEFAULT_CANVAS_ID; | ||
| return { | ||
| canvasId, | ||
| eventManager: this._eventManagers[canvasId] || this._eventManager | ||
| }; | ||
| } | ||
|
|
||
| private _startViewportRebuild(): { | ||
| oldControllers: {[viewId: string]: Controller<any> | null}; | ||
| oldViewEventManagers: {[viewId: string]: ViewEventManager}; | ||
| } { | ||
| const oldControllers = this.controllers; | ||
| const oldViewEventManagers = this._viewEventManagers; | ||
| this._viewports = []; | ||
| this.controllers = {}; | ||
| this._viewEventManagers = {}; | ||
| return {oldControllers, oldViewEventManagers}; | ||
| } | ||
|
|
||
| private _getReusableController( | ||
| controller: Controller<any> | null | undefined, | ||
| oldViewEventManager: ViewEventManager | undefined, | ||
| viewEventManager: ViewEventManager | ||
| ): Controller<any> | null | undefined { | ||
| if ( | ||
| controller && | ||
| (oldViewEventManager?.canvasId !== viewEventManager.canvasId || | ||
| oldViewEventManager?.eventManager !== viewEventManager.eventManager) | ||
| ) { | ||
| controller.finalize(); | ||
| return null; | ||
| } | ||
| return controller; | ||
| } | ||
|
|
||
| private _createController( | ||
| view: View, | ||
| props: {id: string; type: ConstructorOf<Controller<any>>} | ||
|
|
@@ -306,7 +375,7 @@ export default class ViewManager<ViewsT extends View[]> { | |
|
|
||
| const controller = new Controller({ | ||
| timeline: this.timeline, | ||
| eventManager: this._eventManager, | ||
| eventManager: this._getViewEventManager(view).eventManager, | ||
| // Set an internal callback that calls the prop callback if provided | ||
| onViewStateChange: this._eventCallbacks.onViewStateChange, | ||
| onStateChange: this._eventCallbacks.onInteractionStateChange, | ||
|
|
@@ -357,18 +426,22 @@ export default class ViewManager<ViewsT extends View[]> { | |
| private _rebuildViewports(): void { | ||
| const {views} = this; | ||
|
|
||
| const oldControllers = this.controllers; | ||
| this._viewports = []; | ||
| this.controllers = {}; | ||
| const {oldControllers, oldViewEventManagers} = this._startViewportRebuild(); | ||
|
|
||
| let invalidateControllers = false; | ||
| // Create controllers in reverse order, so that views on top receive events first | ||
| for (let i = views.length; i--; ) { | ||
| const view = views[i]; | ||
| const viewEventManager = this._getViewEventManager(view); | ||
| this._viewEventManagers[view.id] = viewEventManager; | ||
| const viewState = this.getViewState(view); | ||
| const viewport = view.makeViewport({viewState, width: this.width, height: this.height}); | ||
|
|
||
| let oldController = oldControllers[view.id]; | ||
| let oldController = this._getReusableController( | ||
| oldControllers[view.id], | ||
| oldViewEventManagers[view.id], | ||
| viewEventManager | ||
| ); | ||
| const hasController = Boolean(view.controller); | ||
| if (hasController && !oldController) { | ||
| // When a new controller is added, invalidate all controllers below it so that | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -18,6 +18,12 @@ export type CommonViewState = TransitionProps; | |||||
| export type CommonViewProps<ViewState> = { | ||||||
| /** A unique id of the view. In a multi-view use case, this is important for matching view states and place contents into this view. */ | ||||||
| id?: string; | ||||||
| /** | ||||||
| * Optional id of the presentation canvas associated with this view. | ||||||
| * Integrations that render views into multiple canvases can use this id to route view-scoped | ||||||
| * resources such as event managers. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| */ | ||||||
| canvasId?: string; | ||||||
| /** A relative (e.g. `'50%'`) or absolute position. Default `0`. */ | ||||||
| x?: number | string; | ||||||
| /** A relative (e.g. `'50%'`) or absolute position. Default `0`. */ | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should
console.warn. Fallback makes sense but should be a little less silent on a public method.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it would be nice to show developer friendly messages all over the API in cases like this, such message strings would all be adding to the bundle size of the application and since they have no value to end user, we generally avoid adding them unless value is very high.