import { cloneElement, createLayout, getElementBounds, getElementsBounds, Group, type Bounds, type GroupProps, type JSXElement, } from '../../jsx'; export interface FlexLayoutProps extends GroupProps { flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse'; justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between'; alignItems?: 'flex-start' | 'flex-end' | 'center'; alignContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between'; flexWrap?: 'wrap' | 'nowrap'; gap?: number; } export const FlexLayout = createLayout( ( children, { flexDirection = 'row', justifyContent = 'flex-start', alignItems = 'flex-start', alignContent = 'flex-start', flexWrap = 'nowrap', gap = 0, ...props }, ) => { if (!children || children.length === 0) { return ; } const isRow = flexDirection === 'row' || flexDirection === 'row-reverse'; const isReverse = flexDirection === 'row-reverse' || flexDirection === 'column-reverse'; const childBounds = children.map((child) => getElementBounds(child)); const childrenBounds = getElementsBounds(children); const containerWidth = props.width ?? childrenBounds.width; const containerHeight = props.height ?? childrenBounds.height; const hasContainerSize = props.width !== undefined && props.height !== undefined; const lines: Array<{ children: JSXElement[]; bounds: Bounds[] }> = []; if (flexWrap === 'wrap' && hasContainerSize) { let currentLine: JSXElement[] = []; let currentLineBounds: Bounds[] = []; let currentLineSize = 0; for (let i = 0; i < children.length; i++) { const child = children[i]; const bounds = childBounds[i]; const childSize = isRow ? bounds.width : bounds.height; const maxSize = isRow ? containerWidth : containerHeight; if ( currentLine.length === 0 || currentLineSize + gap + childSize <= maxSize ) { currentLine.push(child); currentLineBounds.push(bounds); currentLineSize += (currentLine.length > 1 ? gap : 0) + childSize; } else { lines.push({ children: currentLine, bounds: currentLineBounds }); currentLine = [child]; currentLineBounds = [bounds]; currentLineSize = childSize; } } if (currentLine.length > 0) { lines.push({ children: currentLine, bounds: currentLineBounds }); } } else { lines.push({ children, bounds: childBounds }); } const layoutChildren: JSXElement[] = []; let currentCrossPos = 0; const crossSizes: number[] = []; lines.forEach((line) => { const { children: lineChildren, bounds: lineBounds } = line; const totalMainSize = lineBounds.reduce((sum, bounds, index) => { const childMainSize = isRow ? bounds.width : bounds.height; return sum + childMainSize + (index > 0 ? gap : 0); }, 0); const maxCrossSize = Math.max( ...lineBounds.map((bounds) => (isRow ? bounds.height : bounds.width)), ); crossSizes.push(maxCrossSize); let mainStart = 0; const availableMainSpace = (isRow ? containerWidth : containerHeight) - totalMainSize; if (hasContainerSize) { switch (justifyContent) { case 'flex-end': mainStart = availableMainSpace; break; case 'center': mainStart = availableMainSpace / 2; break; case 'space-between': mainStart = 0; break; default: mainStart = 0; break; } } let itemSpacing = gap; if ( hasContainerSize && justifyContent === 'space-between' && lineChildren.length > 1 ) { itemSpacing = availableMainSpace / (lineChildren.length - 1) + gap; } let currentMainPos = mainStart; lineChildren.forEach((child, childIndex) => { const bounds = lineBounds[childIndex]; const childMainSize = isRow ? bounds.width : bounds.height; const childCrossSize = isRow ? bounds.height : bounds.width; const mainOffset = isRow ? bounds.x : bounds.y; const crossOffset = isRow ? bounds.y : bounds.x; let crossPos = currentCrossPos; if (hasContainerSize) { switch (alignItems) { case 'flex-end': crossPos = currentCrossPos + maxCrossSize - childCrossSize; break; case 'center': crossPos = currentCrossPos + (maxCrossSize - childCrossSize) / 2; break; default: crossPos = currentCrossPos; break; } } let x: number, y: number; if (isRow) { x = isReverse ? containerWidth - currentMainPos - childMainSize - mainOffset : currentMainPos - mainOffset; y = crossPos - crossOffset; } else { x = crossPos - crossOffset; y = isReverse ? containerHeight - currentMainPos - childMainSize - mainOffset : currentMainPos - mainOffset; } const clonedChild = cloneElement(child, { x, y }); layoutChildren.push(clonedChild); currentMainPos += childMainSize + itemSpacing; }); currentCrossPos += maxCrossSize + gap; }); if (lines.length > 1 && hasContainerSize) { const totalCrossSize = crossSizes.reduce((sum, size) => sum + size, 0) + (lines.length - 1) * gap; const availableCrossSpace = (isRow ? containerHeight : containerWidth) - totalCrossSize; let crossOffset = 0; switch (alignContent) { case 'flex-end': crossOffset = availableCrossSpace; break; case 'center': crossOffset = availableCrossSpace / 2; break; case 'space-between': if (lines.length > 1) { const lineSpacing = availableCrossSpace / (lines.length - 1); let currentOffset = 0; lines.forEach((line, lineIndex) => { const lineStartIndex = lines .slice(0, lineIndex) .reduce((sum, l) => sum + l.children.length, 0); const lineEndIndex = lineStartIndex + line.children.length; for (let i = lineStartIndex; i < lineEndIndex; i++) { const child = layoutChildren[i]; const newProps = { ...child.props }; if (isRow) { newProps.y = (newProps.y || 0) + currentOffset; } else { newProps.x = (newProps.x || 0) + currentOffset; } layoutChildren[i] = cloneElement(child, newProps); } currentOffset += crossSizes[lineIndex] + gap + (lineIndex < lines.length - 1 ? lineSpacing : 0); }); break; } break; default: crossOffset = 0; break; } if (crossOffset !== 0 && alignContent !== 'space-between') { layoutChildren.forEach((child, index) => { const newProps = { ...child.props }; if (isRow) { newProps.y = (newProps.y || 0) + crossOffset; } else { newProps.x = (newProps.x || 0) + crossOffset; } layoutChildren[index] = cloneElement(child, newProps); }); } } if (!hasContainerSize) { if (alignItems === 'center') { if (isRow) { const maxHeight = Math.max(...childBounds.map((b) => b.height)); layoutChildren.forEach((child, index) => { const bounds = childBounds[index]; const centerOffset = (maxHeight - bounds.height) / 2; const newProps = { ...child.props }; newProps.y = (newProps.y || 0) + centerOffset; layoutChildren[index] = cloneElement(child, newProps); }); } else { const maxWidth = Math.max(...childBounds.map((b) => b.width)); layoutChildren.forEach((child, index) => { const bounds = childBounds[index]; const centerOffset = (maxWidth - bounds.width) / 2; const newProps = { ...child.props }; newProps.x = (newProps.x || 0) + centerOffset; layoutChildren[index] = cloneElement(child, newProps); }); } } } const finalBounds = getElementsBounds(layoutChildren); const containerProps = { ...props, x: props.x ?? childrenBounds.x, y: props.y ?? childrenBounds.y, width: props.width ?? finalBounds.width, height: props.height ?? finalBounds.height, }; return {layoutChildren}; }, );