Skip to content

Commit

Permalink
Add widgets prop to Deck class (#8023)
Browse files Browse the repository at this point in the history
* add(deck) _widget prop

* declarative widgets api

* Add comments

* fix test

* simplify widget api

* docs

* add(core) export types

---------

Co-authored-by: Xiaoji Chen <cxiaoji@gmail.com>
  • Loading branch information
chrisgervang and Pessimistress authored Aug 15, 2023
1 parent fcbbd5f commit 1d56226
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 67 deletions.
30 changes: 30 additions & 0 deletions docs/api-reference/core/widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ deckgl.addWidget(new LoadingIndicator({size: 48}));

When a widget instance is added to Deck, the user can optionally specify a `viewId` that it is attached to (default `null`). If assigned, this widget will only respond to events occured inside the specific view that matches this id.

### Members

A `Widget` implements the following members.

##### `id`

Unique identifier of the widget.

##### `props` (Object)

Any options for the widget, as passed into the constructor and can be updated with `setProps`.

##### `viewId` (String | null)

* Default: `null`

The id of the view that the widget is attached to. If `null`, the widget receives events from all views. Otherwise, it only receives events from the view that matches this id.

##### `placement` (String, optional)

* Default: `'top-left'`

Widget positioning within the view. One of:

- `'top-left'`
- `'top-right'`
- `'bottom-left'`
- `'bottom-right'`
- `'fill'`

### Methods

##### `onAdd`
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export type {Effect, PreRenderOptions, PostRenderOptions} from './lib/effect';
export type {PickingUniforms, ProjectUniforms} from './shaderlib';
export type {DefaultProps} from './lifecycle/prop-types';
export type {LayersPassRenderOptions} from './passes/layers-pass';
export type {Widget} from './lib/widget-manager';
export type {Widget, WidgetPlacement} from './lib/widget-manager';

// INTERNAL, DO NOT USER
// @deprecated internal do not use
Expand Down
8 changes: 6 additions & 2 deletions modules/core/src/lib/deck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import MapView from '../views/map-view';
import EffectManager from './effect-manager';
import DeckRenderer from './deck-renderer';
import DeckPicker from './deck-picker';
import {WidgetManager, Widget, WidgetPlacement} from './widget-manager';
import {WidgetManager, Widget} from './widget-manager';
import Tooltip from './tooltip';
import log from '../utils/log';
import {deepEqual} from '../utils/deep-equal';
Expand Down Expand Up @@ -181,6 +181,8 @@ export type DeckProps = {
_pickable?: boolean;
/** (Experimental) Fine-tune attribute memory usage. See documentation for details. */
_typedArrayManagerProps?: TypedArrayManagerOptions;
/** An array of Widget instances to be added to the parent element. */
widgets?: Widget[];

/** Called once the GPU Device has been initiated. */
onDeviceInitialized?: (device: Device) => void;
Expand Down Expand Up @@ -258,6 +260,7 @@ const defaultProps = {
_pickable: true,
_typedArrayManagerProps: {},
_customRender: null,
widgets: [],

onDeviceInitialized: noop,
onWebGLInitialized: noop,
Expand Down Expand Up @@ -485,6 +488,7 @@ export default class Deck {
this.effectManager.setProps(resolvedProps);
this.deckRenderer.setProps(resolvedProps);
this.deckPicker.setProps(resolvedProps);
this.widgetManager.setProps(resolvedProps);
}

this.stats.get('setProps Time').timeEnd();
Expand Down Expand Up @@ -988,7 +992,7 @@ export default class Deck {
deck: this,
parentElement: this.canvas?.parentElement
});
this.widgetManager.add(new Tooltip(), {placement: 'fill'});
this.widgetManager.addDefault(new Tooltip());

this.setProps(this.props);

Expand Down
7 changes: 6 additions & 1 deletion modules/core/src/lib/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import {Widget} from './widget-manager';
import type {Widget, WidgetPlacement} from './widget-manager';
import type {PickingInfo} from './picking/pick-info';
import type Viewport from '../viewports/viewport';
import type Deck from './deck';
Expand Down Expand Up @@ -46,6 +46,9 @@ export type TooltipContent =
};

export default class Tooltip implements Widget {
id = 'default-tooltip';
placement: WidgetPlacement = 'fill';
props = {};
isVisible: boolean = false;
deck?: Deck;
element?: HTMLDivElement;
Expand All @@ -67,6 +70,8 @@ export default class Tooltip implements Widget {
this.element = undefined;
}

setProps() {}

Check warning on line 73 in modules/core/src/lib/tooltip.ts

View workflow job for this annotation

GitHub Actions / test-node

Unexpected empty method 'setProps'

onViewportChange(viewport: Viewport) {
if (this.isVisible && viewport.id === this.lastViewport?.id && viewport !== this.lastViewport) {
// Camera has moved, clear tooltip
Expand Down
142 changes: 108 additions & 34 deletions modules/core/src/lib/widget-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,23 @@ import type {MjolnirPointerEvent, MjolnirGestureEvent} from 'mjolnir.js';
import type Layer from './layer';

import {EVENTS} from './constants';
import {deepEqual} from '../utils/deep-equal';

export interface Widget<PropsT = any> {
/** Unique identifier of the widget. */
id: string;
/** Widget prop types. */
props: PropsT;
/**
* The view id that this widget is being attached to. Default `null`.
* If assigned, this widget will only respond to events occured inside the specific view that matches this id.
*/
viewId?: string | null;
/** Widget positioning within the view. Default 'top-left'. */
placement?: WidgetPlacement;

// Populated by core when mounted
_element?: HTMLDivElement | null;
_viewId?: string | null;

// Lifecycle hooks
/** Called when the widget is added to a Deck instance.
Expand All @@ -23,7 +35,7 @@ export interface Widget<PropsT = any> {
/** Called when the widget is removed */
onRemove: () => void;
/** Called to update widget options */
setProps?: (props: Partial<PropsT>) => void;
setProps: (props: Partial<PropsT>) => void;

// Optional event hooks
/** Called when the containing view is changed */
Expand All @@ -49,6 +61,7 @@ const PLACEMENTS = {
'bottom-right': {bottom: 0, right: 0},
fill: {top: 0, left: 0, bottom: 0, right: 0}
} as const;
const DEFAULT_PLACEMENT = 'top-left';

export type WidgetPlacement = keyof typeof PLACEMENTS;

Expand All @@ -57,61 +70,122 @@ const ROOT_CONTAINER_ID = '__root';
export class WidgetManager {
deck: Deck;
parentElement?: HTMLElement | null;
containers: {[id: string]: HTMLDivElement} = {};
widgets: Widget[] = [];
lastViewports: {[id: string]: Viewport} = {};

/** Widgets added via the imperative API */
private defaultWidgets: Widget[] = [];
/** Widgets received from the declarative API */
private widgets: Widget[] = [];
/** Resolved widgets from both imperative and declarative APIs */
private resolvedWidgets: Widget[] = [];

/** Mounted HTML containers */
private containers: {[id: string]: HTMLDivElement} = {};
/** Viewport provided to widget on redraw */
private lastViewports: {[id: string]: Viewport} = {};

constructor({deck, parentElement}: {deck: Deck; parentElement?: HTMLElement | null}) {
this.deck = deck;
this.parentElement = parentElement;
}

getWidgets(): Widget[] {
return this.resolvedWidgets;
}

/** Declarative API to configure widgets */
setProps(props: {widgets?: Widget[]}) {
if (props.widgets && !deepEqual(props.widgets, this.widgets, 1)) {
this._setWidgets(props.widgets);
}
}

finalize() {
for (const widget of this.widgets) {
this.remove(widget);
for (const widget of this.getWidgets()) {
this._remove(widget);
}
this.defaultWidgets.length = 0;
this.resolvedWidgets.length = 0;
for (const id in this.containers) {
this.containers[id].remove();
}
}

add(
widget: Widget,
opts: {
viewId?: string | null;
placement?: WidgetPlacement;
} = {}
) {
if (this.widgets.includes(widget)) {
// widget already added
return;
/** Imperative API. Widgets added this way are not affected by the declarative prop. */
addDefault(widget: Widget) {
if (!this.defaultWidgets.find(w => w.id === widget.id)) {
this._add(widget);
this.defaultWidgets.push(widget);
// Update widget list
this._setWidgets(this.widgets);
}
}

/** Resolve widgets from the declarative prop */
private _setWidgets(nextWidgets: Widget[]) {
const oldWidgetMap: Record<string, Widget | null> = {};

for (const widget of this.resolvedWidgets) {
oldWidgetMap[widget.id] = widget;
}
// Clear and rebuild the list
this.resolvedWidgets.length = 0;

const {placement = 'top-left', viewId = null} = opts;
// Add all default widgets
for (const widget of this.defaultWidgets) {
oldWidgetMap[widget.id] = null;
this.resolvedWidgets.push(widget);
}

for (let widget of nextWidgets) {
const oldWidget = oldWidgetMap[widget.id];
if (!oldWidget) {
// Widget is new
this._add(widget);
} else if (
// Widget placement changed
oldWidget.viewId !== widget.viewId ||
oldWidget.placement !== widget.placement
) {
this._remove(oldWidget);
this._add(widget);
} else if (widget !== oldWidget) {
// Widget props changed
oldWidget.setProps(widget.props);
widget = oldWidget;
}

// mark as matched
oldWidgetMap[widget.id] = null;
this.resolvedWidgets.push(widget);
}

for (const id in oldWidgetMap) {
const oldWidget = oldWidgetMap[id];
if (oldWidget) {
// No longer exists
this._remove(oldWidget);
}
}
this.widgets = nextWidgets;
}

private _add(widget: Widget) {
const {viewId = null, placement = DEFAULT_PLACEMENT} = widget;
const element = widget.onAdd({deck: this.deck, viewId});

if (element) {
this._getContainer(viewId, placement).append(element);
}
widget._viewId = viewId;
widget._element = element;
this.widgets.push(widget);
}

remove(widget: Widget) {
const i = this.widgets.indexOf(widget);
if (i < 0) {
// widget not found
return;
}
this.widgets.splice(i, 1);
private _remove(widget: Widget) {
widget.onRemove();

if (widget._element) {
widget._element.remove();
}
widget._element = undefined;
widget._viewId = undefined;
}

/* global document */
Expand Down Expand Up @@ -165,8 +239,8 @@ export class WidgetManager {
}, {});
const {lastViewports} = this;

for (const widget of this.widgets) {
const viewId = widget._viewId;
for (const widget of this.getWidgets()) {
const {viewId} = widget;
if (viewId) {
// Attached to a specific view
const viewport = viewportsById[viewId];
Expand All @@ -193,8 +267,8 @@ export class WidgetManager {
}

onHover(info: PickingInfo, event: MjolnirPointerEvent) {
for (const widget of this.widgets) {
const viewId = widget._viewId;
for (const widget of this.getWidgets()) {
const {viewId} = widget;
if (!viewId || viewId === info.viewport?.id) {
widget.onHover?.(info, event);
}
Expand All @@ -206,8 +280,8 @@ export class WidgetManager {
if (!eventOptions) {
return;
}
for (const widget of this.widgets) {
const viewId = widget._viewId;
for (const widget of this.getWidgets()) {
const {viewId} = widget;
if (!viewId || viewId === info.viewport?.id) {
widget[eventOptions.handler]?.(info, event);
}
Expand Down
5 changes: 2 additions & 3 deletions test/modules/core/lib/tooltip.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function setupTest() {
const container = document.createElement('div');
const widgetManager = new WidgetManager({parentElement: container});
const tooltip = new Tooltip();
widgetManager.add(tooltip);
widgetManager.addDefault(tooltip);
return {tooltip, widgetManager, container};
}

Expand Down Expand Up @@ -90,13 +90,12 @@ test('Tooltip#remove', t => {
const {widgetManager, tooltip, container} = setupTest();

t.equals(container.querySelectorAll('.deck-tooltip').length, 1, 'Tooltip element present');
widgetManager.remove(tooltip);
widgetManager.finalize();
t.equals(
container.querySelectorAll('.deck-tooltip').length,
0,
'Tooltip element successfully removed'
);

widgetManager.finalize();
t.end();
});
Loading

0 comments on commit 1d56226

Please sign in to comment.