diff --git a/Core/BaseTinyPublic/iOS.swift b/Core/BaseTinyPublic/iOS.swift index 63a37a35..03e3df52 100644 --- a/Core/BaseTinyPublic/iOS.swift +++ b/Core/BaseTinyPublic/iOS.swift @@ -10,6 +10,14 @@ public typealias SystemShadow = NSShadow public typealias EdgeInsets = UIEdgeInsets +public typealias UserInterfaceLayoutDirection = UIUserInterfaceLayoutDirection + +extension UIUserInterfaceLayoutDirection { + public static var system: UIUserInterfaceLayoutDirection { + return UIApplication.shared.userInterfaceLayoutDirection + } +} + extension NSShadow { public var cgColor: CGColor? { (shadowColor as? UIColor)?.cgColor } } diff --git a/DivKit/Actions/DivActionHandler.swift b/DivKit/Actions/DivActionHandler.swift index 6500dd95..83efad40 100644 --- a/DivKit/Actions/DivActionHandler.swift +++ b/DivKit/Actions/DivActionHandler.swift @@ -33,7 +33,8 @@ public final class DivActionHandler { patchProvider: DivPatchProvider, variablesStorage: DivVariablesStorage = DivVariablesStorage(), updateCard: @escaping DivActionURLHandler.UpdateCardAction, - showTooltip: @escaping DivActionURLHandler.ShowTooltipAction, + showTooltip: DivActionURLHandler.ShowTooltipAction?, + tooltipActionPerformer: TooltipActionPerformer? = nil, logger: DivActionLogger = EmptyDivActionLogger(), trackVisibility: @escaping TrackVisibility = { _, _ in }, trackDisappear: @escaping TrackVisibility = { _, _ in }, @@ -47,6 +48,7 @@ public final class DivActionHandler { variableUpdater: variablesStorage, updateCard: updateCard, showTooltip: showTooltip, + tooltipActionPerformer: tooltipActionPerformer, performTimerAction: performTimerAction ), logger: logger, diff --git a/DivKit/Actions/DivActionURLHandler.swift b/DivKit/Actions/DivActionURLHandler.swift index 8c675389..2defbac6 100644 --- a/DivKit/Actions/DivActionURLHandler.swift +++ b/DivKit/Actions/DivActionURLHandler.swift @@ -31,7 +31,8 @@ public final class DivActionURLHandler { private let patchProvider: DivPatchProvider private let variableUpdater: DivVariableUpdater private let updateCard: UpdateCardAction - private let showTooltip: ShowTooltipAction + private let showTooltip: ShowTooltipAction? + private let tooltipActionPerformer: TooltipActionPerformer? private let performTimerAction: PerformTimerAction public init( @@ -40,7 +41,8 @@ public final class DivActionURLHandler { patchProvider: DivPatchProvider, variableUpdater: DivVariableUpdater, updateCard: @escaping UpdateCardAction, - showTooltip: @escaping ShowTooltipAction, + showTooltip: ShowTooltipAction?, + tooltipActionPerformer: TooltipActionPerformer?, performTimerAction: @escaping PerformTimerAction = { _, _, _ in } ) { self.stateUpdater = stateUpdater @@ -49,6 +51,7 @@ public final class DivActionURLHandler { self.variableUpdater = variableUpdater self.updateCard = updateCard self.showTooltip = showTooltip + self.tooltipActionPerformer = tooltipActionPerformer self.performTimerAction = performTimerAction } @@ -67,9 +70,17 @@ public final class DivActionURLHandler { switch intent { case let .showTooltip(id, multiple): - showTooltip(TooltipInfo(id: id, showsOnStart: false, multiple: multiple)) - case .hideTooltip: - return false + let tooltipInfo = TooltipInfo(id: id, showsOnStart: false, multiple: multiple) + guard showTooltip == nil else { + showTooltip?(tooltipInfo) + break + } + tooltipActionPerformer?.showTooltip(info: tooltipInfo) + case let .hideTooltip(id): + guard showTooltip == nil else { + break + } + tooltipActionPerformer?.hideTooltip(id: id) case let .download(patchUrl): patchProvider.getPatch( url: patchUrl, diff --git a/DivKit/DivBlockModelingContext.swift b/DivKit/DivBlockModelingContext.swift index d84adea3..4ac2c4f0 100644 --- a/DivKit/DivBlockModelingContext.swift +++ b/DivKit/DivBlockModelingContext.swift @@ -22,22 +22,28 @@ public struct DivBlockModelingContext { public let extensionHandlers: [String: DivExtensionHandler] public let stateInterceptors: [String: DivStateInterceptor] private let variables: DivVariables - public let layoutDirection: LayoutDirection + public let layoutDirection: UserInterfaceLayoutDirection + public let debugParams: DebugParams + public let playerFactory: PlayerFactory? + public var childrenA11yDescription: String? + public weak var parentScrollView: ScrollView? + public let errorsStorage: DivErrorsStorage + private let variableTracker: DivVariableTracker? + + var overridenWidth: DivOverridenSize? + var overridenHeight: DivOverridenSize? + public var expressionResolver: ExpressionResolver { ExpressionResolver( variables: variables, errorTracker: { [weak errorsStorage] error in errorsStorage?.add(DivBlockModelingRuntimeError(error, path: parentPath)) + }, + variableTracker: { [weak variableTracker] variables in + variableTracker?.onVariablesUsed(cardId: cardId, variables: variables) } ) } - public let debugParams: DebugParams - public let playerFactory: PlayerFactory? - public var childrenA11yDescription: String? - public weak var parentScrollView: ScrollView? - public let errorsStorage: DivErrorsStorage - var overridenWidth: DivOverridenSize? - var overridenHeight: DivOverridenSize? public init( cardId: DivCardID, @@ -60,7 +66,8 @@ public struct DivBlockModelingContext { childrenA11yDescription: String? = nil, parentScrollView: ScrollView? = nil, errorsStorage: DivErrorsStorage = DivErrorsStorage(errors: []), - layoutDirection: LayoutDirection = .system + layoutDirection: UserInterfaceLayoutDirection = UserInterfaceLayoutDirection.system, + variableTracker: DivVariableTracker? = nil ) { self.cardId = cardId self.cardLogId = cardLogId @@ -74,14 +81,14 @@ public struct DivBlockModelingContext { self.divCustomBlockFactory = divCustomBlockFactory self.flagsInfo = flagsInfo self.fontProvider = fontProvider ?? DefaultFontProvider() + self.variables = variables self.playerFactory = playerFactory self.debugParams = debugParams self.childrenA11yDescription = childrenA11yDescription self.parentScrollView = parentScrollView self.errorsStorage = errorsStorage self.layoutDirection = layoutDirection - - self.variables = variables + self.variableTracker = variableTracker var extensionsHandlersDictionary = [String: DivExtensionHandler]() extensionHandlers.forEach { diff --git a/DivKit/DivKitComponents.swift b/DivKit/DivKitComponents.swift index c6219ce0..19499a1a 100644 --- a/DivKit/DivKitComponents.swift +++ b/DivKit/DivKitComponents.swift @@ -16,12 +16,13 @@ public final class DivKitComponents { public let flagsInfo: DivFlagsInfo public let fontProvider: DivFontProvider public let imageHolderFactory: ImageHolderFactory - public let layoutDirection: LayoutDirection + public let layoutDirection: UserInterfaceLayoutDirection public let patchProvider: DivPatchProvider public let playerFactory: PlayerFactory? public let safeAreaManager: DivSafeAreaManager - public let showToolip: DivActionURLHandler.ShowTooltipAction public let stateManagement: DivStateManagement + public let showToolip: DivActionURLHandler.ShowTooltipAction? + public let tooltipManager: TooltipManager public let triggersStorage: DivTriggersStorage public let urlOpener: UrlOpener public let variablesStorage: DivVariablesStorage @@ -29,6 +30,8 @@ public final class DivKitComponents { private let timerStorage: DivTimerStorage private let updateAggregator: RunLoopCardUpdateAggregator + private let updateCard: DivActionURLHandler.UpdateCardAction + private let variableTracker = DivVariableTracker() private let disposePool = AutodisposePool() public init( @@ -37,11 +40,12 @@ public final class DivKitComponents { flagsInfo: DivFlagsInfo = .default, fontProvider: DivFontProvider? = nil, imageHolderFactory: ImageHolderFactory? = nil, - layoutDirection: LayoutDirection = .system, + layoutDirection: UserInterfaceLayoutDirection = UserInterfaceLayoutDirection.system, patchProvider: DivPatchProvider? = nil, requestPerformer: URLRequestPerforming? = nil, - showTooltip: @escaping DivActionURLHandler.ShowTooltipAction = { _ in }, + showTooltip: DivActionURLHandler.ShowTooltipAction? = nil, stateManagement: DivStateManagement = DefaultDivStateManagement(), + tooltipManager: TooltipManager? = nil, trackVisibility: @escaping DivActionHandler.TrackVisibility = { _, _ in }, trackDisappear: @escaping DivActionHandler.TrackVisibility = { _, _ in }, updateCardAction: UpdateCardAction?, @@ -62,6 +66,9 @@ public final class DivKitComponents { safeAreaManager = DivSafeAreaManager(storage: variablesStorage) + updateAggregator = RunLoopCardUpdateAggregator(updateCardAction: updateCardAction ?? { _ in }) + updateCard = updateAggregator.aggregate(_:) + let requestPerformer = requestPerformer ?? URLRequestPerformer(urlTransform: nil) self.imageHolderFactory = imageHolderFactory @@ -70,12 +77,19 @@ public final class DivKitComponents { self.patchProvider = patchProvider ?? DivPatchDownloader(requestPerformer: requestPerformer) - let updateAggregator = RunLoopCardUpdateAggregator(updateCardAction: updateCardAction ?? { _ in - }) - self.updateAggregator = updateAggregator - let updateCard: DivActionURLHandler.UpdateCardAction = updateAggregator.aggregate(_:) - weak var weakTimerStorage: DivTimerStorage? + weak var weakActionHandler: DivActionHandler? + + self.tooltipManager = tooltipManager ?? DefaultTooltipManager( + shownDivTooltips: .init(), + handleAction: { + switch $0.payload { + case let .divAction(params: params): + weakActionHandler?.handle(params: params, urlOpener: urlOpener) + default: break + } + } + ) actionHandler = DivActionHandler( stateUpdater: stateManagement, @@ -84,6 +98,7 @@ public final class DivKitComponents { variablesStorage: variablesStorage, updateCard: updateCard, showTooltip: showTooltip, + tooltipActionPerformer: self.tooltipManager, logger: DefaultDivActionLogger( requestPerformer: requestPerformer ), @@ -104,14 +119,12 @@ public final class DivKitComponents { urlOpener: urlOpener, updateCard: updateCard ) + + weakActionHandler = actionHandler weakTimerStorage = timerStorage - variablesStorage.changeEvents.addObserver { change in - switch change.kind { - case .global: - updateCard(.variable(.all)) - case let .local(cardId, _): - updateCard(.variable(.specific([cardId]))) - } + + variablesStorage.changeEvents.addObserver { [weak self] event in + self?.onVariablesChanged(event: event) }.dispose(in: disposePool) } @@ -175,7 +188,8 @@ public final class DivKitComponents { debugParams: DebugParams = DebugParams(), parentScrollView: ScrollView? = nil ) -> DivBlockModelingContext { - DivBlockModelingContext( + variableTracker.onModelingStarted(cardId: cardId) + return DivBlockModelingContext( cardId: cardId, stateManager: stateManagement.getStateManagerForCard(cardId: cardId), blockStateStorage: blockStateStorage, @@ -190,7 +204,8 @@ public final class DivKitComponents { playerFactory: playerFactory, debugParams: debugParams, parentScrollView: parentScrollView, - layoutDirection: layoutDirection + layoutDirection: layoutDirection, + variableTracker: variableTracker ) } @@ -217,6 +232,18 @@ public final class DivKitComponents { public func setTimers(divData: DivData, cardId: DivCardID) { timerStorage.set(cardId: cardId, timers: divData.timers ?? []) } + + private func onVariablesChanged(event: DivVariablesStorage.ChangeEvent) { + switch event.kind { + case let .global(variables): + let cardIds = variableTracker.getAffectedCards(variables: variables) + if (!cardIds.isEmpty) { + updateCard(.variable(.specific(cardIds))) + } + case let .local(cardId, _): + updateCard(.variable(.specific([cardId]))) + } + } } func makeImageHolderFactory(requestPerformer: URLRequestPerforming) -> ImageHolderFactory { diff --git a/DivKit/DivKitInfo.swift b/DivKit/DivKitInfo.swift index e583517c..abb4a32b 100644 --- a/DivKit/DivKitInfo.swift +++ b/DivKit/DivKitInfo.swift @@ -1,3 +1,3 @@ public enum DivKitInfo { - public static let version = "26.1.0" + public static let version = "26.2.0" } diff --git a/DivKit/Expressions/CalcExpression/CalcExpression.swift b/DivKit/Expressions/CalcExpression/CalcExpression.swift index b02c4474..a866bdd9 100644 --- a/DivKit/Expressions/CalcExpression/CalcExpression.swift +++ b/DivKit/Expressions/CalcExpression/CalcExpression.swift @@ -229,7 +229,7 @@ final class CalcExpression: CustomStringConvertible { func makeOutputMessage(for expression: String) -> String { switch self { case let .shortMessage(message): - return "Failed to evaluate [\(expression)]. \(message)" + return "Failed to evaluate [\(expression.escaped)]. \(message)" default: return self.description } @@ -1436,3 +1436,9 @@ extension UnicodeScalarView { } } } + +extension String { + fileprivate var escaped: String { + replacingOccurrences(of: "\\", with: "\\\\") + } +} diff --git a/DivKit/Expressions/ExpressionLink.swift b/DivKit/Expressions/ExpressionLink.swift index 027d100f..4e2aabb4 100644 --- a/DivKit/Expressions/ExpressionLink.swift +++ b/DivKit/Expressions/ExpressionLink.swift @@ -15,24 +15,10 @@ public struct ExpressionLink { let validator: ExpressionValueValidator? let errorTracker: ExpressionErrorTracker? - init?( - expression: String, - validator: ExpressionValueValidator?, - errorTracker: ExpressionErrorTracker? = nil, - resolveNested: Bool = true - ) throws { - try self.init( - rawValue: "@{\(expression)}", - validator: validator, - errorTracker: errorTracker, - resolveNested: resolveNested - ) - } - @usableFromInline init?( rawValue: String, - validator: ExpressionValueValidator?, + validator: ExpressionValueValidator? = nil, errorTracker: ExpressionErrorTracker? = nil, resolveNested: Bool = true ) throws { @@ -63,10 +49,10 @@ public struct ExpressionLink { let value = String(currentValue[start...end]) if resolveNested, let link = try ExpressionLink( rawValue: value, - validator: nil, errorTracker: errorTracker ) { items.append(.nestedCalcExpression(link)) + variablesNames.append(contentsOf: link.variablesNames) } else { let parsedCalcExpression = CalcExpression.parse(value) items.append(.calcExpression(parsedCalcExpression)) diff --git a/DivKit/Expressions/ExpressionResolver.swift b/DivKit/Expressions/ExpressionResolver.swift index 1b16ed66..49683c75 100644 --- a/DivKit/Expressions/ExpressionResolver.swift +++ b/DivKit/Expressions/ExpressionResolver.swift @@ -7,19 +7,24 @@ public typealias ExpressionValueValidator = (T) -> Bool public typealias ExpressionErrorTracker = (ExpressionError) -> Void public final class ExpressionResolver { + public typealias VariableTracker = (Set) -> Void + @usableFromInline let variables: DivVariables private let errorTracker: ExpressionErrorTracker + let variableTracker: VariableTracker public init( variables: DivVariables, - errorTracker: ExpressionErrorTracker? = nil + errorTracker: ExpressionErrorTracker? = nil, + variableTracker: @escaping VariableTracker = { _ in } ) { self.variables = variables self.errorTracker = { DivKitLogger.error($0.description) errorTracker?($0) } + self.variableTracker = variableTracker } public func resolveString(expression: String) -> String { @@ -60,6 +65,7 @@ public final class ExpressionResolver { case let .value(val): return resolveEscaping(val) case let .link(link): + variableTracker(Set(link.variablesNames.map(DivVariableName.init(rawValue:)))) return evaluateStringBasedValue( link: link, initializer: initializer @@ -69,7 +75,29 @@ public final class ExpressionResolver { } } - func resolveEscaping(_ value: T?) -> T? { + func resolveNumericValue( + expression: Expression? + ) -> T? { + switch expression { + case let .value(val): + return val + case let .link(link): + variableTracker(Set(link.variablesNames.map(DivVariableName.init(rawValue:)))) + return evaluateSingleItem(link: link) + case .none: + return nil + } + } + + @inlinable + func getVariableValue(_ name: String) -> T? { + guard let value: T = variables[DivVariableName(rawValue: name)]?.typedValue() else { + return nil + } + return value + } + + private func resolveEscaping(_ value: T?) -> T? { guard var value = value as? String, value.contains("\\") else { return value } @@ -101,24 +129,7 @@ public final class ExpressionResolver { return value as? T } - @inlinable - func resolveNumericValue( - expression: Expression? - ) -> T? { - switch expression { - case let .value(val): - return val - case let .link(link): - return evaluateSingleItem( - link: link - ) - case .none: - return nil - } - } - - @usableFromInline - func evaluateSingleItem(link: ExpressionLink) -> T? { + private func evaluateSingleItem(link: ExpressionLink) -> T? { guard link.items.count == 1, case let .calcExpression(parsedExpression) = link.items.first else { @@ -148,8 +159,7 @@ public final class ExpressionResolver { } } - @usableFromInline - func evaluateStringBasedValue( + private func evaluateStringBasedValue( link: ExpressionLink, initializer: (String) -> T? ) -> T? { @@ -186,8 +196,7 @@ public final class ExpressionResolver { initializer: { $0 } ) { let link = try? ExpressionLink( - expression: expression, - validator: nil, + rawValue: "@{\(expression)}", errorTracker: link.errorTracker, resolveNested: false ) @@ -211,14 +220,6 @@ public final class ExpressionResolver { return validatedValue(value: result, validator: link.validator, rawValue: link.rawValue) } - @inlinable - func getVariableValue(_ name: String) -> T? { - guard let value: T = variables[DivVariableName(rawValue: name)]?.typedValue() else { - return nil - } - return value - } - private func validatedValue( value: T?, validator: ExpressionValueValidator?, @@ -239,7 +240,7 @@ public final class ExpressionResolver { expression: String, initializer: (String) -> T? ) -> T? { - guard let expressionLink = try? ExpressionLink(rawValue: expression, validator: nil) else { + guard let expressionLink = try? ExpressionLink(rawValue: expression) else { return nil } return resolveStringBasedValue( diff --git a/DivKit/Expressions/Functions/StringFunctions.swift b/DivKit/Expressions/Functions/StringFunctions.swift index a1ab0e23..a6044832 100644 --- a/DivKit/Expressions/Functions/StringFunctions.swift +++ b/DivKit/Expressions/Functions/StringFunctions.swift @@ -46,6 +46,7 @@ enum StringFunctions: String, CaseIterable { case decodeUri case padStart case padEnd + case testRegex var declaration: [AnyCalcExpression.Symbol: AnyCalcExpression.SymbolEvaluator] { [.function(rawValue, arity: function.arity): function.symbolEvaluator] @@ -93,6 +94,8 @@ enum StringFunctions: String, CaseIterable { FunctionTernary(impl: _padEndInt), ] ) + case .testRegex: + return FunctionBinary(impl: _testRegex) } } } @@ -230,3 +233,13 @@ extension String { distance(from: startIndex, to: index) } } + +private func _testRegex(text: String, regex: String) throws -> Bool { + do { + let regex = try NSRegularExpression(pattern: regex) + let range = NSRange(text.startIndex..., in: text) + return regex.firstMatch(in: text, range: range) != nil + } catch { + throw CalcExpression.Error.shortMessage("Invalid regular expression.") + } +} diff --git a/DivKit/Expressions/Functions/ValueFunctions.swift b/DivKit/Expressions/Functions/ValueFunctions.swift index 6be942ae..f69092d5 100644 --- a/DivKit/Expressions/Functions/ValueFunctions.swift +++ b/DivKit/Expressions/Functions/ValueFunctions.swift @@ -70,6 +70,7 @@ enum ValueFunctions: String, CaseIterable { extension ExpressionResolver { fileprivate func getValueFunction() -> GetOrDefault { { name, fallbackValue in + self.variableTracker([DivVariableName(rawValue: name)]) guard let value = self.getValue(name) else { return fallbackValue } @@ -81,11 +82,11 @@ extension ExpressionResolver { } } - fileprivate func getValueFunctionWithTransform< - T, - U - >(transform: @escaping (U) throws -> T) -> GetOrDefaultWithTransform { + fileprivate func getValueFunctionWithTransform( + transform: @escaping (U) throws -> T + ) -> GetOrDefaultWithTransform { { name, fallbackValue in + self.variableTracker([DivVariableName(rawValue: name)]) guard let value = self.getValue(name) else { return try transform(fallbackValue) } diff --git a/DivKit/Extensions/DivBase/DivBaseExtensions.swift b/DivKit/Extensions/DivBase/DivBaseExtensions.swift index f9f9bb3c..a9c58a23 100644 --- a/DivKit/Extensions/DivBase/DivBaseExtensions.swift +++ b/DivKit/Extensions/DivBase/DivBaseExtensions.swift @@ -34,7 +34,7 @@ extension DivBase { let internalInsets = options.contains(.noPaddings) ? .zero - : paddings.makeEdgeInsets(context: context) + : paddings.makeEdgeInsets(context: context) block = block.addingEdgeInsets(internalInsets) let externalInsets = margins.makeEdgeInsets(context: context) diff --git a/DivKit/Extensions/DivContainerExtensions.swift b/DivKit/Extensions/DivContainerExtensions.swift index 7fc152f5..ddb097c3 100644 --- a/DivKit/Extensions/DivContainerExtensions.swift +++ b/DivKit/Extensions/DivContainerExtensions.swift @@ -157,13 +157,15 @@ extension DivContainer: DivBlockModeling { let axialAlignment = makeAxialAlignment( layoutDirection, verticalAlignment: divContentAlignmentVertical, - horizontalAlignment: divContentAlignmentHorizontal + horizontalAlignment: divContentAlignmentHorizontal, + uiLayoutDirection: context.layoutDirection ) let crossAlignment = makeCrossAlignment( layoutDirection, verticalAlignment: divContentAlignmentVertical, - horizontalAlignment: divContentAlignmentHorizontal + horizontalAlignment: divContentAlignmentHorizontal, + uiLayoutDirection: context.layoutDirection ) let fallbackWidth = getFallbackWidth( @@ -216,7 +218,7 @@ extension DivContainer: DivBlockModeling { content: block, crossAlignment: div.value.crossAlignment( for: layoutDirection, - expressionResolver: expressionResolver + context: context ) ?? defaultCrossAlignment ) } @@ -229,6 +231,7 @@ extension DivContainer: DivBlockModeling { let widthTrait = makeContentWidthTrait(with: context) let aspectRatio = resolveAspectRatio(expressionResolver) let containerBlock = try ContainerBlock( + blockLayoutDirection: context.layoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode.system, widthTrait: widthTrait, @@ -321,11 +324,12 @@ extension DivContainer: DivBlockModeling { extension DivBase { fileprivate func crossAlignment( for direction: ContainerBlock.LayoutDirection, - expressionResolver: ExpressionResolver + context: DivBlockModelingContext ) -> ContainerBlock.CrossAlignment? { + let expressionResolver = context.expressionResolver switch direction { case .horizontal: return resolveAlignmentVertical(expressionResolver)?.crossAlignment - case .vertical: return resolveAlignmentHorizontal(expressionResolver)?.crossAlignment + case .vertical: return resolveAlignmentHorizontal(expressionResolver)?.makeCrossAlignment(uiLayoutDirection: context.layoutDirection) } } } @@ -347,23 +351,27 @@ extension DivContainer.Orientation { extension DivAlignmentHorizontal { var alignment: Alignment { switch self { - case .left: + case .left, .start: return .leading case .center: return .center - case .right: + case .right, .end: return .trailing } } - var crossAlignment: ContainerBlock.CrossAlignment { + func makeCrossAlignment(uiLayoutDirection: UserInterfaceLayoutDirection) -> ContainerBlock.CrossAlignment { switch self { case .left: return .leading - case .center: - return .center case .right: return .trailing + case .start: + return uiLayoutDirection == .leftToRight ? .leading : .trailing + case .center: + return .center + case .end: + return uiLayoutDirection == .rightToLeft ? .leading : .trailing } } } @@ -400,11 +408,11 @@ extension DivAlignmentVertical { extension DivContentAlignmentHorizontal { fileprivate var alignment: Alignment { switch self { - case .left: + case .left, .start: return .leading case .center: return .center - case .right: + case .right, .end: return .trailing case .spaceEvenly, .spaceAround, .spaceBetween: DivKitLogger.warning("Alignment \(rawValue) is not supported") @@ -482,7 +490,8 @@ private let defaultFallbackSize = DivOverridenSize( fileprivate func makeCrossAlignment( _ direction: ContainerBlock.LayoutDirection, verticalAlignment: DivContentAlignmentVertical, - horizontalAlignment: DivContentAlignmentHorizontal + horizontalAlignment: DivContentAlignmentHorizontal, + uiLayoutDirection: UserInterfaceLayoutDirection = .leftToRight ) -> ContainerBlock.CrossAlignment { switch direction { case .horizontal: @@ -501,10 +510,14 @@ fileprivate func makeCrossAlignment( case .vertical: switch horizontalAlignment { case .left: + return uiLayoutDirection == .leftToRight ? .leading : .trailing + case .right: + return uiLayoutDirection == .rightToLeft ? .leading : .trailing + case .start: return .leading case .center: return .center - case .right: + case .end: return .trailing case .spaceBetween, .spaceEvenly, .spaceAround: return .center @@ -515,16 +528,21 @@ fileprivate func makeCrossAlignment( fileprivate func makeAxialAlignment( _ direction: ContainerBlock.LayoutDirection, verticalAlignment: DivContentAlignmentVertical, - horizontalAlignment: DivContentAlignmentHorizontal + horizontalAlignment: DivContentAlignmentHorizontal, + uiLayoutDirection: UserInterfaceLayoutDirection = .leftToRight ) -> ContainerBlock.AxialAlignment { switch direction { case .horizontal: switch horizontalAlignment { case .left: + return uiLayoutDirection == .leftToRight ? .leading : .trailing + case .right: + return uiLayoutDirection == .rightToLeft ? .leading : .trailing + case .start: return .leading case .center: return .center - case .right: + case .end: return .trailing case .spaceBetween: return .spaceBetween diff --git a/DivKit/Extensions/DivData/DivDataExtensions.swift b/DivKit/Extensions/DivData/DivDataExtensions.swift index d40672c5..67b30cb8 100644 --- a/DivKit/Extensions/DivData/DivDataExtensions.swift +++ b/DivKit/Extensions/DivData/DivDataExtensions.swift @@ -31,6 +31,7 @@ extension DivData: DivBlockModeling { ) return block .addingStateBlock( + stateId: stateId, ids: stateManager.getVisibleIds(statePath: statePath) ) .addingDebugInfo(context: divContext) diff --git a/DivKit/Extensions/DivEdgeInsetsExtensions.swift b/DivKit/Extensions/DivEdgeInsetsExtensions.swift index 91cc61d1..9ef1dfc9 100644 --- a/DivKit/Extensions/DivEdgeInsetsExtensions.swift +++ b/DivKit/Extensions/DivEdgeInsetsExtensions.swift @@ -18,7 +18,7 @@ extension DivEdgeInsets { let end = resolveEnd(context.expressionResolver).flatMap(unit.makeScaledValue) if start != nil || end != nil { - switch (context.layoutDirection.uiLayoutDirection) { + switch context.layoutDirection { case .rightToLeft: right = start ?? 0 left = end ?? 0 diff --git a/DivKit/Extensions/DivImage/DivImageContentMode.swift b/DivKit/Extensions/DivImage/DivImageContentMode.swift index 7274ddb4..c8682ce4 100644 --- a/DivKit/Extensions/DivImage/DivImageContentMode.swift +++ b/DivKit/Extensions/DivImage/DivImageContentMode.swift @@ -53,11 +53,11 @@ extension DivAlignmentVertical { extension DivAlignmentHorizontal { fileprivate var contentModeAlignment: ImageContentMode.HorizontalAlignment { switch self { - case .left: + case .left, .start: return .left case .center: return .center - case .right: + case .right, .end: return .right } } diff --git a/DivKit/Extensions/DivInputExtensions.swift b/DivKit/Extensions/DivInputExtensions.swift index 2f3772bf..990ebcd0 100644 --- a/DivKit/Extensions/DivInputExtensions.swift +++ b/DivKit/Extensions/DivInputExtensions.swift @@ -72,11 +72,11 @@ extension DivInput: DivBlockModeling { extension DivAlignmentHorizontal { fileprivate var system: TextAlignment { switch self { - case .left: + case .left, .start: return .left case .center: return .center - case .right: + case .right, .end: return .right } } diff --git a/DivKit/Extensions/DivStateExtensions.swift b/DivKit/Extensions/DivStateExtensions.swift index c7515458..afe8b52a 100644 --- a/DivKit/Extensions/DivStateExtensions.swift +++ b/DivKit/Extensions/DivStateExtensions.swift @@ -99,6 +99,7 @@ extension DivState: DivBlockModeling { ), ] ).addingStateBlock( + stateId: activeStateId, ids: stateManager.getVisibleIds(statePath: activeStatePath) ) } diff --git a/DivKit/Extensions/DivTextExtensions.swift b/DivKit/Extensions/DivTextExtensions.swift index e54cefd2..a57a5f9d 100644 --- a/DivKit/Extensions/DivTextExtensions.swift +++ b/DivKit/Extensions/DivTextExtensions.swift @@ -256,11 +256,11 @@ extension CFMutableAttributedString { extension DivAlignmentHorizontal { fileprivate var system: TextAlignment { switch self { - case .left: + case .left, .start: return .left case .center: return .center - case .right: + case .right, .end: return .right } } diff --git a/DivKit/Variables/DivTriggersStorage.swift b/DivKit/Variables/DivTriggersStorage.swift index 68d81a7a..3b6eeedb 100644 --- a/DivKit/Variables/DivTriggersStorage.swift +++ b/DivKit/Variables/DivTriggersStorage.swift @@ -142,8 +142,8 @@ extension DivTrigger { extension Expression { fileprivate var variablesNames: Set { switch self { - case let .link(resolver): - return Set(resolver.variablesNames.map(DivVariableName.init(rawValue:))) + case let .link(link): + return Set(link.variablesNames.map(DivVariableName.init(rawValue:))) case .value: return [] } diff --git a/DivKit/Variables/DivVariableTracker.swift b/DivKit/Variables/DivVariableTracker.swift new file mode 100644 index 00000000..1bd87ef1 --- /dev/null +++ b/DivKit/Variables/DivVariableTracker.swift @@ -0,0 +1,25 @@ +import Foundation + +public final class DivVariableTracker { + private var usedVariables: [DivCardID: Set] = [:] + + init() {} + + func onModelingStarted(cardId: DivCardID) { + usedVariables[cardId] = [] + } + + func onVariablesUsed(cardId: DivCardID, variables: Set) { + usedVariables[cardId] = (usedVariables[cardId] ?? []).union(variables) + } + + func getAffectedCards(variables: Set) -> Set { + var cardIds = Set() + usedVariables.forEach { cardId, usedVariables in + if (!usedVariables.isDisjoint(with: variables)) { + cardIds.insert(cardId) + } + } + return cardIds + } +} diff --git a/DivKit/generated_sources/DivAlignmentHorizontal.swift b/DivKit/generated_sources/DivAlignmentHorizontal.swift index 6af0fcd9..2d43280e 100644 --- a/DivKit/generated_sources/DivAlignmentHorizontal.swift +++ b/DivKit/generated_sources/DivAlignmentHorizontal.swift @@ -9,4 +9,6 @@ public enum DivAlignmentHorizontal: String, CaseIterable { case left = "left" case center = "center" case right = "right" + case start = "start" + case end = "end" } diff --git a/DivKit/generated_sources/DivContentAlignmentHorizontal.swift b/DivKit/generated_sources/DivContentAlignmentHorizontal.swift index 7f67224d..774965a1 100644 --- a/DivKit/generated_sources/DivContentAlignmentHorizontal.swift +++ b/DivKit/generated_sources/DivContentAlignmentHorizontal.swift @@ -9,6 +9,8 @@ public enum DivContentAlignmentHorizontal: String, CaseIterable { case left = "left" case center = "center" case right = "right" + case start = "start" + case end = "end" case spaceBetween = "space-between" case spaceAround = "space-around" case spaceEvenly = "space-evenly" diff --git a/LayoutKit/LayoutKit/Base/LayoutDirection.swift b/LayoutKit/LayoutKit/Base/LayoutDirection.swift deleted file mode 100644 index c0e52b3e..00000000 --- a/LayoutKit/LayoutKit/Base/LayoutDirection.swift +++ /dev/null @@ -1,20 +0,0 @@ -import UIKit - -public enum LayoutDirection { - case system - case rightToLeft - case leftToRight -} - -extension LayoutDirection { - public var uiLayoutDirection: UIUserInterfaceLayoutDirection { - switch (self) { - case .leftToRight: - return .leftToRight - case .rightToLeft: - return .rightToLeft - case .system: - return UIApplication.shared.userInterfaceLayoutDirection - } - } -} diff --git a/LayoutKit/LayoutKit/Base/TooltipManager.swift b/LayoutKit/LayoutKit/Base/TooltipManager.swift new file mode 100644 index 00000000..1331ffeb --- /dev/null +++ b/LayoutKit/LayoutKit/Base/TooltipManager.swift @@ -0,0 +1,99 @@ +import BasePublic +import CommonCorePublic +import UIKit + +public protocol TooltipManager: AnyObject, TooltipActionPerformer, RenderingDelegate { + func tooltipAnchorViewAdded(anchorView: TooltipAnchorView) + func tooltipAnchorViewRemoved(anchorView: TooltipAnchorView) +} + +public protocol TooltipActionPerformer { + func showTooltip(info: TooltipInfo) + func hideTooltip(id: String) +} + +extension TooltipManager { + public func mapView(_: BlockView, to _: BlockViewID) {} +} + +public final class DefaultTooltipManager: TooltipManager { + public struct Tooltip { + public let id: String + public let duration: Duration + public let view: VisibleBoundsTrackingView + } + + public var shownDivTooltips: BasePublic.Property> + + private let handleAction: (UIActionEvent) -> Void + private var existingAnchorViews = WeakCollection() + private var showingTooltips = [String: TooltipContainerView]() + + public init( + shownDivTooltips: BasePublic.Property>, + handleAction: @escaping (UIActionEvent) -> Void + ) { + self.handleAction = handleAction + self.shownDivTooltips = shownDivTooltips + } + + public func showTooltip(info: TooltipInfo) { + if !info.multiple, !shownDivTooltips.value.insert(info.id).inserted { return } + guard !showingTooltips.keys.contains(info.id), + let tooltip = existingAnchorViews.compactMap({ $0?.makeTooltip(id: info.id) }).first + else { return } + + let window: UIWindow? + if #available(iOS 13.0, *) { + window = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first + } else { + window = UIApplication.shared.windows.first + } + + let view = TooltipContainerView( + tooltipView: tooltip.view, + tooltipID: tooltip.id, + handleAction: handleAction, + onCloseAction: { [weak self] in self?.showingTooltips.removeValue(forKey: tooltip.id) } + ) + window?.addSubview(view) + view.frame = window?.bounds ?? .zero + showingTooltips[info.id] = view + if !tooltip.duration.value.isZero { + after(tooltip.duration.value, block: { self.hideTooltip(id: tooltip.id) }) + } + } + + public func hideTooltip(id: String) { + guard let tooltipView = showingTooltips[id] else { return } + tooltipView.close() + } + + public func tooltipAnchorViewAdded(anchorView: TooltipAnchorView) { + existingAnchorViews.append(anchorView) + } + + public func tooltipAnchorViewRemoved(anchorView: TooltipAnchorView) { + existingAnchorViews.remove(anchorView) + } +} + +extension TooltipAnchorView { + fileprivate func makeTooltip(id: String) -> DefaultTooltipManager.Tooltip? { + tooltips + .first { $0.id == id } + .flatMap { + let tooltip = $0 + return DefaultTooltipManager.Tooltip( + id: tooltip.id, + duration: tooltip.duration, + view: { + let tooltipView = tooltip.block.makeBlockView() + let frame = tooltip.calculateFrame(targeting: bounds) + tooltipView.frame = convert(frame, to: nil) + return tooltipView + }() + ) + } + } +} diff --git a/LayoutKit/LayoutKit/Blocks/Block.swift b/LayoutKit/LayoutKit/Blocks/Block.swift index ea30ffba..261f065a 100644 --- a/LayoutKit/LayoutKit/Blocks/Block.swift +++ b/LayoutKit/LayoutKit/Blocks/Block.swift @@ -42,9 +42,13 @@ public protocol Block: AnyObject, var minHeight: CGFloat { get } func equals(_ other: Block) -> Bool + + var blockLayoutDirection: UserInterfaceLayoutDirection { get } } extension Block { + public var blockLayoutDirection: UserInterfaceLayoutDirection { .leftToRight } + public var calculateWidthFirst: Bool { true } public func widthOfHorizontallyNonResizableBlock(forHeight _: CGFloat) -> CGFloat { @@ -69,14 +73,18 @@ extension Block { width = widthOfHorizontallyResizableBlock } else { let intrinsicWidth = widthOfHorizontallyNonResizableBlock - width = isHorizontallyConstrained ? min(intrinsicWidth, constrainedWidth) : intrinsicWidth + let constrainedWidth = isHorizontallyConstrained ? min(intrinsicWidth, constrainedWidth) : + intrinsicWidth + width = max(minWidth, constrainedWidth) } let height: CGFloat if isVerticallyResizable { height = heightOfVerticallyResizableBlock } else { let intrinsicHeight = heightOfVerticallyNonResizableBlock(forWidth: width) - height = isVerticallyConstrained ? min(intrinsicHeight, constrainedHeight) : intrinsicHeight + let constrainedHeight = isVerticallyConstrained ? min(intrinsicHeight, constrainedHeight) : + intrinsicHeight + height = max(minHeight, constrainedHeight) } return CGSize(width: width, height: height) } else { @@ -85,14 +93,18 @@ extension Block { height = heightOfVerticallyResizableBlock } else { let intrinsicHeight = heightOfVerticallyNonResizableBlock - height = isVerticallyConstrained ? min(intrinsicHeight, constrainedHeight) : intrinsicHeight + let constrainedHeight = isVerticallyConstrained ? min(intrinsicHeight, constrainedHeight) : + intrinsicHeight + height = max(constrainedHeight, minHeight) } let width: CGFloat if isHorizontallyResizable { width = widthOfHorizontallyResizableBlock } else { let intrinsicWidth = widthOfHorizontallyNonResizableBlock(forHeight: height) - width = isHorizontallyConstrained ? min(intrinsicWidth, constrainedWidth) : intrinsicWidth + let constrainedWidth = isHorizontallyConstrained ? min(intrinsicWidth, constrainedWidth) : + intrinsicWidth + width = max(constrainedWidth, minWidth) } return CGSize(width: width, height: height) } diff --git a/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift b/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift index 09e4ed3f..878dd63f 100644 --- a/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/Container/ContainerBlock.swift @@ -88,6 +88,7 @@ public final class ContainerBlock: BlockWithLayout { var nonResizableSize: (width: CGFloat, height: CGFloat?)? } + public let blockLayoutDirection: UserInterfaceLayoutDirection public let layoutDirection: LayoutDirection public let layoutMode: LayoutMode public let widthTrait: LayoutTrait @@ -107,6 +108,7 @@ public final class ContainerBlock: BlockWithLayout { private var cached = CachedSizes() public init( + blockLayoutDirection: UserInterfaceLayoutDirection = .leftToRight, layoutDirection: LayoutDirection, layoutMode: LayoutMode = .noWrap, widthTrait: LayoutTrait = .resizable, @@ -132,6 +134,7 @@ public final class ContainerBlock: BlockWithLayout { throw Error.childAndGapCountMismatch } + self.blockLayoutDirection = blockLayoutDirection self.layoutDirection = layoutDirection self.layoutMode = layoutMode self.widthTrait = widthTrait @@ -168,6 +171,7 @@ public final class ContainerBlock: BlockWithLayout { separator: separator, lineSeparator: lineSeparator, gaps: gaps, + blockLayoutDirection: blockLayoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode, axialAlignment: axialAlignment, @@ -295,11 +299,13 @@ public final class ContainerBlock: BlockWithLayout { separator: separator, lineSeparator: lineSeparator, gaps: gaps, + blockLayoutDirection: blockLayoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode, axialAlignment: axialAlignment, crossAlignment: crossAlignment, - size: CGSize(width: width, height: .zero) + size: CGSize(width: width, height: .zero), + needCompressConstrainedBlocks: false ) result = layout.blockFrames.map { $0.maxY }.max() ?? 0 case .vertical: @@ -379,6 +385,7 @@ public final class ContainerBlock: BlockWithLayout { separator: separator, lineSeparator: lineSeparator, gaps: gaps, + blockLayoutDirection: blockLayoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode, axialAlignment: axialAlignment, @@ -432,6 +439,7 @@ public final class ContainerBlock: BlockWithLayout { separator: separator, lineSeparator: lineSeparator, gaps: gaps, + blockLayoutDirection: blockLayoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode, axialAlignment: axialAlignment, diff --git a/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift b/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift index d0f3e82f..ef3145f5 100644 --- a/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift +++ b/LayoutKit/LayoutKit/Blocks/Container/ContainerBlockLayout.swift @@ -31,30 +31,36 @@ struct ContainerBlockLayout { public private(set) var blockFrames: [CGRect] = [] public private(set) var ascent: CGFloat? let gaps: [CGFloat] + let blockLayoutDirection: UserInterfaceLayoutDirection let layoutDirection: ContainerBlock.LayoutDirection let layoutMode: ContainerBlock.LayoutMode let axialAlignment: ContainerBlock.AxialAlignment let crossAlignment: ContainerBlock.CrossAlignment let size: CGSize + let needCompressConstrainedBlocks: Bool public init( children: [ContainerBlock.Child], separator: ContainerBlock.Separator? = nil, lineSeparator: ContainerBlock.Separator? = nil, gaps: [CGFloat], + blockLayoutDirection: UserInterfaceLayoutDirection = .leftToRight, layoutDirection: ContainerBlock.LayoutDirection, layoutMode: ContainerBlock.LayoutMode, axialAlignment: ContainerBlock.AxialAlignment, crossAlignment: ContainerBlock.CrossAlignment, - size: CGSize + size: CGSize, + needCompressConstrainedBlocks: Bool = true ) { precondition(gaps.count == children.count + 1) + self.blockLayoutDirection = blockLayoutDirection self.gaps = gaps self.layoutDirection = layoutDirection self.layoutMode = layoutMode self.axialAlignment = axialAlignment self.crossAlignment = crossAlignment self.size = size + self.needCompressConstrainedBlocks = needCompressConstrainedBlocks (self.childrenWithSeparators, self.blockFrames, self.ascent) = calculateBlockFrames( children: children, separator: separator, @@ -84,12 +90,17 @@ struct ContainerBlockLayout { private func calculateNoWrapLayoutFrames( children: [ContainerBlock.Child] ) -> ([ContainerBlock.Child], [CGRect], CGFloat?) { + var children = children var frames = [CGRect]() var containerAscent: CGFloat? let gapsSize = gaps.reduce(0, +) var shift = CGPoint(x: 0, y: 0) switch layoutDirection { case .horizontal: + if blockLayoutDirection == .rightToLeft { + children.reverse() + } + let horizontallyResizableBlocks = children.map { $0.content } .filter { $0.isHorizontallyResizable } let widthOfHorizontallyNonResizableBlocks = @@ -127,7 +138,7 @@ struct ContainerBlockLayout { widthOfHorizontallyResizableBlock: widthIfResizable, heightOfVerticallyResizableBlock: size.height, constrainedWidth: widthIfConstrained, - constrainedHeight: size.height + constrainedHeight: needCompressConstrainedBlocks ? size.height : .infinity ) containerAscent = getMaxAscent( current: containerAscent, child: child, childSize: blockSize @@ -140,55 +151,66 @@ struct ContainerBlockLayout { frames.addBaselineOffset(children: children, ascent: containerAscent) shift.x = axialAlignment.offset(forAvailableSpace: size.width, contentSize: x) case .vertical: - let verticallyResizableBlocks = children.map { $0.content } - .filter { $0.isVerticallyResizable } - let heightOfVerticallyNonResizableBlocks = - heightsOfVerticallyNonResizableBlocksIn(children.map { $0.content }, forWidth: size.width) - .reduce(0, +) - let heightAvailableForResizableBlocks = ( - size.height - heightOfVerticallyNonResizableBlocks - gapsSize - ) - let resizableBlockWeights = verticallyResizableBlocks + let blocks = children.map { $0.content } + + let blockSizes = blocks.map { + ( + $0, + $0 + .sizeFor( + widthOfHorizontallyResizableBlock: size.width, + heightOfVerticallyResizableBlock: size.height, + constrainedWidth: size.width, + constrainedHeight: .infinity + ).height + ) + } + + let heightOfVerticallyNonResizableBlocks = blockSizes.filter { !$0.0.isVerticallyResizable } + .map(\.1).reduce(0, +) + + let heightAvailableForResizableBlocks = size + .height - heightOfVerticallyNonResizableBlocks - gapsSize + + let verticallyResizableBlocks = blocks.filter { $0.isVerticallyResizable } + let verticallyResizableBlocksWeight = verticallyResizableBlocks .map { $0.weightOfVerticallyResizableBlock.rawValue } - let heightAvailablePerWeightUnit = max(0, heightAvailableForResizableBlocks) / - resizableBlockWeights.reduce(0, +) - var y = gaps[0] + .reduce(0, +) var blockMeasure = ResizableBlockMeasure( resizableBlockCount: verticallyResizableBlocks.count, - lengthAvailablePerWeightUnit: heightAvailablePerWeightUnit, + lengthAvailablePerWeightUnit: max(0, heightAvailableForResizableBlocks) / + verticallyResizableBlocksWeight, lengthAvailableForResizableBlocks: heightAvailableForResizableBlocks ) - let verticallyConstrainedBlocks = children.map { $0.content } - .filter { $0.isVerticallyConstrained } var constrainedBlockSizesIterator = decreaseConstrainedBlockSizes( - blockSizes: verticallyConstrainedBlocks - .map { - .init( - size: $0.heightOfVerticallyNonResizableBlock(forWidth: size.width), - minSize: $0.minHeight - ) - }, + blockSizes: blockSizes.filter { $0.0.isVerticallyConstrained }.map { + ConstrainedBlockSize(size: $0.1, minSize: $0.0.minHeight) + }, lengthToDecrease: heightAvailableForResizableBlocks < 0 ? - -heightAvailableForResizableBlocks : - 0 + -heightAvailableForResizableBlocks : 0 ) + var y = gaps[0] zip(children, gaps.dropFirst()).forEach { child, gapAfterBlock in let block = child.content - var width = block.isHorizontallyResizable - ? size.width - : block.widthOfHorizontallyNonResizableBlock - width = block.isHorizontallyConstrained ? min(width, size.width) : width - let alignmentSpace = size.width - width - let x = child.crossAlignment.offset(forAvailableSpace: alignmentSpace) - let heightIfResizable = blockMeasure.measureNext(block.verticalMeasure) - var height = block.isVerticallyResizable - ? heightIfResizable - : block.heightOfVerticallyNonResizableBlock(forWidth: width) - if block.isVerticallyConstrained { + let width: CGFloat + if block.isHorizontallyResizable { + width = size.width + } else { + let intrinsicWidth = block.widthOfHorizontallyNonResizableBlock + width = block.isHorizontallyConstrained ? min(intrinsicWidth, size.width) : intrinsicWidth + } + let height: CGFloat + if block.isVerticallyResizable { + height = blockMeasure.measureNext(block.verticalMeasure) + } else if block.isVerticallyConstrained { height = constrainedBlockSizesIterator.next() ?? 0 + } else { + height = block.heightOfVerticallyNonResizableBlock(forWidth: width) } + let alignmentSpace = size.width - width + let x = child.crossAlignment.offset(forAvailableSpace: alignmentSpace) frames.append(CGRect(x: x, y: y, width: width, height: height)) y += height + gapAfterBlock } diff --git a/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift b/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift index df77ac94..9aef9dbf 100644 --- a/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/ImageRenderableBlock/ContainerBlock+ImageRenderableBlock.swift @@ -10,6 +10,7 @@ extension ContainerBlock: ImageRenderableBlock { separator: separator, lineSeparator: lineSeparator, gaps: gaps, + blockLayoutDirection: blockLayoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode, axialAlignment: axialAlignment, diff --git a/LayoutKit/LayoutKit/Blocks/StateBlock.swift b/LayoutKit/LayoutKit/Blocks/StateBlock.swift index f07b8e47..47822215 100644 --- a/LayoutKit/LayoutKit/Blocks/StateBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/StateBlock.swift @@ -2,13 +2,16 @@ import CommonCorePublic public final class StateBlock: WrapperBlock, LayoutCachingDefaultImpl { public let child: Block + public let stateId: String public let ids: Set public init( child: Block, + stateId: String, ids: Set ) { self.child = child + self.stateId = stateId self.ids = ids } @@ -20,13 +23,14 @@ public final class StateBlock: WrapperBlock, LayoutCachingDefaultImpl { } public func makeCopy(wrapping child: Block) -> StateBlock { - StateBlock(child: child, ids: ids) + StateBlock(child: child, stateId: stateId, ids: ids) } } extension StateBlock: Equatable { public static func ==(lhs: StateBlock, rhs: StateBlock) -> Bool { lhs.child == rhs.child + && lhs.stateId == rhs.stateId && lhs.ids == rhs.ids } } @@ -36,7 +40,10 @@ extension StateBlock: CustomDebugStringConvertible { } extension Block { - public func addingStateBlock(ids: Set) -> Block { - StateBlock(child: self, ids: ids) + public func addingStateBlock( + stateId: String, + ids: Set + ) -> Block { + StateBlock(child: self, stateId: stateId, ids: ids) } } diff --git a/LayoutKit/LayoutKit/Blocks/TextBlock.swift b/LayoutKit/LayoutKit/Blocks/TextBlock.swift index e06e4517..98f08cb5 100644 --- a/LayoutKit/LayoutKit/Blocks/TextBlock.swift +++ b/LayoutKit/LayoutKit/Blocks/TextBlock.swift @@ -124,7 +124,7 @@ public final class TextBlock: BlockWithTraits { public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat { switch heightTrait { - case let .intrinsic(constrained, minSize, maxSize): + case let .intrinsic(_, minSize, maxSize): if let cached = cachedIntrinsicHeight, cached.width.isApproximatelyEqualTo(width) { return cached.height @@ -137,7 +137,7 @@ public final class TextBlock: BlockWithTraits { minNumberOfHiddenLines: minNumberOfHiddenLines ) ) - let result = constrained ? height : clamp(height, min: minSize, max: maxSize) + let result = clamp(height, min: minSize, max: maxSize) cachedIntrinsicHeight = (width: width, height: result) return result case let .fixed(value): diff --git a/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift index 3d28b226..206db51f 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/ContainerBlock+UIViewRenderableBlock.swift @@ -27,6 +27,7 @@ extension ContainerBlock { separator: separator, lineSeparator: lineSeparator, gaps: gaps, + blockLayoutDirection: blockLayoutDirection, layoutDirection: layoutDirection, layoutMode: layoutMode, axialAlignment: axialAlignment, @@ -54,6 +55,7 @@ private final class ContainerBlockView: UIView, BlockViewProtocol, VisibleBounds let separator: ContainerBlock.Separator? let lineSeparator: ContainerBlock.Separator? let gaps: [CGFloat] + let blockLayoutDirection: UserInterfaceLayoutDirection let layoutDirection: ContainerBlock.LayoutDirection let layoutMode: ContainerBlock.LayoutMode let axialAlignment: ContainerBlock.AxialAlignment @@ -143,6 +145,7 @@ private final class ContainerBlockView: UIView, BlockViewProtocol, VisibleBounds separator: model.separator, lineSeparator: model.lineSeparator, gaps: model.gaps, + blockLayoutDirection: model.blockLayoutDirection, layoutDirection: model.layoutDirection, layoutMode: model.layoutMode, axialAlignment: model.axialAlignment, diff --git a/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift index a2b182b3..8c5f2a40 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift @@ -124,6 +124,7 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT private var model: Model! private weak var observer: ElementStateObserver? + private weak var renderingDelegate: RenderingDelegate? private(set) var childView: BlockView? private var contextMenuDelegate: NSObjectProtocol? @@ -289,9 +290,16 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT return } + if self.model?.tooltips.isEmpty ?? true, !model.tooltips.isEmpty { + renderingDelegate?.tooltipAnchorViewAdded(anchorView: self) + } else if self.model?.tooltips.isEmpty == false, model.tooltips.isEmpty { + renderingDelegate?.tooltipAnchorViewRemoved(anchorView: self) + } + let shouldUpdateChildView = model.child !== self.model?.child || self.observer !== observer self.model = model self.observer = observer + self.renderingDelegate = renderingDelegate blurView = model.blurEffect.map { UIVisualEffectView(effect: UIBlurEffect(style: $0.cast())) } @@ -414,6 +422,12 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT duration: tooltipModel.duration ) } + + deinit { + if model?.tooltips.isEmpty == false { + renderingDelegate?.tooltipAnchorViewRemoved(anchorView: self) + } + } } extension DecoratingView { @@ -440,6 +454,10 @@ extension DecoratingView { } } +extension DecoratingView: TooltipAnchorView { + var tooltips: [BlockTooltip] { model.tooltips } +} + extension DecoratingView.HighlightState { var alpha: CGFloat { switch self { diff --git a/LayoutKit/LayoutKit/UI/Blocks/RenderingDelegate.swift b/LayoutKit/LayoutKit/UI/Blocks/RenderingDelegate.swift index 8c4b54cb..1a27dcde 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/RenderingDelegate.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/RenderingDelegate.swift @@ -5,6 +5,8 @@ import CommonCorePublic #if os(iOS) public protocol RenderingDelegate: AnyObject { func mapView(_ view: BlockView, to id: BlockViewID) + func tooltipAnchorViewAdded(anchorView: TooltipAnchorView) + func tooltipAnchorViewRemoved(anchorView: TooltipAnchorView) } public typealias BlockViewID = Tagged @@ -12,6 +14,10 @@ public typealias BlockViewID = Tagged public protocol DivViewMetaProviding: AnyObject { func subview(with id: BlockViewID) -> BlockView? } + +public protocol TooltipAnchorView: ViewType { + var tooltips: [BlockTooltip] { get } +} #else public protocol RenderingDelegate {} public typealias BlockViewID = Tagged diff --git a/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift index be8f3dca..ba9095f4 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/StateBlock+UIViewRenderableBlock.swift @@ -19,6 +19,7 @@ extension StateBlock { ) { (view as! StateBlockView).configure( child: child, + stateId: stateId, ids: Set(ids.map { BlockViewID(rawValue: $0) }), observer: observer, overscrollDelegate: overscrollDelegate, @@ -60,6 +61,14 @@ private final class SubviewStorage: RenderingDelegate { wrappedRenderingDelegate?.mapView(view, to: id) } + func tooltipAnchorViewAdded(anchorView: TooltipAnchorView) { + wrappedRenderingDelegate?.tooltipAnchorViewAdded(anchorView: anchorView) + } + + func tooltipAnchorViewRemoved(anchorView: TooltipAnchorView) { + wrappedRenderingDelegate?.tooltipAnchorViewRemoved(anchorView: anchorView) + } + func getView(_ id: BlockViewID) -> DetachableAnimationBlockView? { views.first { $0.id == id }?.view } @@ -99,6 +108,8 @@ private final class SubviewStorage: RenderingDelegate { private final class StateBlockView: BlockView { private var subviewStorage = SubviewStorage(wrappedRenderingDelegate: nil, ids: []) private var childView: BlockView? + private var stateId: String? + private var isStateChanged = false var effectiveBackgroundColor: UIColor? { childView?.effectiveBackgroundColor } @@ -113,11 +124,17 @@ private final class StateBlockView: BlockView { func configure( child: Block, + stateId: String, ids: Set, observer: ElementStateObserver?, overscrollDelegate: ScrollDelegate?, renderingDelegate: RenderingDelegate? ) { + if (self.stateId != stateId) { + isStateChanged = true + self.stateId = stateId + } + // remove views with unfinished animations subviews.forEach { if $0 !== childView { @@ -171,6 +188,15 @@ private final class StateBlockView: BlockView { } extension StateBlockView: VisibleBoundsTrackingContainer { + func onVisibleBoundsChanged(from: CGRect, to: CGRect) { + if isStateChanged { + isStateChanged = false + passVisibleBoundsChanged(from: .zero, to: to) + } else { + passVisibleBoundsChanged(from: from, to: to) + } + } + var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] { childView.asArray() } diff --git a/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift b/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift new file mode 100644 index 00000000..311b1f39 --- /dev/null +++ b/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift @@ -0,0 +1,73 @@ +import BaseTinyPublic +import CommonCorePublic +import Foundation +import UIKit + +public final class TooltipContainerView: UIView, UIActionEventPerforming { + private let tooltipView: VisibleBoundsTrackingView + private let tooltipID: String + private let handleAction: (LayoutKit.UIActionEvent) -> Void + private let onCloseAction: Action + + private var lastNonZeroBounds: CGRect? + private var onVisibleBoundsChanged: Action? + + public init( + tooltipView: VisibleBoundsTrackingView, + tooltipID: String, + handleAction: @escaping (LayoutKit.UIActionEvent) -> Void, + onCloseAction: @escaping Action + ) { + self.tooltipView = tooltipView + self.tooltipID = tooltipID + self.handleAction = handleAction + self.onCloseAction = onCloseAction + let tooltipBounds = tooltipView.bounds + onVisibleBoundsChanged = { [weak tooltipView] in + tooltipView?.onVisibleBoundsChanged(from: .zero, to: tooltipBounds) + } + super.init(frame: .zero) + addSubview(tooltipView) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + public override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if let touch = touches.first { + let point = touch.location(in: self) + if !tooltipView.point(inside: tooltipView.convert(point, from: self), with: event) { + close() + } + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + if let lastNonZeroBounds = lastNonZeroBounds, + lastNonZeroBounds != bounds { + close() + } + + if bounds != .zero { + lastNonZeroBounds = bounds + } + + onVisibleBoundsChanged?() + onVisibleBoundsChanged = nil + } + + public func perform(uiActionEvent event: LayoutKit.UIActionEvent, from _: AnyObject) { + handleAction(event) + } + + public func close() { + self.tooltipView.onVisibleBoundsChanged(from: tooltipView.bounds, to: .zero) + removeFromParentAnimated(completion: { + self.onCloseAction() + }) + } +} diff --git a/Specs/BasePublic/26.2.0/BasePublic.podspec b/Specs/BasePublic/26.2.0/BasePublic.podspec new file mode 100644 index 00000000..d8e8dce2 --- /dev/null +++ b/Specs/BasePublic/26.2.0/BasePublic.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'BasePublic' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'BaseTinyPublic', s.version.to_s + s.dependency 'BaseUIPublic', s.version.to_s + + s.source_files = [ + 'Core/BasePublic/**/*' + ] +end diff --git a/Specs/BaseTinyPublic/26.2.0/BaseTinyPublic.podspec b/Specs/BaseTinyPublic/26.2.0/BaseTinyPublic.podspec new file mode 100644 index 00000000..31c3e2ff --- /dev/null +++ b/Specs/BaseTinyPublic/26.2.0/BaseTinyPublic.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |s| + s.name = 'BaseTinyPublic' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.source_files = [ + 'Core/BaseTinyPublic/**/*' + ] +end diff --git a/Specs/BaseUIPublic/26.2.0/BaseUIPublic.podspec b/Specs/BaseUIPublic/26.2.0/BaseUIPublic.podspec new file mode 100644 index 00000000..d1f86276 --- /dev/null +++ b/Specs/BaseUIPublic/26.2.0/BaseUIPublic.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'BaseUIPublic' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'BaseTinyPublic', s.version.to_s + + s.source_files = [ + 'Core/BaseUIPublic/**/*' + ] +end diff --git a/Specs/CommonCorePublic/26.2.0/CommonCorePublic.podspec b/Specs/CommonCorePublic/26.2.0/CommonCorePublic.podspec new file mode 100644 index 00000000..75d7accf --- /dev/null +++ b/Specs/CommonCorePublic/26.2.0/CommonCorePublic.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'CommonCorePublic' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'BasePublic', s.version.to_s + + s.source_files = [ + 'Core/CommonCorePublic/**/*' + ] +end diff --git a/Specs/DivKit/26.2.0/DivKit.podspec b/Specs/DivKit/26.2.0/DivKit.podspec new file mode 100644 index 00000000..6e135a9b --- /dev/null +++ b/Specs/DivKit/26.2.0/DivKit.podspec @@ -0,0 +1,25 @@ +Pod::Spec.new do |s| + s.name = 'DivKit' + s.version = '26.2.0' + s.summary = 'DivKit framework' + s.description = 'DivKit is a backend-driven UI framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'CommonCorePublic', s.version.to_s + s.dependency 'LayoutKit', s.version.to_s + s.dependency 'NetworkingPublic', s.version.to_s + s.dependency 'Serialization', s.version.to_s + + s.source_files = [ + 'DivKit/**/*' + ] +end diff --git a/Specs/DivKitExtensions/26.2.0/DivKitExtensions.podspec b/Specs/DivKitExtensions/26.2.0/DivKitExtensions.podspec new file mode 100644 index 00000000..559bf83e --- /dev/null +++ b/Specs/DivKitExtensions/26.2.0/DivKitExtensions.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'DivKitExtensions' + s.version = '26.2.0' + s.summary = 'DivKit framework extensions' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'DivKit', s.version.to_s + + s.source_files = [ + 'DivKitExtensions/**/*' + ] +end diff --git a/Specs/LayoutKit/26.2.0/LayoutKit.podspec b/Specs/LayoutKit/26.2.0/LayoutKit.podspec new file mode 100644 index 00000000..72533ee2 --- /dev/null +++ b/Specs/LayoutKit/26.2.0/LayoutKit.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'LayoutKit' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'CommonCorePublic', s.version.to_s + s.dependency 'LayoutKitInterface', s.version.to_s + + s.source_files = [ + 'LayoutKit/LayoutKit/**/*' + ] +end diff --git a/Specs/LayoutKitInterface/26.2.0/LayoutKitInterface.podspec b/Specs/LayoutKitInterface/26.2.0/LayoutKitInterface.podspec new file mode 100644 index 00000000..4c4b2a1f --- /dev/null +++ b/Specs/LayoutKitInterface/26.2.0/LayoutKitInterface.podspec @@ -0,0 +1,24 @@ +Pod::Spec.new do |s| + s.name = 'LayoutKitInterface' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'BasePublic', s.version.to_s + s.dependency 'BaseTinyPublic', s.version.to_s + s.dependency 'BaseUIPublic', s.version.to_s + + s.source_files = [ + 'LayoutKit/Interface/**/*' + ] +end diff --git a/Specs/NetworkingPublic/26.2.0/NetworkingPublic.podspec b/Specs/NetworkingPublic/26.2.0/NetworkingPublic.podspec new file mode 100644 index 00000000..199eb39a --- /dev/null +++ b/Specs/NetworkingPublic/26.2.0/NetworkingPublic.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'NetworkingPublic' + s.version = '26.2.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'BasePublic', s.version.to_s + + s.source_files = [ + 'Core/NetworkingPublic/**/*' + ] +end diff --git a/Specs/Serialization/26.2.0/Serialization.podspec b/Specs/Serialization/26.2.0/Serialization.podspec new file mode 100644 index 00000000..4c07b7d8 --- /dev/null +++ b/Specs/Serialization/26.2.0/Serialization.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'Serialization' + s.version = '26.2.0' + s.summary = 'Serialization' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '11.0' } + + s.dependency 'CommonCorePublic', s.version.to_s + + s.source_files = [ + 'Serialization/**/*' + ] +end