mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
6ca6f7068c
commit_hash:38abb7f9e9f06b0407ea79074ba775ec5b9db34a
500 lines
14 KiB
Swift
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
|
|
}
|