@@ -55,6 +55,29 @@ type PaneParams = {
5555} ;
5656
5757type SplitDirection = 'row' | 'column' ;
58+ type DockviewGridNode = DockviewLayout [ 'grid' ] [ 'root' ] ;
59+ type DockviewLeafNode = {
60+ type : 'leaf' ;
61+ data : {
62+ id : string ;
63+ views : string [ ] ;
64+ activeView ?: string ;
65+ } ;
66+ } ;
67+ type DockviewBranchNode = {
68+ type : 'branch' ;
69+ data : DockviewGridNode [ ] ;
70+ } ;
71+
72+ type LayoutSiblingNode = {
73+ groupIds : string [ ] ;
74+ } ;
75+
76+ type LayoutSiblingInfo = {
77+ direction : 'horizontal' | 'vertical' ;
78+ siblings : LayoutSiblingNode [ ] ;
79+ targetIndex : number ;
80+ } ;
5881
5982type SplitProps = {
6083 direction : SplitDirection ;
@@ -76,6 +99,62 @@ const splitOrientation = (direction: SplitDirection): Orientation => (
7699 direction === 'column' ? Orientation . VERTICAL : Orientation . HORIZONTAL
77100) ;
78101
102+ const getNextLayoutDirection = ( direction : 'horizontal' | 'vertical' ) : 'horizontal' | 'vertical' => (
103+ direction === 'horizontal' ? 'vertical' : 'horizontal'
104+ ) ;
105+
106+ const getLayoutDirection = ( orientation : Orientation ) : 'horizontal' | 'vertical' => (
107+ orientation === Orientation . HORIZONTAL ? 'horizontal' : 'vertical'
108+ ) ;
109+
110+ const isDockviewLeafNode = ( node : DockviewGridNode ) : node is DockviewLeafNode => (
111+ node . type === 'leaf'
112+ ) ;
113+
114+ const getLeafGroupIds = ( node : DockviewGridNode ) : string [ ] => {
115+ if ( isDockviewLeafNode ( node ) ) {
116+ return [ node . data . id ] ;
117+ }
118+
119+ const branch = node as unknown as DockviewBranchNode ;
120+ return branch . data . flatMap ( getLeafGroupIds ) ;
121+ } ;
122+
123+ const findLayoutSiblings = (
124+ node : DockviewGridNode ,
125+ targetGroupId : string ,
126+ direction : 'horizontal' | 'vertical' ,
127+ ) : LayoutSiblingInfo | null => {
128+ if ( isDockviewLeafNode ( node ) ) {
129+ return null ;
130+ }
131+
132+ const branch = node as unknown as DockviewBranchNode ;
133+ const directTargetIndex = branch . data . findIndex ( ( child ) => (
134+ isDockviewLeafNode ( child ) && child . data . id === targetGroupId
135+ ) ) ;
136+
137+ if ( directTargetIndex !== - 1 ) {
138+ return {
139+ direction,
140+ siblings : branch . data . map ( ( child ) => ( {
141+ groupIds : getLeafGroupIds ( child ) ,
142+ } ) ) ,
143+ targetIndex : directTargetIndex ,
144+ } ;
145+ }
146+
147+ const nextDirection = getNextLayoutDirection ( direction ) ;
148+ for ( const child of branch . data ) {
149+ const nested = findLayoutSiblings ( child , targetGroupId , nextDirection ) ;
150+ if ( nested ) {
151+ return nested ;
152+ }
153+ }
154+
155+ return null ;
156+ } ;
157+
79158export const Split : React . FC < SplitProps > = ( { direction, panes, className } ) => {
80159 const apiRef = useRef < SplitviewApi | null > ( null ) ;
81160 const containerClassName = useMemo ( ( ) => (
@@ -483,7 +562,7 @@ const shouldUseSavedLayout = (layoutState: ReturnType<typeof loadLayoutState>):
483562
484563 return Object . keys ( layout . panels ) . some ( ( panelKey ) => {
485564 const layoutPanelId = getLayoutPanelId ( panelKey ) ;
486- return layoutPanelId !== null && layoutPanelId !== 'toolbar' ;
565+ return layoutPanelId !== null && layoutPanelId !== 'toolbar' && layoutPanelId !== 'picker' ;
487566 } ) ;
488567} ;
489568
@@ -741,6 +820,7 @@ export const Layout: React.FC<LayoutProps> = ({
741820 const apiRef = useRef < DockviewApi | null > ( null ) ;
742821 const [ visibility , setVisibility ] = useState < PanelVisibility > ( loadPanelVisibility ) ;
743822 const listenersRef = useRef < Array < { dispose : ( ) => void } > > ( [ ] ) ;
823+ const parentGroupMap = useRef < Map < string , string > > ( new Map ( Object . entries ( loadItem < Record < string , string > > ( 'layoutParentGroups' ) ?? { } ) ) ) ;
744824 const layoutSaveTimerRef = useRef < number | null > ( null ) ;
745825 const emptyPaneRestoreTimerRef = useRef < number | null > ( null ) ;
746826 const isRestoringLayoutRef = useRef < boolean > ( false ) ;
@@ -940,6 +1020,20 @@ export const Layout: React.FC<LayoutProps> = ({
9401020 }
9411021 layoutSaveTimerRef . current = window . setTimeout ( ( ) => {
9421022 layoutSaveTimerRef . current = null ;
1023+
1024+ // Keep parentGroupMap clean by only keeping entries for groups that still exist
1025+ const currentGroupIds = new Set ( api . groups . map ( g => g . id ) ) ;
1026+ let mapChanged = false ;
1027+ for ( const key of parentGroupMap . current . keys ( ) ) {
1028+ if ( ! currentGroupIds . has ( key ) ) {
1029+ parentGroupMap . current . delete ( key ) ;
1030+ mapChanged = true ;
1031+ }
1032+ }
1033+ if ( mapChanged ) {
1034+ saveItem ( 'layoutParentGroups' , Object . fromEntries ( parentGroupMap . current . entries ( ) ) ) ;
1035+ }
1036+
9431037 const snapshot = getLayoutSnapshot ( api ) ;
9441038 if ( snapshot === lastLayoutSnapshotRef . current ) {
9451039 return ;
@@ -1114,6 +1208,11 @@ export const Layout: React.FC<LayoutProps> = ({
11141208 } ) ;
11151209 pickerPanel . group . api . setConstraints ( PANEL_CONSTRAINTS ) ;
11161210
1211+ if ( direction === 'right' || direction === 'below' ) {
1212+ parentGroupMap . current . set ( pickerPanel . group . id , referencePanel . group . id ) ;
1213+ saveItem ( 'layoutParentGroups' , Object . fromEntries ( parentGroupMap . current . entries ( ) ) ) ;
1214+ }
1215+
11171216 if ( splitSize . width !== undefined || splitSize . height !== undefined ) {
11181217 window . requestAnimationFrame ( ( ) => {
11191218 referencePanel . group . api . setSize ( splitSize ) ;
@@ -1157,7 +1256,96 @@ export const Layout: React.FC<LayoutProps> = ({
11571256 addPickerPanel ( api , panel . id , 'within' ) ;
11581257 }
11591258
1259+ const group = panel . group ;
1260+ const shouldRestoreProportions = group && group . panels . length === 1 ;
1261+
1262+ interface SiblingGroupSize {
1263+ id : string ;
1264+ width : number ;
1265+ height : number ;
1266+ }
1267+
1268+ let siblingSizes : SiblingGroupSize [ ] = [ ] ;
1269+ let layoutSiblingInfo : LayoutSiblingInfo | null = null ;
1270+ let absorberGroupIds : string [ ] = [ ] ;
1271+ let closedGroupSize = 0 ;
1272+
1273+ if ( shouldRestoreProportions ) {
1274+ const layout = api . toJSON ( ) ;
1275+ layoutSiblingInfo = findLayoutSiblings (
1276+ layout . grid . root ,
1277+ group . id ,
1278+ getLayoutDirection ( layout . grid . orientation ) ,
1279+ ) ;
1280+
1281+ if ( layoutSiblingInfo ) {
1282+ closedGroupSize = layoutSiblingInfo . direction === 'horizontal'
1283+ ? group . api . width
1284+ : group . api . height ;
1285+
1286+ const siblingGroupIds = layoutSiblingInfo . siblings
1287+ . flatMap ( ( sibling ) => sibling . groupIds )
1288+ . filter ( ( id ) => id !== group . id ) ;
1289+
1290+ siblingSizes = siblingGroupIds
1291+ . map ( ( id ) => {
1292+ const siblingGroup = api . groups . find ( ( g ) => g . id === id ) ;
1293+ return siblingGroup
1294+ ? {
1295+ id,
1296+ width : siblingGroup . api . width ,
1297+ height : siblingGroup . api . height ,
1298+ }
1299+ : null ;
1300+ } )
1301+ . filter ( ( size ) : size is SiblingGroupSize => size !== null ) ;
1302+ }
1303+ }
1304+
1305+ const parentGroupId = group ? parentGroupMap . current . get ( group . id ) : undefined ;
1306+
1307+ if ( layoutSiblingInfo ) {
1308+ const parentSibling = parentGroupId
1309+ ? layoutSiblingInfo . siblings . find ( ( sibling ) => sibling . groupIds . includes ( parentGroupId ) )
1310+ : undefined ;
1311+ const adjacentSibling = layoutSiblingInfo . siblings [
1312+ layoutSiblingInfo . targetIndex > 0
1313+ ? layoutSiblingInfo . targetIndex - 1
1314+ : layoutSiblingInfo . targetIndex + 1
1315+ ] ;
1316+ absorberGroupIds = ( parentSibling ?? adjacentSibling ) ?. groupIds ?? [ ] ;
1317+ }
1318+
1319+ if ( group ) {
1320+ parentGroupMap . current . delete ( group . id ) ;
1321+ saveItem ( 'layoutParentGroups' , Object . fromEntries ( parentGroupMap . current . entries ( ) ) ) ;
1322+ }
1323+
11601324 panel . api . close ( ) ;
1325+
1326+ if ( layoutSiblingInfo && siblingSizes . length > 0 && absorberGroupIds . length > 0 ) {
1327+ window . requestAnimationFrame ( ( ) => {
1328+ siblingSizes . forEach ( ( sibling ) => {
1329+ const remainingGroup = api . groups . find ( ( g ) => g . id === sibling . id ) ;
1330+ if ( ! remainingGroup ) return ;
1331+
1332+ const isAbsorber = absorberGroupIds . includes ( sibling . id ) ;
1333+ const targetSize : { width ?: number ; height ?: number } = { } ;
1334+
1335+ if ( layoutSiblingInfo . direction === 'horizontal' ) {
1336+ targetSize . width = isAbsorber
1337+ ? sibling . width + closedGroupSize
1338+ : sibling . width ;
1339+ } else {
1340+ targetSize . height = isAbsorber
1341+ ? sibling . height + closedGroupSize
1342+ : sibling . height ;
1343+ }
1344+
1345+ remainingGroup . api . setSize ( targetSize ) ;
1346+ } ) ;
1347+ } ) ;
1348+ }
11611349 } , [ addPickerPanel ] ) ;
11621350
11631351 useEffect ( ( ) => {
@@ -1296,6 +1484,10 @@ export const Layout: React.FC<LayoutProps> = ({
12961484 isRestoringLayoutRef . current = false ;
12971485 }
12981486
1487+ if ( api . totalPanels === 0 ) {
1488+ addRootPickerPanel ( api , handleSelectPanelFromPicker ) . api . setActive ( ) ;
1489+ }
1490+
12991491 if ( ! restoredSavedLayout ) {
13001492 api . getPanel ( 'files' ) ?. api . setActive ( ) ;
13011493 api . getPanel ( 'editor' ) ?. api . setActive ( ) ;
0 commit comments