-
Notifications
You must be signed in to change notification settings - Fork 2.2k
RFC: Add view layout compiler #10269
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
Changes from 5 commits
c5c3f5c
404f41d
3defef0
17c79b4
0aed46a
05304a7
e95c6a0
9e3050f
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 |
|---|---|---|
| @@ -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'`. | ||
|
|
||
| `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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| */ | ||
|
|
@@ -50,10 +50,11 @@ type ManagedViewLayout = { | |
| height: number; | ||
| }; | ||
|
|
||
| function parseViewLayout(root: ViewLayout): ManagedViewLayout[] { | ||
| function parseViewLayout(root: SplitterWidgetViewLayout): ManagedViewLayout[] { | ||
|
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. 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 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
I'll take a closer look at your examples and maybe that'll clear the use case up for me
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. See comment in the examples PR.. my preference is to put the ViewLayout compiler in What do you think?
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.
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:
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.
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. 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: 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; | ||
|
|
@@ -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; | ||
|
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. Accept the new ViewLayout as well?
Collaborator
Author
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. I would personally prefer not to. It is a more complex layout system not focused on widgets. |
||
| /** Callback invoked when the splitter is dragged with the new split value */ | ||
| onChange?: (views: ViewsT) => void; | ||
| /** Callback invoked when dragging starts */ | ||
|
|
||
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.
I don't think that's necessary... We just need a helper inside SplitterWidget to convert the old format to the new one.