diff --git a/db/migrations-wstore/000001_init.down.sql b/db/migrations-wstore/000001_init.down.sql index 9e6fc6d6..177ce086 100644 --- a/db/migrations-wstore/000001_init.down.sql +++ b/db/migrations-wstore/000001_init.down.sql @@ -5,4 +5,3 @@ DROP TABLE db_workspace; DROP TABLE db_tab; DROP TABLE db_block; - diff --git a/db/migrations-wstore/000001_init.up.sql b/db/migrations-wstore/000001_init.up.sql index 7fa0c3bd..34c3b883 100644 --- a/db/migrations-wstore/000001_init.up.sql +++ b/db/migrations-wstore/000001_init.up.sql @@ -27,3 +27,4 @@ CREATE TABLE db_block ( version int NOT NULL, data json NOT NULL ); + diff --git a/db/migrations-wstore/000002_init.down.sql b/db/migrations-wstore/000002_init.down.sql new file mode 100644 index 00000000..f7ef05cd --- /dev/null +++ b/db/migrations-wstore/000002_init.down.sql @@ -0,0 +1 @@ +DROP TABLE db_layout; diff --git a/db/migrations-wstore/000002_init.up.sql b/db/migrations-wstore/000002_init.up.sql new file mode 100644 index 00000000..b0a05456 --- /dev/null +++ b/db/migrations-wstore/000002_init.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE db_layout ( + oid varchar(36) PRIMARY KEY, + version int NOT NULL, + data json NOT NULL +); diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 5ab78133..da3f0530 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -3,7 +3,6 @@ // WaveObjectStore -import { LayoutNode } from "@/faraday/index"; import { Call as $Call, Events } from "@wailsio/runtime"; import * as jotai from "jotai"; import * as React from "react"; @@ -297,7 +296,7 @@ function getObjectValue(oref: string, getFn?: jotai.Getter): T { // sets the value of a WaveObject in the cache. // should provide setFn if it is available (e.g. inside of a jotai atom) // otherwise it will use the globalStore.set function -function setObjectValue(value: WaveObj, setFn?: jotai.Setter, pushToServer?: boolean) { +function setObjectValue(value: T, setFn?: jotai.Setter, pushToServer?: boolean) { const oref = makeORef(value.otype, value.oid); let wov = waveObjectValueCache.get(oref); if (wov == null) { @@ -309,7 +308,9 @@ function setObjectValue(value: WaveObj, setFn?: jotai.Setter, pushToServer?: } console.log("Setting", oref, "to", value); setFn(wov.dataAtom, { value: value, loading: false }); + console.log("Setting", oref, "to", value, "done"); if (pushToServer) { + console.log("pushToServer", oref, value); UpdateObject(value, false); } } @@ -326,8 +327,8 @@ export function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts); } -export function DeleteBlock(blockId: string, newLayout?: LayoutNode): Promise { - return wrapObjectServiceCall("DeleteBlock", blockId, newLayout); +export function DeleteBlock(blockId: string): Promise { + return wrapObjectServiceCall("DeleteBlock", blockId); } export function CloseTab(tabId: string): Promise { @@ -339,9 +340,9 @@ export function UpdateObjectMeta(blockId: string, meta: MetadataType): Promise { + console.log("UpdateObject", waveObj, returnUpdates); return wrapObjectServiceCall("UpdateObject", waveObj, returnUpdates); } - export { cleanWaveObjectCache, clearWaveObjectCache, diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 4da6fe35..6b3e25cb 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -27,13 +27,10 @@ const TabContent = ({ tabId }: { tabId: string }) => { return ; }, []); - const onNodeDelete = useCallback( - (data: TabLayoutData) => { - console.log("onNodeDelete", data, tabData); - WOS.DeleteBlock(data.blockId, tabData.layout); - }, - [tabData] - ); + const onNodeDelete = useCallback((data: TabLayoutData) => { + console.log("onNodeDelete", data); + return WOS.DeleteBlock(data.blockId); + }, []); if (tabLoading) { return ; diff --git a/frontend/app/workspace/workspace.tsx b/frontend/app/workspace/workspace.tsx index 87d794e1..816cfe55 100644 --- a/frontend/app/workspace/workspace.tsx +++ b/frontend/app/workspace/workspace.tsx @@ -77,7 +77,7 @@ function Widgets() { }; dispatchLayoutStateAction(insertNodeAction); }, - [activeTabAtom] + [activeTabAtom, dispatchLayoutStateAction] ); async function createBlock(blockDef: BlockDef) { diff --git a/frontend/faraday/lib/TileLayout.tsx b/frontend/faraday/lib/TileLayout.tsx index 8e336c56..f41b7f5c 100644 --- a/frontend/faraday/lib/TileLayout.tsx +++ b/frontend/faraday/lib/TileLayout.tsx @@ -22,7 +22,7 @@ import { setTransform as createTransform, debounce, determineDropDirection } fro export interface TileLayoutProps { layoutTreeStateAtom: WritableLayoutTreeStateAtom; renderContent: ContentRenderer; - onNodeDelete?: (data: T) => void; + onNodeDelete?: (data: T) => Promise; className?: string; } @@ -120,7 +120,7 @@ export const TileLayout = ({ layoutTreeStateAtom, className, renderContent, ); } }, 30), - [activeDrag, overlayContainerRef, displayContainerRef, layoutTreeState, nodeRefs] + [activeDrag, overlayContainerRef, displayContainerRef, layoutTreeState.leafs, nodeRefs] ); // Update the transforms whenever we drag something and whenever the layout updates. @@ -133,6 +133,7 @@ export const TileLayout = ({ layoutTreeStateAtom, className, renderContent, // reattach the new callback. const [prevUpdateTransforms, setPrevUpdateTransforms] = useState<() => void>(undefined); useEffect(() => { + console.log("replace resize listener"); if (prevUpdateTransforms) window.removeEventListener("resize", prevUpdateTransforms); window.addEventListener("resize", updateTransforms); setPrevUpdateTransforms(updateTransforms); @@ -156,7 +157,7 @@ export const TileLayout = ({ layoutTreeStateAtom, className, renderContent, }, []); const onLeafClose = useCallback( - (node: LayoutNode) => { + async (node: LayoutNode) => { console.log("onLeafClose", node); const deleteAction: LayoutTreeDeleteNodeAction = { type: LayoutTreeActionType.DeleteNode, @@ -165,7 +166,7 @@ export const TileLayout = ({ layoutTreeStateAtom, className, renderContent, console.log("calling dispatch", deleteAction); dispatch(deleteAction); console.log("calling onNodeDelete", node); - onNodeDelete?.(node.data); + await onNodeDelete?.(node.data); console.log("node deleted"); }, [onNodeDelete, dispatch] diff --git a/frontend/faraday/lib/layoutAtom.ts b/frontend/faraday/lib/layoutAtom.ts index b5ed97f9..65e74bc0 100644 --- a/frontend/faraday/lib/layoutAtom.ts +++ b/frontend/faraday/lib/layoutAtom.ts @@ -1,11 +1,13 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { PrimitiveAtom, WritableAtom, atom, useAtom } from "jotai"; +import { WOS } from "@/app/store/global.js"; +import { Atom, Getter, PrimitiveAtom, WritableAtom, atom, useAtom } from "jotai"; import { useCallback } from "react"; import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState.js"; import { LayoutNode, + LayoutNodeWaveObj, LayoutTreeAction, LayoutTreeState, WritableLayoutNodeAtom, @@ -29,27 +31,16 @@ export function newLayoutTreeStateAtom(rootNode: LayoutNode): PrimitiveAto * @returns The derived WritableLayoutTreeStateAtom. */ export function withLayoutTreeState(layoutNodeAtom: WritableLayoutNodeAtom): WritableLayoutTreeStateAtom { - return atom( - (get) => newLayoutTreeState(get(layoutNodeAtom)), - (_get, set, value) => set(layoutNodeAtom, value.rootNode) - ); -} - -export function withLayoutStateFromTab( - tabAtom: WritableAtom -): WritableLayoutTreeStateAtom { + const pendingActionAtom = atom(null) as PrimitiveAtom; return atom( (get) => { - const tabData = get(tabAtom); - console.log("get layout state from tab", tabData); - return newLayoutTreeState(tabData?.layout); + const layoutState = newLayoutTreeState(get(layoutNodeAtom)); + layoutState.pendingAction = get(pendingActionAtom); + return layoutState; }, - (get, set, value) => { - const tabValue = get(tabAtom); - const newTabValue = { ...tabValue }; - newTabValue.layout = value.rootNode; - console.log("set tab", tabValue, value); - set(tabAtom, newTabValue); + (_get, set, value) => { + set(pendingActionAtom, value.pendingAction); + set(layoutNodeAtom, value.rootNode); } ); } @@ -72,6 +63,38 @@ export function useLayoutTreeStateReducerAtom( const tabLayoutAtomCache = new Map>(); +function getLayoutNodeWaveObjAtomFromTab( + tabAtom: Atom, + get: Getter +): WritableAtom, [value: LayoutNodeWaveObj], void> { + const tabValue = get(tabAtom); + console.log("getLayoutNodeWaveObjAtomFromTab tabValue", tabValue); + if (!tabValue) return; + const layoutNodeOref = WOS.makeORef("layout", tabValue.layoutNode); + console.log("getLayoutNodeWaveObjAtomFromTab oref", layoutNodeOref); + return WOS.getWaveObjectAtom>(layoutNodeOref); +} + +export function withLayoutNodeAtomFromTab(tabAtom: Atom): WritableLayoutNodeAtom { + return atom( + (get) => { + console.log("get withLayoutNodeAtomFromTab", tabAtom); + const atom = getLayoutNodeWaveObjAtomFromTab(tabAtom, get); + if (!atom) return null; + const retVal = get(atom)?.node; + console.log("get withLayoutNodeAtomFromTab end", retVal); + return get(atom)?.node; + }, + (get, set, value) => { + console.log("set withLayoutNodeAtomFromTab", value); + const waveObjAtom = getLayoutNodeWaveObjAtomFromTab(tabAtom, get); + if (!waveObjAtom) return; + const newWaveObjAtom = { ...get(waveObjAtom), node: value }; + set(waveObjAtom, newWaveObjAtom); + } + ); +} + export function getLayoutStateAtomForTab( tabId: string, tabAtom: WritableAtom @@ -82,7 +105,7 @@ export function getLayoutStateAtomForTab( return atom; } console.log("Creating new atom for tab", tabId); - atom = withLayoutStateFromTab(tabAtom); + atom = withLayoutTreeState(withLayoutNodeAtomFromTab(tabAtom)); tabLayoutAtomCache.set(tabId, atom); return atom; } diff --git a/frontend/faraday/lib/model.ts b/frontend/faraday/lib/model.ts index dda6a704..861effba 100644 --- a/frontend/faraday/lib/model.ts +++ b/frontend/faraday/lib/model.ts @@ -131,3 +131,7 @@ export type WritableLayoutNodeAtom = WritableAtom, [value: Layo export type WritableLayoutTreeStateAtom = WritableAtom, [value: LayoutTreeState], void>; export type ContentRenderer = (data: T, ready: boolean, onClose?: () => void) => React.ReactNode; + +export interface LayoutNodeWaveObj extends WaveObj { + node: LayoutNode; +} diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 78f1dea9..cfa74d35 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -1,8 +1,6 @@ // Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { LayoutNode } from "../faraday"; - declare global { type UIContext = { windowid: string; @@ -70,7 +68,7 @@ declare global { version: number; name: string; blockids: string[]; - layout: LayoutNode; + layoutNode: string; }; type Point = { diff --git a/pkg/filestore/blockstore_test.go b/pkg/filestore/blockstore_test.go index 1fda2e00..e5ba928a 100644 --- a/pkg/filestore/blockstore_test.go +++ b/pkg/filestore/blockstore_test.go @@ -79,7 +79,7 @@ func TestCreate(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "testfile", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) @@ -153,7 +153,7 @@ func TestDelete(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "testfile", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) @@ -216,7 +216,7 @@ func TestSetMeta(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "testfile", nil, FileOptsType{}) if err != nil { t.Fatalf("error creating file: %v", err) @@ -319,7 +319,7 @@ func TestAppend(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() fileName := "t2" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { @@ -347,7 +347,7 @@ func TestWriteFile(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() fileName := "t3" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { @@ -391,7 +391,7 @@ func TestCircularWrites(t *testing.T) { defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() err := WFS.MakeFile(ctx, zoneId, "c1", nil, FileOptsType{Circular: true, MaxSize: 50}) if err != nil { t.Fatalf("error creating file: %v", err) @@ -478,7 +478,7 @@ func TestMultiPart(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() fileName := "m2" data := makeText(80) err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) @@ -554,7 +554,7 @@ func TestSimpleDBFlush(t *testing.T) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() fileName := "t1" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { @@ -586,7 +586,7 @@ func TestConcurrentAppend(t *testing.T) { defer cleanupDb(t) ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() - zoneId := uuid.New().String() + zoneId := uuid.NewString() fileName := "t1" err := WFS.MakeFile(ctx, zoneId, fileName, nil, FileOptsType{}) if err != nil { diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 3f569cab..fcffe6e4 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -147,11 +147,11 @@ func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wsto return updatesRtn(ctx, rtn) } -func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string, newLayout any) (any, error) { +func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string) (any, error) { ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) defer cancelFn() ctx = wstore.ContextWithUpdates(ctx) - err := wstore.DeleteBlock(ctx, uiContext.ActiveTabId, blockId, newLayout) + err := wstore.DeleteBlock(ctx, uiContext.ActiveTabId, blockId) if err != nil { return nil, fmt.Errorf("error deleting block: %w", err) } diff --git a/pkg/wshrpc/rpc_client.go b/pkg/wshrpc/rpc_client.go index 425fae7c..9495b9c6 100644 --- a/pkg/wshrpc/rpc_client.go +++ b/pkg/wshrpc/rpc_client.go @@ -199,7 +199,7 @@ func (c *RpcClient) removeReqInfo(rpcId string, clearSend bool) { } func (c *RpcClient) SimpleReq(ctx context.Context, command string, data any) (any, error) { - rpcId := uuid.New().String() + rpcId := uuid.NewString() seqNum := c.NextSeqNum.Add(1) var timeoutInfo *TimeoutInfo deadline, ok := ctx.Deadline() @@ -235,7 +235,7 @@ func (c *RpcClient) SimpleReq(ctx context.Context, command string, data any) (an } func (c *RpcClient) StreamReq(ctx context.Context, command string, data any, respTimeout time.Duration) (chan *RpcPacket, error) { - rpcId := uuid.New().String() + rpcId := uuid.NewString() seqNum := c.NextSeqNum.Add(1) var timeoutInfo *TimeoutInfo = &TimeoutInfo{RespPacketTimeout: respTimeout.Milliseconds()} deadline, ok := ctx.Deadline() diff --git a/pkg/wstore/wstore.go b/pkg/wstore/wstore.go index a9d95e48..24747dd3 100644 --- a/pkg/wstore/wstore.go +++ b/pkg/wstore/wstore.go @@ -140,13 +140,19 @@ func CreateTab(ctx context.Context, workspaceId string, name string) (*Tab, erro if ws == nil { return nil, fmt.Errorf("workspace not found: %q", workspaceId) } + layoutNodeId := uuid.NewString() tab := &Tab{ - OID: uuid.New().String(), - Name: name, - BlockIds: []string{}, + OID: uuid.NewString(), + Name: name, + BlockIds: []string{}, + LayoutNode: layoutNodeId, + } + layoutNode := &LayoutNode{ + OID: layoutNodeId, } ws.TabIds = append(ws.TabIds, tab.OID) DBInsert(tx.Context(), tab) + DBInsert(tx.Context(), layoutNode) DBUpdate(tx.Context(), ws) return tab, nil }) @@ -154,7 +160,7 @@ func CreateTab(ctx context.Context, workspaceId string, name string) (*Tab, erro func CreateWorkspace(ctx context.Context) (*Workspace, error) { ws := &Workspace{ - OID: uuid.New().String(), + OID: uuid.NewString(), TabIds: []string{}, } DBInsert(ctx, ws) @@ -185,7 +191,7 @@ func CreateBlock(ctx context.Context, tabId string, blockDef *BlockDef, rtOpts * if tab == nil { return nil, fmt.Errorf("tab not found: %q", tabId) } - blockId := uuid.New().String() + blockId := uuid.NewString() blockData := &Block{ OID: blockId, BlockDef: blockDef, @@ -210,7 +216,7 @@ func findStringInSlice(slice []string, val string) int { return -1 } -func DeleteBlock(ctx context.Context, tabId string, blockId string, newLayout any) error { +func DeleteBlock(ctx context.Context, tabId string, blockId string) error { return WithTx(ctx, func(tx *TxWrap) error { tab, _ := DBGet[*Tab](tx.Context(), tabId) if tab == nil { @@ -221,11 +227,8 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string, newLayout an return nil } tab.BlockIds = append(tab.BlockIds[:blockIdx], tab.BlockIds[blockIdx+1:]...) - if newLayout != nil { - tab.Layout = newLayout - } DBUpdate(tx.Context(), tab) - DBDelete(tx.Context(), "block", blockId) + DBDelete(tx.Context(), OType_Block, blockId) return nil }) } @@ -246,9 +249,10 @@ func CloseTab(ctx context.Context, workspaceId string, tabId string) error { } ws.TabIds = append(ws.TabIds[:tabIdx], ws.TabIds[tabIdx+1:]...) DBUpdate(tx.Context(), ws) - DBDelete(tx.Context(), "tab", tabId) + DBDelete(tx.Context(), OType_Tab, tabId) + DBDelete(tx.Context(), OType_LayoutNode, tab.LayoutNode) for _, blockId := range tab.BlockIds { - DBDelete(tx.Context(), "block", blockId) + DBDelete(tx.Context(), OType_Block, blockId) } return nil }) @@ -300,11 +304,12 @@ func EnsureInitialData() error { if clientCount > 0 { return nil } - windowId := uuid.New().String() - workspaceId := uuid.New().String() - tabId := uuid.New().String() + windowId := uuid.NewString() + workspaceId := uuid.NewString() + tabId := uuid.NewString() + layoutNodeId := uuid.NewString() client := &Client{ - OID: uuid.New().String(), + OID: uuid.NewString(), MainWindowId: windowId, } err = DBInsert(ctx, client) @@ -339,13 +344,22 @@ func EnsureInitialData() error { return fmt.Errorf("error inserting workspace: %w", err) } tab := &Tab{ - OID: tabId, - Name: "Tab-1", - BlockIds: []string{}, + OID: tabId, + Name: "Tab-1", + BlockIds: []string{}, + LayoutNode: layoutNodeId, } err = DBInsert(ctx, tab) if err != nil { return fmt.Errorf("error inserting tab: %w", err) } + + layoutNode := &LayoutNode{ + OID: layoutNodeId, + } + err = DBInsert(ctx, layoutNode) + if err != nil { + return fmt.Errorf("error inserting layout node: %w", err) + } return nil } diff --git a/pkg/wstore/wstore_types.go b/pkg/wstore/wstore_types.go index 036d16ce..356e5626 100644 --- a/pkg/wstore/wstore_types.go +++ b/pkg/wstore/wstore_types.go @@ -21,6 +21,15 @@ const ( UpdateType_Delete = "delete" ) +const ( + OType_Client = "client" + OType_Window = "window" + OType_Workspace = "workspace" + OType_Tab = "tab" + OType_LayoutNode = "layout" + OType_Block = "block" +) + type WaveObjUpdate struct { UpdateType string `json:"updatetype"` OType string `json:"otype"` @@ -51,7 +60,7 @@ type Client struct { } func (*Client) GetOType() string { - return "client" + return OType_Client } // stores the ui-context of the window @@ -69,7 +78,7 @@ type Window struct { } func (*Window) GetOType() string { - return "window" + return OType_Window } type Workspace struct { @@ -81,20 +90,31 @@ type Workspace struct { } func (*Workspace) GetOType() string { - return "workspace" + return OType_Workspace } type Tab struct { - OID string `json:"oid"` - Version int `json:"version"` - Name string `json:"name"` - Layout any `json:"layout,omitempty"` - BlockIds []string `json:"blockids"` - Meta map[string]any `json:"meta"` + OID string `json:"oid"` + Version int `json:"version"` + Name string `json:"name"` + LayoutNode string `json:"layoutNode"` + BlockIds []string `json:"blockids"` + Meta map[string]any `json:"meta"` } func (*Tab) GetOType() string { - return "tab" + return OType_Tab +} + +type LayoutNode struct { + OID string `json:"oid"` + Version int `json:"version"` + Node any `json:"node,omitempty"` + Meta map[string]any `json:"meta,omitempty"` +} + +func (*LayoutNode) GetOType() string { + return OType_LayoutNode } type FileDef struct { @@ -138,7 +158,7 @@ type Block struct { } func (*Block) GetOType() string { - return "block" + return OType_Block } func AllWaveObjTypes() []reflect.Type { @@ -148,5 +168,6 @@ func AllWaveObjTypes() []reflect.Type { reflect.TypeOf(&Workspace{}), reflect.TypeOf(&Tab{}), reflect.TypeOf(&Block{}), + reflect.TypeOf(&LayoutNode{}), } }