/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow */ import type {Rect} from './geometry'; import type {View} from './View'; export type LayoutInfo = {view: View, frame: Rect}; export type Layout = LayoutInfo[]; /** * A function that takes a list of subviews, currently laid out in * `existingLayout`, and lays them out into `containingFrame`. */ export type Layouter = ( existingLayout: Layout, containingFrame: Rect, ) => Layout; function viewToLayoutInfo(view: View): LayoutInfo { return {view, frame: view.frame}; } export function viewsToLayout(views: View[]): Layout { return views.map(viewToLayoutInfo); } /** * Applies `layout`'s `frame`s to its corresponding `view`. */ export function collapseLayoutIntoViews(layout: Layout) { layout.forEach(({view, frame}) => view.setFrame(frame)); } /** * A no-operation layout; does not modify the layout. */ export const noopLayout: Layouter = layout => layout; /** * Layer views on top of each other. All views' frames will be set to `containerFrame`. * * Equivalent to composing: * - `alignToContainerXLayout`, * - `alignToContainerYLayout`, * - `containerWidthLayout`, and * - `containerHeightLayout`. */ export const layeredLayout: Layouter = (layout, containerFrame) => { return layout.map(layoutInfo => ({...layoutInfo, frame: containerFrame})); }; /** * Stacks `views` vertically in `frame`. * All views in `views` will have their widths set to the frame's width. */ export const verticallyStackedLayout: Layouter = (layout, containerFrame) => { let currentY = containerFrame.origin.y; return layout.map(layoutInfo => { const desiredSize = layoutInfo.view.desiredSize(); const height = desiredSize ? desiredSize.height : containerFrame.origin.y + containerFrame.size.height - currentY; const proposedFrame = { origin: {x: containerFrame.origin.x, y: currentY}, size: {width: containerFrame.size.width, height}, }; currentY += height; return { ...layoutInfo, frame: proposedFrame, }; }); }; /** * A layouter that aligns all frames' lefts to the container frame's left. */ export const alignToContainerXLayout: Layouter = (layout, containerFrame) => { return layout.map(layoutInfo => ({ ...layoutInfo, frame: { origin: { x: containerFrame.origin.x, y: layoutInfo.frame.origin.y, }, size: layoutInfo.frame.size, }, })); }; /** * A layouter that aligns all frames' tops to the container frame's top. */ export const alignToContainerYLayout: Layouter = (layout, containerFrame) => { return layout.map(layoutInfo => ({ ...layoutInfo, frame: { origin: { x: layoutInfo.frame.origin.x, y: containerFrame.origin.y, }, size: layoutInfo.frame.size, }, })); }; /** * A layouter that sets all frames' widths to `containerFrame.size.width`. */ export const containerWidthLayout: Layouter = (layout, containerFrame) => { return layout.map(layoutInfo => ({ ...layoutInfo, frame: { origin: layoutInfo.frame.origin, size: { width: containerFrame.size.width, height: layoutInfo.frame.size.height, }, }, })); }; /** * A layouter that sets all frames' heights to `containerFrame.size.height`. */ export const containerHeightLayout: Layouter = (layout, containerFrame) => { return layout.map(layoutInfo => ({ ...layoutInfo, frame: { origin: layoutInfo.frame.origin, size: { width: layoutInfo.frame.size.width, height: containerFrame.size.height, }, }, })); }; /** * A layouter that sets all frames' heights to the desired height of its view. * If the view has no desired size, the frame's height is set to 0. */ export const desiredHeightLayout: Layouter = layout => { return layout.map(layoutInfo => { const desiredSize = layoutInfo.view.desiredSize(); const height = desiredSize ? desiredSize.height : 0; return { ...layoutInfo, frame: { origin: layoutInfo.frame.origin, size: { width: layoutInfo.frame.size.width, height, }, }, }; }); }; /** * A layouter that sets all frames' heights to the height of the tallest frame. */ export const uniformMaxSubviewHeightLayout: Layouter = layout => { const maxHeight = Math.max( ...layout.map(layoutInfo => layoutInfo.frame.size.height), ); return layout.map(layoutInfo => ({ ...layoutInfo, frame: { origin: layoutInfo.frame.origin, size: { width: layoutInfo.frame.size.width, height: maxHeight, }, }, })); }; /** * A layouter that sets heights in this fashion: * - If a frame's height >= `containerFrame.size.height`, the frame is left unchanged. * - Otherwise, sets the frame's height to `containerFrame.size.height`. */ export const atLeastContainerHeightLayout: Layouter = ( layout, containerFrame, ) => { return layout.map(layoutInfo => ({ ...layoutInfo, frame: { origin: layoutInfo.frame.origin, size: { width: layoutInfo.frame.size.width, height: Math.max( containerFrame.size.height, layoutInfo.frame.size.height, ), }, }, })); }; /** * Create a layouter that applies each layouter in `layouters` in sequence. */ export function createComposedLayout(...layouters: Layouter[]): Layouter { if (layouters.length === 0) { return noopLayout; } const composedLayout: Layouter = (layout, containerFrame) => { return layouters.reduce( (intermediateLayout, layouter) => layouter(intermediateLayout, containerFrame), layout, ); }; return composedLayout; }