Files
booster 5f737443ce Fixed accessibility merging
commit_hash:bd1ab1f35f3c7f568d0355c82eb0f0ad2fa12e50
2026-04-30 18:28:56 +03:00

376 lines
9.4 KiB
Swift

import CoreGraphics
import LayoutKit
final class DivBaseBlockBuilder {
enum Visibility {
case visible(Block, alpha: CGFloat)
case invisible(Block)
case gone
var isVisible: Bool {
if case .visible = self { return true }
return false
}
}
private let context: DivBlockModelingContext
private let visibility: Visibility
private let div: DivBase
private let clipToBounds: Bool
private let extensionHandlers: [DivExtensionHandler]
private let identity: String
private let isFocused: Bool
private lazy var rotation = self.div.transform?.resolveRotation(self.expressionResolver)
private lazy var border = isFocused ? div.focus?.border ?? div.border : div.border
private lazy var boundary: BoundaryTrait? = if !clipToBounds {
.noClip
} else if let border {
.clipCorner(border.resolveCornerRadii(expressionResolver))
} else {
nil
}
private lazy var shadow: BlockShadow? = border?.resolveShadow(expressionResolver)
private lazy var finalClipToBounds: Bool = [nil, 0.0].contains(rotation) && shadow == nil
private lazy var block: Block = switch visibility {
case let .visible(block, _), let .invisible(block):
block
case .gone:
makeGoneBlock(div: div)
}
private var expressionResolver: ExpressionResolver {
context.expressionResolver
}
private var path: UIElementPath {
context.path
}
private var statePath: DivStatePath {
context.parentDivStatePath ?? DivData.rootPath
}
init(
context: DivBlockModelingContext,
visibility: Visibility,
div: DivBase,
clipToBounds: Bool,
extensionHandlers: [DivExtensionHandler],
identity: String,
isFocused: Bool
) throws {
self.context = context
self.visibility = visibility
self.div = div
self.clipToBounds = clipToBounds
self.extensionHandlers = extensionHandlers
self.identity = identity
self.isFocused = isFocused
}
func applyExtensionHandlers(
stage: ApplyExtensionHandlersStage
) -> Self {
block = block.applyExtensionHandlers(
order: stage,
div: div,
extensionHandlers: extensionHandlers,
context: context
)
return self
}
func applyEdgeInsets(
applyPaddings: Bool
) -> Self {
block = block.addingEdgeInsets(
applyPaddings ? div.paddings.resolve(context) : .zero,
clipsToBounds: clipToBounds
)
return self
}
func applyExternalEdgeInsets(
externalInsets: EdgeInsets
) -> Self {
block = block.addingEdgeInsets(
externalInsets,
clipsToBounds: visibility.isVisible ? false : clipToBounds
)
return self
}
func applyBlockFinalDecorations() -> Self {
switch visibility {
case let .visible(_, alpha):
block = block
.addingDecorations(
boundary: finalClipToBounds ? nil : .noClip,
alpha: alpha
)
case .invisible:
block = block.addingDecorations(alpha: 0)
case .gone:
assertionFailure("This function should not be called for \".gone\" visibility")
}
return self
}
func applyBackground() -> Self {
guard let backgrounds = getBackground(isFocused) else {
return self
}
// optimization for the most common case: saves Array alloc/dealloc
if backgrounds.count == 1 {
guard let background = backgrounds[0].makeBlockBackground(context: context) else {
return self
}
if case let .solidColor(color) = background {
block = block.addingDecorations(backgroundColor: color)
return self
}
block = BackgroundBlock(
background: background,
child: block
)
return self
}
let blockBackgrounds = backgrounds.compactMap {
$0.makeBlockBackground(context: context)
}
guard let background = blockBackgrounds.composite() else {
return self
}
block = BackgroundBlock(
background: background,
child: block
)
return self
}
func applyVisibilityActionsAndDecorations(
customAccessibilityParams: CustomAccessibilityParams
) throws -> Self {
// We can't put container properties into single container.
// Properties should be applied in specific order.
// For example, shadow should be applied to block with border
// and alpha should be applied to block with border and shadow.
var visibilityActions = div.makeVisibilityActions(
actionsType: .appear,
context: context
)
visibilityActions += div.makeVisibilityActions(
actionsType: .disappear,
context: context
)
let visibilityParams = context.makeVisibilityParams(
actions: visibilityActions,
isVisible: true
)
let accessibilityElement = (div.accessibility ?? DivAccessibility()).resolve(
expressionResolver,
id: context.currentDivId,
block: block,
customParams: customAccessibilityParams
)
block = try block
.addingDecorations(
boundary: boundary,
border: border?.resolveBorder(expressionResolver),
shadow: shadow,
visibilityParams: visibilityParams,
tooltips: div.tooltips.makeTooltips(context: context),
accessibilityElement: accessibilityElement,
isFocused: isFocused
)
return self
}
func applyTransitioningAnimations() -> Self {
let expressionResolver = context.expressionResolver
let initialAnimationIn = div.transitionIn?.resolveAnimations(
expressionResolver,
type: .appearing
)
let animationIn: [TransitioningAnimation]? = if isAppearing() {
initialAnimationIn
} else {
nil
}
let animationOut = div.transitionOut?.resolveAnimations(expressionResolver, type: .disappearing)
let animationChange = div.transitionChange?.resolveTransition(expressionResolver)
if animationIn != nil || animationOut != nil || animationChange != nil {
block = DetachableAnimationBlock(
child: block,
id: identity,
animationIn: animationIn,
animationOut: animationOut,
animationChange: animationChange
)
addAnimationWarningsIfNeeded(
isPresentAnimationIn: initialAnimationIn != nil
)
}
return self
}
func applyActions(
actionsHolder: DivActionsHolder?
) -> Self {
block = block
.addActions(
context: context,
actionsHolder: actionsHolder,
clipToBounds: finalClipToBounds
)
return self
}
func applyTransformations() -> Self {
block = block
.addingTransformations(
transform: div.transform,
rotation: rotation,
expressionResolver: expressionResolver
)
return self
}
func setBlockLayoutProvider(_ layoutProvider: DivLayoutProvider?) -> Self {
if let layoutProvider {
block = layoutProvider.apply(block: block, context: context)
}
return self
}
func setBlockReuseId(_ reuseId: String?) -> Self {
guard let reuseId else { return self }
block = block.addingDecorations(
reuseId: reuseId
)
return self
}
func setupContextForBlockVisibility() -> Self {
if visibility.isVisible {
context.lastVisibleBoundsCache.onBecomeVisible(path)
} else {
context.lastVisibleBoundsCache.onBecomeInvisible(path)
}
context.stateManager.setBlockVisibility(
statePath: statePath,
resolvedId: identity,
div: div,
isVisible: visibility.isVisible
)
return self
}
func build() -> Block {
block
}
private func makeGoneBlock(
div: DivBase
) -> Block {
context.stateManager.setBlockVisibility(
statePath: statePath,
div: div,
isVisible: false
)
if let visibilityParams = context.makeVisibilityParams(
actions: div.makeVisibilityActions(
actionsType: .disappear,
context: context
),
isVisible: false
) {
return EmptyBlock.zeroSized.addingDecorations(
visibilityParams: visibilityParams,
isEmpty: true
)
}
context.lastVisibleBoundsCache.onBecomeInvisible(path)
return EmptyBlock.zeroSized
}
private func getBackground(_ isFocused: Bool) -> [DivBackground]? {
guard isFocused else {
return div.background
}
return div.focus?.background ?? div.background
}
private func isAppearing() -> Bool {
let stateManager = context.stateManager
if stateManager.shouldBlockAppearWithTransition(path: statePath + identity) {
return true
}
return stateManager.isBlockAdded(identity, stateBlockPath: statePath.stateBlockPath)
}
private func addAnimationWarningsIfNeeded(isPresentAnimationIn: Bool) {
guard context.currentDivId == nil,
let block = block as? DetachableAnimationBlock else { return }
func warningMessage(_ animation: String) -> String {
"The component id with the \(animation) property for state change is missing. Either specify the id, or specify the \"transition_trigger\" property without \"state_change\" value."
}
if isPresentAnimationIn,
div.transitionTriggersOrDefault.contains(.stateChange) {
context.addWarning(
message: warningMessage("\"transition_in\"")
)
}
if block.animationOut != nil,
div.transitionTriggersOrDefault.contains(.stateChange) {
context.addWarning(
message: warningMessage("\"transition_out\"")
)
}
if block.animationChange != nil {
context.addWarning(
message: warningMessage("\"transition_change\"")
)
}
}
}