Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api-reference/widgets/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This module contains the following widgets:

- [FullscreenWidget](./fullscreen-widget.md)
- [SplitterWidget](./splitter-widget.md)
- [View Layout](./view-layout.md)

### Information Widgets

Expand Down
9 changes: 6 additions & 3 deletions docs/api-reference/widgets/splitter-widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ function App() {
## Constructor

```ts
import {_SplitterWidget as SplitterWidget, type SplitterWidgetProps} from '@deck.gl/widgets';
import {
_SplitterWidget as SplitterWidget,
type SplitterWidgetProps
} from '@deck.gl/widgets';
new SplitterWidget<ViewType[]>({} satisfies SplitterWidgetProps);
```

Expand All @@ -150,7 +153,7 @@ new SplitterWidget<ViewType[]>({} satisfies SplitterWidgetProps);

The `SplitterWidget` accepts the generic [`WidgetProps`](../core/widget.md#widgetprops) and:

#### `viewLayout` (ViewLayout, required) {#viewlayout}
#### `viewLayout` (SplitterWidgetViewLayout, required) {#viewlayout}

Layout descriptor of how views are arranged on the canvas. Contains the following fields:

Expand All @@ -161,7 +164,7 @@ Layout descriptor of how views are arranged on the canvas. Contains the followin
- `minSplit` (number, optional) - Min value of the split. The user cannot make the first view smaller than this ratio. Default `0.05`.
- `maxSplit` (number, optional) - Max value of the split. The user cannot make the first view larger than this ratio. Default `0.95`.

You may also replace one or both item in `views` with a `ViewLayout` object, composing more than two views into a complex layout.
You may also replace one or both items in `views` with a nested `SplitterWidgetViewLayout` object, composing more than two views into a complex layout.


#### `onChange` (Function, optional) {#onchange}
Expand Down
133 changes: 133 additions & 0 deletions docs/api-reference/widgets/view-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# View Layout

The view layout helpers build stable deck.gl view arrays from a declarative layout tree. They are generic utilities for applications that need multiple coordinated views without hand-computing every view rectangle in render code.

```js
import {buildViewsFromViewLayout} from '@deck.gl/widgets';
```

## Usage

Start by defining the deck.gl `View` instances that your application needs. Then place them in a plain layout object tree and compile that tree for the current deck canvas size.

```tsx
import React from 'react';
import {DeckGL} from '@deck.gl/react';
import {OrthographicView} from '@deck.gl/core';
import {buildViewsFromViewLayout} from '@deck.gl/widgets';
import type {ViewLayout} from '@deck.gl/widgets';

const VIEW_LAYOUT = {
type: 'column',
children: [
new OrthographicView({id: 'header', height: 48, controller: false}),
{
type: 'row',
children: [
new OrthographicView({id: 'sidebar', controller: false}),
{
type: 'overlay',
children: [
new OrthographicView({id: 'main', controller: true}),
new OrthographicView({
id: 'minimap',
x: 'calc(100% - 180px)',
y: 16,
width: 164,
height: 120,
controller: false,
clear: true
})
]
}
]
}
]
} satisfies ViewLayout;

export function App({width, height, layers}) {
const compiled = buildViewsFromViewLayout({layout: VIEW_LAYOUT, width, height});
return <DeckGL views={compiled.views} layers={layers} />;
}
```

The returned `compiled.rectsById` map contains the same resolved rectangles keyed by view id. Use it when you need to position DOM overlays next to deck views, debug the generated layout, or scope non-layer UI to a view rectangle.

The returned `compiled.splittersById` map contains splitter metadata for rows and columns that declare a `splitId`. For two-child splits, the splitter id is exactly `splitId`. For three or more children, the compiler creates one splitter between each adjacent pair using generated ids such as `splitId-0`, `splitId-1`, and so on. Applications that store split values can pass them back into `buildViewsFromViewLayout` via `splitValues`.

Layout items may also define `minPixels` and `maxPixels` to constrain their size in the parent stack axis. The compiler combines those pixel constraints with percentage-based `width` or `height` values, and with `minSplit` and `maxSplit` when returning splitter metadata.

Use `viewPropsById` when an application needs to control layout-only bounds for a view without rebuilding the static layout tree. Override values use the same length syntax as authored view props.

The layout tree is a discriminated union of plain objects:

- `row`: lays out children left to right.
- `column`: lays out children top to bottom.
- `overlay`: gives each child the same parent rectangle.
- `spacer`: reserves empty fixed or flexible space.

Raw deck.gl `View` instances are leaf nodes in `children`. Put layout-only `width`, `height`, `x`, and `y` props directly on the `View` when a leaf needs fixed sizing or overlay positioning.

For split layouts, `ViewLayout` also accepts the `SplitterWidgetViewLayout`-style aliases `orientation: 'horizontal' | 'vertical'` and `views`. A horizontal orientation is equivalent to `type: 'row'`; a vertical orientation is equivalent to `type: 'column'`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's necessary... We just need a helper inside SplitterWidget to convert the old format to the new one.


`buildViewsFromViewLayout` compiles a layout tree into:

- `views`: concrete deck.gl views with numeric `x`, `y`, `width`, and `height`.
- `rectsById`: resolved rectangles keyed by deck view id.
- `splittersById`: resolved splitter metadata keyed by split id.

## Layout Sizing

`width`, `height`, `x`, and `y` accept numbers or CSS-like length strings such as percentages and simple `calc(...)` expressions. The compiler resolves those values against the current parent rectangle before passing numeric bounds to deck.gl.

```ts
new OrthographicView({
id: 'overlay',
x: '50%',
width: 'calc(50% - 12px)',
height: 80
});
```

## View Reuse

Pass the previous `CompiledDeckViews` result back to `buildViewsFromViewLayout` when a caller needs structural view reuse across renders. A previous view is reused when its id, constructor, and resolved props match the next compilation.

```ts
let compiled = buildViewsFromViewLayout({layout, width, height});

compiled = buildViewsFromViewLayout({
layout,
width,
height,
previous: compiled
});
```

## Types

### `ViewLayout`

Plain discriminated layout object. Children may be nested layout objects, raw deck.gl `View` instances, or falsey optional children.

### `buildViewsFromViewLayout`

Compiles a layout tree for the current deck canvas size.

Parameters:

- `layout` (`ViewLayout`) - Root layout tree to compile.
- `width` (`number`) - Current deck width in pixels.
- `height` (`number`) - Current deck height in pixels.
- `previous` (`CompiledDeckViews`, optional) - Previous compilation for view reuse.
- `splitValues` (`Record<string, number>`, optional) - Controlled split ratios keyed by layout `splitId`.
- `viewPropsById` (`Record<string, {x?, y?, width?, height?}>`, optional) - Controlled layout-only view prop overrides keyed by deck view id.

Returns:

- `views` (`View[]`) - Concrete deck.gl views.
- `rectsById` (`Record<string, {x, y, width, height}>`) - Resolved rectangles keyed by view id.

## Source

[modules/widgets/src/view-layout/build-views-from-view-layout.ts](https://github.com/visgl/deck.gl/tree/master/modules/widgets/src/view-layout/build-views-from-view-layout.ts)
3 changes: 2 additions & 1 deletion docs/table-of-contents.json
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@
"type": "category",
"label": "@deck.gl/react",
"items": [
"api-reference/react/overview",
"api-reference/react/overview",
"api-reference/react/deckgl",
"api-reference/react/use-widget"
]
Expand Down Expand Up @@ -338,6 +338,7 @@
"api-reference/widgets/theme-widget",
"api-reference/widgets/timeline-widget",
"api-reference/widgets/toggle-widget",
"api-reference/widgets/view-layout",
"api-reference/widgets/zoom-widget"
]
}
Expand Down
4 changes: 4 additions & 0 deletions modules/main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export {
// View widgets
FullscreenWidget,
_SplitterWidget,
buildViewsFromViewLayout,
// Information widgets
InfoWidget,
PopupWidget,
Expand Down Expand Up @@ -244,6 +245,9 @@ export type {
StatsWidgetProps,
ContextMenuWidgetProps,
SplitterWidgetProps,
SplitterWidgetViewLayout,
CompiledDeckViews,
ViewLayout,
TimelineWidgetProps,
SelectorWidgetProps,
GimbalWidgetProps,
Expand Down
7 changes: 6 additions & 1 deletion modules/widgets/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export {GeocoderWidget as _GeocoderWidget} from './geocoder-widget';
// View widgets
export {FullscreenWidget} from './fullscreen-widget';
export {SplitterWidget as _SplitterWidget} from './splitter-widget';
export {
buildViewsFromViewLayout,
type CompiledDeckViews
} from './view-layout/build-views-from-view-layout';
export type {ViewLayout} from './view-layout/view-layout';

// Information widgets
export {InfoWidget} from './info-widget';
Expand Down Expand Up @@ -49,7 +54,7 @@ export type {InfoWidgetProps} from './info-widget';
export type {PopupWidgetProps} from './popup-widget';
export type {StatsWidgetProps} from './stats-widget';
export type {ContextMenuWidgetProps} from './context-menu-widget';
export type {SplitterWidgetProps} from './splitter-widget';
export type {SplitterWidgetProps, SplitterWidgetViewLayout} from './splitter-widget';
export type {TimelineWidgetProps} from './timeline-widget';
export type {SelectorWidgetProps} from './selector-widget';
export type {GimbalWidgetProps} from './gimbal-widget';
Expand Down
13 changes: 7 additions & 6 deletions modules/widgets/src/splitter-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import {
type View
} from '@deck.gl/core';

export type ViewLayout = {
export type SplitterWidgetViewLayout = {
/** Stacking orientation of the sub views */
orientation: 'vertical' | 'horizontal';
/** Initial instances that describe the sub views.
* x, y, width and height of the views' props will be overwritten by the SplitterWidget as split changes. */
views: [view1: View | ViewLayout, view2: View | ViewLayout];
views: [view1: View | SplitterWidgetViewLayout, view2: View | SplitterWidgetViewLayout];
/** The ratio of view1's share over the whole available height (vertical) or width (horizontal). Between 0-1.
* @default 0.5
*/
Expand Down Expand Up @@ -50,10 +50,11 @@ type ManagedViewLayout = {
height: number;
};

function parseViewLayout(root: ViewLayout): ManagedViewLayout[] {
function parseViewLayout(root: SplitterWidgetViewLayout): ManagedViewLayout[] {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there overlap between this parser implementation and the extracted one? I think this PR's use case is missing for me.. is this PR adding a function to help me implement a widget, or is it a utility to use by an application to fill the top-level views prop?

I think at the root of this is a question of discoverability.. is the widgets module where I'd look for a view builder function?

We don't really have a good module for this.. a @deck.gl/views or /utils would be more intuitive, though I don't think this change alone warrants the addition of a new module.

/extensions has meant layer extensions up until now, but we could call this is a view extension of sorts..

I'll take a closer look at your examples and maybe that'll clear the use case up for me

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment in the examples PR.. my preference is to put the ViewLayout compiler in @deck.gl/json.. it seems like the natural home for a JSON utility like this.

What do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my preference is to put the ViewLayout compiler in @deck.gl/json

As I thought about this, it struck me that one problem is dependency chains. If we have the widgets module using the types exported here, then we get a dependency from widgets on the json or experimental modules, which seems undesirable.

FWIW, This was not created as something JSON specific. I developed this for a real use case, a big non-geospatial application that composes a lot of views (headersm legends, overviews, separate timelines etc, and the views can be reconfigured by the user. Changing those views around with offsets and heights etc was a pain, and this system makes it effortless.

I personally think we could make advanced view support a "tentpole" of the 9.4 release:

  • Multicanvas Views, this dynamic View Layout system, GlobeView graduation (tons of improvements there) and a bunch of extra things in the view tracker.
  • The JSON/pydeck bindings and the new widgets would just be icing on that cake.

I think landing it in the widgets module for now would not be unreasonable, then we have a bit more time to consider how to make this work with multi-canvas views and maybe other improvements to the views we want to make.

But if the temperature is lukewarm, I can always land it in community instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I think that'd be a very compelling theme for 9.4 and this adds of value towards it.

We can always move where this lives up until 9.4 is released.

@Pessimistress I read your feedback around this being placed outside of core because it's self-contained, but I'm starting to see this as a branch that leaf modules are depending on in JSON and widgets

We could even consider this for a core API change: deck.setProps({ views: ViewLayout | ... })

Curious to get more perspective on how you're seeing it fit in

const layoutsById: ManagedViewLayout[] = [];
const isViewLayout = (v: View | ViewLayout): v is ViewLayout => 'views' in v;
function createManagedViewLayout(l: ViewLayout): ManagedViewLayout {
const isViewLayout = (v: View | SplitterWidgetViewLayout): v is SplitterWidgetViewLayout =>
'views' in v;
function createManagedViewLayout(l: SplitterWidgetViewLayout): ManagedViewLayout {
const id = layoutsById.length;
const minSplit = l.minSplit ?? 0.05;
const maxSplit = l.maxSplit ?? 0.95;
Expand Down Expand Up @@ -150,7 +151,7 @@ function evaluateViews(root: ManagedViewLayout): View[] {
/** Properties for the SplitterWidget */
export type SplitterWidgetProps<ViewsT extends View[] = View[]> = WidgetProps & {
/** Stacking views descriptor */
viewLayout: ViewLayout;
viewLayout: SplitterWidgetViewLayout;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accept the new ViewLayout as well?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would personally prefer not to. It is a more complex layout system not focused on widgets.
I would keep this SplitterWidget.views props as compatibility only for now and if we can't find a better way to integrate widgets with views we can always add it later.

/** Callback invoked when the splitter is dragged with the new split value */
onChange?: (views: ViewsT) => void;
/** Callback invoked when dragging starts */
Expand Down
Loading
Loading