Files
booster 6ca6f7068c Refined match_parent inside wrap_content layout
commit_hash:38abb7f9e9f06b0407ea79074ba775ec5b9db34a
2026-06-01 00:36:15 +03:00

500 lines
14 KiB
Swift

import CoreGraphics
import LayoutKit
import VGSL
extension DivContainer: DivBlockModeling {
public func makeBlock(context: DivBlockModelingContext) throws -> Block {
let context = modifiedContextParentPath(context)
return try applyBaseProperties(
to: { try makeBaseBlock(context: context) },
context: context,
actionsHolder: self,
applyPaddings: false,
clipToBounds: resolveClipToBounds(context.expressionResolver)
)
}
var nonNilItems: [Div] {
items ?? []
}
private func makeBaseBlock(context: DivBlockModelingContext) throws -> Block {
let expressionResolver = context.expressionResolver
let params = DivContainerParams(
orientation: resolveOrientation(expressionResolver),
paddings: paddings?.resolve(context) ?? .zero,
aspectRatio: aspect.resolveAspectRatio(expressionResolver),
clipToBounds: resolveClipToBounds(expressionResolver)
)
var block: Block = switch params.orientation {
case .overlap:
try makeOverlapBlock(context: context, params: params)
case .horizontal, .vertical:
try makeContainerBlock(
context: context,
params: params,
layoutMode: resolveLayoutMode(expressionResolver)
)
}
block = block.addingEdgeInsets(params.paddings, clipsToBounds: params.clipToBounds)
if let aspectRatio = params.aspectRatio, block.calculateWidthFirst {
block = block.aspectRatio(aspectRatio)
}
return block
}
private func makeOverlapBlock(
context: DivBlockModelingContext,
params: DivContainerParams
) throws -> Block {
let expressionResolver = context.expressionResolver
let defaultAlignment = BlockAlignment2D(
horizontal: resolveContentAlignmentHorizontal(expressionResolver).alignment,
vertical: resolveContentAlignmentVertical(expressionResolver).alignment
)
let children = makeChildren(
context: context,
mappedBy: { div, block, context in
LayeredBlock.Child(
content: block,
alignment: div.value.resolveAlignment(context, defaultAlignment: defaultAlignment)
)
}
)
return LayeredBlock(
widthTrait: resolveContentWidthTrait(context, paddings: params.paddings),
heightTrait: resolveContentHeightTrait(context, params: params),
children: children
)
}
private func makeContainerBlock(
context: DivBlockModelingContext,
params: DivContainerParams,
layoutMode: LayoutMode
) throws -> Block {
let expressionResolver = context.expressionResolver
let layoutDirection = params.orientation.layoutDirection
let divContentAlignmentVertical = resolveContentAlignmentVertical(expressionResolver)
let divContentAlignmentHorizontal = resolveContentAlignmentHorizontal(expressionResolver)
let axialAlignment = makeAxialAlignment(
layoutDirection,
verticalAlignment: divContentAlignmentVertical,
horizontalAlignment: divContentAlignmentHorizontal,
uiLayoutDirection: context.layoutDirection
)
let crossAlignment = makeCrossAlignment(
layoutDirection,
verticalAlignment: divContentAlignmentVertical,
horizontalAlignment: divContentAlignmentHorizontal,
uiLayoutDirection: context.layoutDirection
)
let defaultCrossAlignment = switch layoutMode {
case .noWrap:
crossAlignment
case .wrap:
ContainerBlock.CrossAlignment.leading
}
let crossSizeIsWrapContent: Bool = switch layoutDirection {
case .horizontal:
getTransformedHeight(context).isIntrinsic && aspect == nil
case .vertical:
getTransformedWidth(context).isIntrinsic
}
let children: [ContainerBlock.Child] = makeChildren(
context: context,
mappedBy: { div, block, context in
let childMatchesParentOnCrossAxis = layoutDirection == .horizontal
? div.isVerticallyMatchParent
: div.isHorizontallyMatchParent
return ContainerBlock.Child(
content: block,
crossAlignment: div.value.crossAlignment(
for: layoutDirection,
context: context
) ?? defaultCrossAlignment,
fillsCrossAxis: crossSizeIsWrapContent && childMatchesParentOnCrossAxis
)
}
)
let separator = resolveSeparator(context, orientation: params.orientation)
let lineSeparator = resolveLineSeparator(context, orientation: params.orientation)
let paddings = params.paddings
return try ContainerBlock(
blockLayoutDirection: context.layoutDirection,
layoutDirection: layoutDirection,
layoutMode: layoutMode.system,
widthTrait: resolveContentWidthTrait(context, paddings: paddings),
heightTrait: resolveContentHeightTrait(context, params: params),
axialAlignment: axialAlignment,
crossAlignment: crossAlignment,
children: children,
separator: separator,
lineSeparator: lineSeparator,
clipContent: params.clipToBounds && paddings == .zero,
path: context.path
)
}
private func createSpacingSeparator(
spacing: Int,
orientation: DivContainer.Orientation,
isLineSpacing: Bool
) -> ContainerBlock.Child? {
guard spacing > 0 else { return nil }
let direction: SeparatorBlock.Direction
switch orientation {
case .horizontal:
direction = isLineSpacing ? .vertical : .horizontal
case .vertical:
direction = isLineSpacing ? .horizontal : .vertical
case .overlap:
return nil
}
let spacingBlock = SeparatorBlock(
color: .clear,
direction: direction,
size: CGFloat(spacing)
)
return ContainerBlock.Child(content: spacingBlock)
}
private func resolveContentHeightTrait(
_ context: DivBlockModelingContext,
params: DivContainerParams
) -> LayoutTrait {
if params.aspectRatio != nil {
return .resizable
}
return resolveContentHeightTrait(context, paddings: params.paddings)
}
private func resolveSeparator(
_ context: DivBlockModelingContext,
orientation: DivContainer.Orientation
) -> ContainerBlock.Separator? {
let expressionResolver = context.expressionResolver
let itemSpacing = resolveItemSpacing(expressionResolver)
let betweenSeparator = createSpacingSeparator(
spacing: itemSpacing,
orientation: orientation,
isLineSpacing: false
)
guard let separator else {
return betweenSeparator.flatMap {
ContainerBlock.Separator(start: nil, end: nil, between: $0)
}
}
let separatorBlock = separator.style.makeBlock(
context: context, corners: .all
).addingEdgeInsets(separator.margins.resolve(context))
let style = ContainerBlock.Child(
content: separatorBlock,
crossAlignment: .center
)
let showBetween = separator.resolveShowBetween(expressionResolver)
if showBetween, betweenSeparator != nil {
let error = DivBlockModelingWarning(
"item_spacing will be ignored due to the 'separator' property.",
path: context.path
)
context.errorsStorage.add(error)
}
return ContainerBlock.Separator(
start: separator.resolveShowAtStart(expressionResolver) ? style : nil,
end: separator.resolveShowAtEnd(expressionResolver) ? style : nil,
between: showBetween ? style : betweenSeparator
)
}
private func resolveLineSeparator(
_ context: DivBlockModelingContext,
orientation: DivContainer.Orientation
) -> ContainerBlock.Separator? {
let expressionResolver = context.expressionResolver
let lineSpacing = resolveLineSpacing(expressionResolver)
let betweenSeparator = createSpacingSeparator(
spacing: lineSpacing,
orientation: orientation,
isLineSpacing: true
)
guard let lineSeparator else {
return betweenSeparator.flatMap {
ContainerBlock.Separator(start: nil, end: nil, between: $0)
}
}
let lineSeparatorBlock = lineSeparator.style.makeBlock(
context: context, corners: .all
).addingEdgeInsets(lineSeparator.margins.resolve(context))
let style = ContainerBlock.Child(
content: lineSeparatorBlock,
crossAlignment: .center
)
let showBetween = lineSeparator.resolveShowBetween(expressionResolver)
if showBetween, betweenSeparator != nil {
let error = DivBlockModelingWarning(
"line_spacing will be ignored due to the 'line_separator' property.",
path: context.path
)
context.errorsStorage.add(error)
}
return ContainerBlock.Separator(
start: lineSeparator.resolveShowAtStart(expressionResolver) ? style : nil,
end: lineSeparator.resolveShowAtEnd(expressionResolver) ? style : nil,
between: lineSeparator.resolveShowBetween(expressionResolver) ? style : betweenSeparator
)
}
}
extension DivBase {
fileprivate func crossAlignment(
for direction: ContainerBlock.LayoutDirection,
context: DivBlockModelingContext
) -> ContainerBlock.CrossAlignment? {
let expressionResolver = context.expressionResolver
switch direction {
case .horizontal:
return resolveAlignmentVertical(expressionResolver)?.crossAlignment
case .vertical:
return resolveAlignmentHorizontal(expressionResolver)?.makeCrossAlignment(
layoutDirection: context.layoutDirection
)
}
}
}
extension DivContainer.Orientation {
fileprivate var layoutDirection: ContainerBlock.LayoutDirection {
switch self {
case .vertical:
return .vertical
case .horizontal:
return .horizontal
case .overlap:
assertionFailure("layout direction for overlap")
return .vertical
}
}
}
extension DivAlignmentHorizontal {
func alignment(isRTLLayout: Bool) -> Alignment {
switch self {
case .left:
isRTLLayout ? .trailing : .leading
case .right:
isRTLLayout ? .leading : .trailing
case .start:
.leading
case .center:
.center
case .end:
.trailing
}
}
fileprivate func makeCrossAlignment(
layoutDirection: UserInterfaceLayoutDirection
) -> ContainerBlock.CrossAlignment {
switch self {
case .left:
.leading
case .right:
.trailing
case .start:
layoutDirection == .leftToRight ? .leading : .trailing
case .center:
.center
case .end:
layoutDirection == .rightToLeft ? .leading : .trailing
}
}
}
extension DivAlignmentVertical {
var alignment: Alignment {
switch self {
case .top:
return .leading
case .center:
return .center
case .bottom:
return .trailing
case .baseline:
DivKitLogger.warning("Baseline alignment not supported.")
return .leading
}
}
fileprivate var crossAlignment: ContainerBlock.CrossAlignment {
switch self {
case .top:
.leading
case .center:
.center
case .bottom:
.trailing
case .baseline:
.baseline
}
}
}
extension DivContentAlignmentHorizontal {
fileprivate var alignment: Alignment {
switch self {
case .left, .start:
return .leading
case .center:
return .center
case .right, .end:
return .trailing
case .spaceEvenly, .spaceAround, .spaceBetween:
DivKitLogger.warning("Alignment \(rawValue) is not supported")
return .leading
}
}
}
extension DivContentAlignmentVertical {
fileprivate var alignment: Alignment {
switch self {
case .top:
return .leading
case .center:
return .center
case .bottom:
return .trailing
case .spaceEvenly, .spaceAround, .spaceBetween, .baseline:
DivKitLogger.warning("Alignment \(rawValue) is not supported")
return .leading
}
}
}
extension DivContainer.LayoutMode {
fileprivate var system: ContainerBlock.LayoutMode {
switch self {
case .noWrap:
.noWrap
case .wrap:
.wrap
}
}
}
fileprivate func makeCrossAlignment(
_ direction: ContainerBlock.LayoutDirection,
verticalAlignment: DivContentAlignmentVertical,
horizontalAlignment: DivContentAlignmentHorizontal,
uiLayoutDirection: UserInterfaceLayoutDirection = .leftToRight
) -> ContainerBlock.CrossAlignment {
switch direction {
case .horizontal:
switch verticalAlignment {
case .top:
.leading
case .center:
.center
case .bottom:
.trailing
case .baseline:
.baseline
case .spaceBetween, .spaceEvenly, .spaceAround:
.leading
}
case .vertical:
switch horizontalAlignment {
case .left:
uiLayoutDirection == .leftToRight ? .leading : .trailing
case .right:
uiLayoutDirection == .rightToLeft ? .leading : .trailing
case .start:
.leading
case .center:
.center
case .end:
.trailing
case .spaceBetween, .spaceEvenly, .spaceAround:
.center
}
}
}
fileprivate func makeAxialAlignment(
_ direction: ContainerBlock.LayoutDirection,
verticalAlignment: DivContentAlignmentVertical,
horizontalAlignment: DivContentAlignmentHorizontal,
uiLayoutDirection: UserInterfaceLayoutDirection = .leftToRight
) -> ContainerBlock.AxialAlignment {
switch direction {
case .horizontal:
switch horizontalAlignment {
case .left:
return .leading
case .right:
return .trailing
case .start:
return uiLayoutDirection == .leftToRight ? .leading : .trailing
case .center:
return .center
case .end:
return uiLayoutDirection == .rightToLeft ? .leading : .trailing
case .spaceBetween:
return .spaceBetween
case .spaceAround:
return .spaceAround
case .spaceEvenly:
return .spaceEvenly
}
case .vertical:
switch verticalAlignment {
case .top:
return .leading
case .center:
return .center
case .bottom:
return .trailing
case .baseline:
DivKitLogger.warning("Baseline alignment not supported.")
return .leading
case .spaceBetween:
return .spaceBetween
case .spaceAround:
return .spaceAround
case .spaceEvenly:
return .spaceEvenly
}
}
}
private struct DivContainerParams {
let orientation: DivContainer.Orientation
let paddings: EdgeInsets
let aspectRatio: CGFloat?
let clipToBounds: Bool
}