mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
update swiftformat, add organize rule
commit_hash:0f7ba811b37db6d2a9fc05efe5f94502feda4856
This commit is contained in:
@@ -21,3 +21,9 @@
|
||||
--wraparguments before-first
|
||||
--wrapcollections before-first
|
||||
--wrapparameters before-first
|
||||
|
||||
|
||||
--rules organizeDeclarations
|
||||
# options for organizeDeclarations
|
||||
--organization-mode type
|
||||
--mark-categories false
|
||||
@@ -58,7 +58,7 @@ custom_rules:
|
||||
message: "Such a cast to CF* type is unsafe. Use Serialization.safeCFCast (or add one if missing)"
|
||||
no_direct_use_of_repeating_count_initializer:
|
||||
name: "Dangerous repeating initializer"
|
||||
regex: '\(\s*repeating\s*:\s*\S.*,\s*count\s*:\s*\S.*\)'
|
||||
regex: '\(\s*repeating\s*:\s*[^,]*,\s*count\s*:\s*'
|
||||
message: "Use init(repeating:times:) instead"
|
||||
severity: warning
|
||||
no_direct_use_of_unique_keys_with_values_initializer:
|
||||
|
||||
@@ -46,6 +46,7 @@ final class ValueAnimator<I: ValueInterpolator>: Animator {
|
||||
}
|
||||
|
||||
let id: String
|
||||
|
||||
private var configuration: Configuration
|
||||
private let animationBlock: (AnimatedType) -> Void
|
||||
private let valueInterpolator: I
|
||||
@@ -135,6 +136,14 @@ final class ValueAnimator<I: ValueInterpolator>: Animator {
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
item?.cancel()
|
||||
item = nil
|
||||
displayLink?.invalidate()
|
||||
displayLink = nil
|
||||
cancelAction()
|
||||
}
|
||||
|
||||
@objc private func update() {
|
||||
guard let startTime, displayLink != nil else { return }
|
||||
|
||||
@@ -180,13 +189,6 @@ final class ValueAnimator<I: ValueInterpolator>: Animator {
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
item?.cancel()
|
||||
item = nil
|
||||
displayLink?.invalidate()
|
||||
displayLink = nil
|
||||
cancelAction()
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
@@ -201,6 +203,7 @@ extension CADisplayLink {
|
||||
private let currentMediaTime = { Double(0) }
|
||||
private class DisplayLink {
|
||||
init(target _: Any, selector _: Selector) {}
|
||||
|
||||
func invalidate() {}
|
||||
func addToCurrentRunLoop() {}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,12 @@ final class DivAnimatorController {
|
||||
private var animators = [UIElementPath: Animator]()
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
deinit {
|
||||
lock.withLock {
|
||||
animators.values.forEach { $0.stop() }
|
||||
}
|
||||
}
|
||||
|
||||
func startAnimator(
|
||||
path: UIElementPath,
|
||||
id: String,
|
||||
@@ -61,9 +67,4 @@ final class DivAnimatorController {
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.withLock {
|
||||
animators.values.forEach { $0.stop() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ extension Double: BuildFromDivVariable {
|
||||
|
||||
struct DoubleInterpolator: ValueInterpolator {
|
||||
typealias ValueType = Double
|
||||
|
||||
func interpolate(from: Double, to: Double, progress: CGFloat) -> Double {
|
||||
from + (to - from) * progress
|
||||
}
|
||||
@@ -49,6 +50,7 @@ extension Color: BuildFromDivVariable {
|
||||
|
||||
struct ColorInterpolator: ValueInterpolator {
|
||||
typealias ValueType = Color
|
||||
|
||||
func interpolate(from: Color, to: Color, progress: CGFloat) -> Color {
|
||||
guard let fromComponents = from.cgColor.components, let toComponents = to.cgColor.components,
|
||||
fromComponents.count >= 3, toComponents.count >= 3 else {
|
||||
|
||||
@@ -32,14 +32,6 @@ extension DebugBlock: UIViewRenderable {
|
||||
private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer {
|
||||
private var childView: BlockView?
|
||||
|
||||
var effectiveBackgroundColor: UIColor? {
|
||||
childView?.backgroundColor
|
||||
}
|
||||
|
||||
var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] {
|
||||
childView.map { [$0] } ?? []
|
||||
}
|
||||
|
||||
private var showDebugInfo: ((ViewType) -> Void)?
|
||||
private var errorCollector: DebugErrorCollector?
|
||||
private let disposePool = AutodisposePool()
|
||||
@@ -54,6 +46,14 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer {
|
||||
return button
|
||||
}()
|
||||
|
||||
var effectiveBackgroundColor: UIColor? {
|
||||
childView?.backgroundColor
|
||||
}
|
||||
|
||||
var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] {
|
||||
childView.map { [$0] } ?? []
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
addSubview(errorsButton)
|
||||
@@ -65,6 +65,21 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
childView?.frame = bounds
|
||||
errorsButton.frame = CGRect(
|
||||
center: CGPoint(x: buttonSize / 2.0, y: bounds.midY),
|
||||
size: CGSize(squareDimension: buttonSize)
|
||||
)
|
||||
errorsButton.layer.cornerRadius = buttonSize / 2.0
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result === self ? nil : result
|
||||
}
|
||||
|
||||
func configure(
|
||||
child: Block,
|
||||
errorCollector: DebugErrorCollector,
|
||||
@@ -91,14 +106,9 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer {
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
childView?.frame = bounds
|
||||
errorsButton.frame = CGRect(
|
||||
center: CGPoint(x: buttonSize / 2.0, y: bounds.midY),
|
||||
size: CGSize(squareDimension: buttonSize)
|
||||
)
|
||||
errorsButton.layer.cornerRadius = buttonSize / 2.0
|
||||
@objc func errorsButtonTapped() {
|
||||
guard let showDebugInfo, let errorCollector, errorCollector.totalErrorCount > 0 else { return }
|
||||
showDebugInfo(ErrorListView(errors: errorCollector.errorList))
|
||||
}
|
||||
|
||||
private func updateCountLabel() {
|
||||
@@ -108,15 +118,6 @@ private final class DebugBlockView: BlockView, VisibleBoundsTrackingContainer {
|
||||
errorsButton.setTitle("\(min(maxCount, errorsCount))", for: .normal)
|
||||
}
|
||||
|
||||
@objc func errorsButtonTapped() {
|
||||
guard let showDebugInfo, let errorCollector, errorCollector.totalErrorCount > 0 else { return }
|
||||
showDebugInfo(ErrorListView(errors: errorCollector.errorList))
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result === self ? nil : result
|
||||
}
|
||||
}
|
||||
|
||||
private let showOverlayURL = URL(string: "debugInfo://show")!
|
||||
|
||||
@@ -8,6 +8,10 @@ final class DebugBlock: WrapperBlock, LayoutCachingDefaultImpl {
|
||||
let errorCollector: DebugErrorCollector
|
||||
let showDebugInfo: (ViewType) -> Void
|
||||
|
||||
var debugDescription: String {
|
||||
"DebugBlock errors: \(errorCollector.debugDescription). Child: \(child)"
|
||||
}
|
||||
|
||||
init(
|
||||
child: Block,
|
||||
errorCollector: DebugErrorCollector,
|
||||
@@ -32,10 +36,6 @@ final class DebugBlock: WrapperBlock, LayoutCachingDefaultImpl {
|
||||
return errorCollector === other.errorCollector && child.equals(other.child)
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"DebugBlock errors: \(errorCollector.debugDescription). Child: \(child)"
|
||||
}
|
||||
|
||||
func getImageHolders() -> [any VGSLUI.ImageHolder] {
|
||||
child.getImageHolders()
|
||||
}
|
||||
|
||||
@@ -2,13 +2,25 @@ import LayoutKit
|
||||
import VGSLFundamentals
|
||||
|
||||
final class DebugErrorCollector: DivReporter {
|
||||
private let wrappedDivReporter: DivReporter
|
||||
|
||||
var errorStorage: DivErrorsStorage
|
||||
|
||||
private(set) var layoutErrors = [DivError]()
|
||||
let observableErrorCount = ObservableProperty<Int>(initialValue: 0)
|
||||
|
||||
private let wrappedDivReporter: DivReporter
|
||||
|
||||
var totalErrorCount: Int {
|
||||
errorStorage.errors.count + layoutErrors.count
|
||||
}
|
||||
|
||||
var errorList: [String] {
|
||||
errorStorage.errors.map(\.prettyMessage) + layoutErrors.map(\.prettyMessage)
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"Errors: \(errorList)"
|
||||
}
|
||||
|
||||
init(
|
||||
wrappedDivReporter: DivReporter,
|
||||
errorStorage: DivErrorsStorage
|
||||
@@ -28,18 +40,6 @@ final class DebugErrorCollector: DivReporter {
|
||||
wrappedDivReporter.reportAction(cardId: cardId, info: info)
|
||||
}
|
||||
|
||||
var totalErrorCount: Int {
|
||||
errorStorage.errors.count + layoutErrors.count
|
||||
}
|
||||
|
||||
var errorList: [String] {
|
||||
errorStorage.errors.map(\.prettyMessage) + layoutErrors.map(\.prettyMessage)
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
"Errors: \(errorList)"
|
||||
}
|
||||
|
||||
private func hasError(_ error: DivError) -> Bool {
|
||||
layoutErrors.contains {
|
||||
$0.kind == error.kind
|
||||
|
||||
@@ -18,15 +18,15 @@ final class ErrorListView: UIView {
|
||||
addSubview(errorView)
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
self.errorView.frame = self.bounds.inset(by: safeAreaInsets)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
self.errorView.frame = self.bounds.inset(by: safeAreaInsets)
|
||||
}
|
||||
|
||||
private func copyToPasteboard() {
|
||||
UIPasteboard.general.string = errorString
|
||||
}
|
||||
|
||||
@@ -16,11 +16,11 @@ public final class TimeMeasure: @unchecked Sendable {
|
||||
case warm
|
||||
}
|
||||
|
||||
public private(set) var time: Time?
|
||||
|
||||
private var startTime: Date?
|
||||
private var status: Status = .cold
|
||||
|
||||
public private(set) var time: Time?
|
||||
|
||||
init() {}
|
||||
|
||||
public func updateMeasure<T>(action: () throws -> T) throws -> T {
|
||||
|
||||
@@ -9,17 +9,24 @@ import AppKit
|
||||
#endif
|
||||
|
||||
public struct DivBlockModelingContext {
|
||||
public let actionHandler: DivActionHandler?
|
||||
public let blockStateStorage: DivBlockStateStorage
|
||||
public let imageHolderFactory: DivImageHolderFactory
|
||||
public let fontProvider: DivFontProvider
|
||||
public private(set) var errorsStorage: DivErrorsStorage
|
||||
public let variablesStorage: DivVariablesStorage
|
||||
public private(set) var expressionResolver: ExpressionResolver
|
||||
public let variableTracker: DivVariableTracker?
|
||||
public private(set) var path: UIElementPath
|
||||
public private(set) var currentDivId: String?
|
||||
|
||||
private(set) var viewId: DivViewId
|
||||
private(set) var cardLogId: String?
|
||||
private(set) var parentDivStatePath: DivStatePath?
|
||||
let stateManager: DivStateManager
|
||||
public let actionHandler: DivActionHandler?
|
||||
public let blockStateStorage: DivBlockStateStorage
|
||||
let visibilityCounter: DivVisibilityCounter
|
||||
let lastVisibleBoundsCache: DivLastVisibleBoundsCache
|
||||
public let imageHolderFactory: DivImageHolderFactory
|
||||
let divCustomBlockFactory: DivCustomBlockFactory
|
||||
public let fontProvider: DivFontProvider
|
||||
let flagsInfo: DivFlagsInfo
|
||||
let extensionHandlers: [String: DivExtensionHandler]
|
||||
let layoutDirection: UserInterfaceLayoutDirection
|
||||
@@ -27,18 +34,10 @@ public struct DivBlockModelingContext {
|
||||
let scheduler: Scheduling
|
||||
let playerFactory: PlayerFactory?
|
||||
private(set) weak var parentScrollView: ScrollView?
|
||||
public private(set) var errorsStorage: DivErrorsStorage
|
||||
let debugErrorCollector: DebugErrorCollector?
|
||||
private let persistentValuesStorage: DivPersistentValuesStorage
|
||||
let tooltipViewFactory: DivTooltipViewFactory?
|
||||
let functionsStorage: DivFunctionsStorage?
|
||||
public let variablesStorage: DivVariablesStorage
|
||||
let triggersStorage: DivTriggersStorage?
|
||||
public private(set) var expressionResolver: ExpressionResolver
|
||||
private let functionsProvider: FunctionsProvider
|
||||
public let variableTracker: DivVariableTracker?
|
||||
public private(set) var path: UIElementPath
|
||||
public private(set) var currentDivId: String?
|
||||
// Overriden id for modified contexts of child divs, used in prototypes
|
||||
private(set) var overridenId: String?
|
||||
private(set) var sizeModifier: DivSizeModifier?
|
||||
@@ -48,11 +47,18 @@ public struct DivBlockModelingContext {
|
||||
let animatorController: DivAnimatorController?
|
||||
private(set) var accessibilityElementsStorage = DivAccessibilityElementsStorage()
|
||||
|
||||
private let persistentValuesStorage: DivPersistentValuesStorage
|
||||
private let functionsProvider: FunctionsProvider
|
||||
|
||||
// Deprecated, `parentPath` was changed to `path`
|
||||
public var parentPath: UIElementPath {
|
||||
path
|
||||
}
|
||||
|
||||
public var cardId: DivCardID {
|
||||
viewId.cardId
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public init(
|
||||
cardId: DivCardID,
|
||||
@@ -171,10 +177,6 @@ public struct DivBlockModelingContext {
|
||||
)
|
||||
}
|
||||
|
||||
public var cardId: DivCardID {
|
||||
viewId.cardId
|
||||
}
|
||||
|
||||
public func getExtensionHandlers(for div: DivBase) -> [DivExtensionHandler] {
|
||||
(div.extensions ?? []).compactMap {
|
||||
let id = $0.id
|
||||
|
||||
@@ -29,9 +29,26 @@ public final class DivBlockStateStorage {
|
||||
case idFocused(IdAndCardId)
|
||||
}
|
||||
|
||||
private(set) var isInputFocused = false
|
||||
|
||||
private var shouldRefreshCachedStates = false
|
||||
private var _cachedStates: BlocksState?
|
||||
|
||||
private var _states: [StateKey: ElementState] {
|
||||
didSet {
|
||||
shouldRefreshCachedStates = true
|
||||
}
|
||||
}
|
||||
|
||||
private var focusedElement: FocusedElement = .none {
|
||||
didSet {
|
||||
isInputFocused = false
|
||||
}
|
||||
}
|
||||
|
||||
private let lock = AllocatedUnfairLock()
|
||||
private let stateUpdatesPipe = SignalPipe<ChangeEvent>()
|
||||
|
||||
public var states: BlocksState {
|
||||
if let cached = _cachedStates,
|
||||
!shouldRefreshCachedStates {
|
||||
@@ -56,23 +73,6 @@ public final class DivBlockStateStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private var _states: [StateKey: ElementState] {
|
||||
didSet {
|
||||
shouldRefreshCachedStates = true
|
||||
}
|
||||
}
|
||||
|
||||
private var focusedElement: FocusedElement = .none {
|
||||
didSet {
|
||||
isInputFocused = false
|
||||
}
|
||||
}
|
||||
|
||||
private let lock = AllocatedUnfairLock()
|
||||
private let stateUpdatesPipe = SignalPipe<ChangeEvent>()
|
||||
|
||||
private(set) var isInputFocused = false
|
||||
|
||||
var stateUpdates: Signal<ChangeEvent> {
|
||||
stateUpdatesPipe.signal
|
||||
}
|
||||
@@ -147,12 +147,6 @@ public final class DivBlockStateStorage {
|
||||
}
|
||||
}
|
||||
|
||||
func setFocused(isFocused: Bool, element: IdAndCardId) {
|
||||
lock.withLock {
|
||||
focusedElement = isFocused ? .idFocused(element) : removeFocus(from: element)
|
||||
}
|
||||
}
|
||||
|
||||
public func setFocused(
|
||||
isFocused: Bool,
|
||||
path: UIElementPath
|
||||
@@ -168,22 +162,55 @@ public final class DivBlockStateStorage {
|
||||
}
|
||||
}
|
||||
|
||||
func isFocused(element: IdAndCardId) -> Bool {
|
||||
lock.withLock {
|
||||
isFocusedInternal(checkedElement: .idFocused(element))
|
||||
}
|
||||
}
|
||||
|
||||
public func isFocused(path: UIElementPath) -> Bool {
|
||||
lock.withLock {
|
||||
isFocusedInternal(checkedElement: .pathFocused(path))
|
||||
}
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
lock.withLock {
|
||||
_states = [:]
|
||||
focusedElement = .none
|
||||
}
|
||||
}
|
||||
|
||||
public func reset(cardId: DivCardID) {
|
||||
lock.withLock {
|
||||
_states = _states.filter { $0.key.cardID != cardId }
|
||||
if getFocusedElement()?.cardId == cardId {
|
||||
focusedElement = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setFocused(isFocused: Bool, element: IdAndCardId) {
|
||||
lock.withLock {
|
||||
focusedElement = isFocused ? .idFocused(element) : removeFocus(from: element)
|
||||
}
|
||||
}
|
||||
|
||||
func isFocused(element: IdAndCardId) -> Bool {
|
||||
lock.withLock {
|
||||
isFocusedInternal(checkedElement: .idFocused(element))
|
||||
}
|
||||
}
|
||||
|
||||
func setInputFocused() {
|
||||
isInputFocused = true
|
||||
}
|
||||
|
||||
func getFocusedElement() -> IdAndCardId? {
|
||||
switch focusedElement {
|
||||
case .none:
|
||||
nil
|
||||
case let .pathFocused(focusedPath):
|
||||
IdAndCardId(path: focusedPath)
|
||||
case let .idFocused(focusedId):
|
||||
focusedId
|
||||
}
|
||||
}
|
||||
|
||||
private func isFocusedInternal(checkedElement: FocusedElement) -> Bool {
|
||||
switch (focusedElement, checkedElement) {
|
||||
case (.none, _), (_, .none):
|
||||
@@ -203,32 +230,6 @@ public final class DivBlockStateStorage {
|
||||
isFocusedInternal(checkedElement: FocusedElement.idFocused(element)) ? .none : focusedElement
|
||||
}
|
||||
|
||||
func getFocusedElement() -> IdAndCardId? {
|
||||
switch focusedElement {
|
||||
case .none:
|
||||
nil
|
||||
case let .pathFocused(focusedPath):
|
||||
IdAndCardId(path: focusedPath)
|
||||
case let .idFocused(focusedId):
|
||||
focusedId
|
||||
}
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
lock.withLock {
|
||||
_states = [:]
|
||||
focusedElement = .none
|
||||
}
|
||||
}
|
||||
|
||||
public func reset(cardId: DivCardID) {
|
||||
lock.withLock {
|
||||
_states = _states.filter { $0.key.cardID != cardId }
|
||||
if getFocusedElement()?.cardId == cardId {
|
||||
focusedElement = .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DivBlockStateStorage: ElementStateObserver {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
/// included in the framework.
|
||||
/// You can access the default `DivFlagsInfo` instance using the static property `default`.
|
||||
public struct DivFlagsInfo {
|
||||
/// The default instance of `DivFlagsInfo`.
|
||||
public static let `default` = DivFlagsInfo()
|
||||
|
||||
/// Defines the behavior of the visibility/disappear actions.
|
||||
///
|
||||
/// `true` - visibility action URLs will be handled by `DivUrlHandler` the same way regular
|
||||
@@ -65,6 +68,4 @@ public struct DivFlagsInfo {
|
||||
self.fontCacheEnabled = fontCacheEnabled
|
||||
}
|
||||
|
||||
/// The default instance of `DivFlagsInfo`.
|
||||
public static let `default` = DivFlagsInfo()
|
||||
}
|
||||
|
||||
@@ -32,10 +32,6 @@ public final class DivKitComponents {
|
||||
public let visibilityCounter = DivVisibilityCounter()
|
||||
public let resourcesPreloader: DivDataResourcesPreloader?
|
||||
|
||||
public var updateCardSignal: Signal<[DivCardUpdateReason]> {
|
||||
updateCardPipe.signal
|
||||
}
|
||||
|
||||
private let animatorController = DivAnimatorController()
|
||||
private let disposePool = AutodisposePool()
|
||||
private let idToPath = IdToPath()
|
||||
@@ -50,6 +46,10 @@ public final class DivKitComponents {
|
||||
private let variableTracker = DivVariableTracker()
|
||||
private var debugErrorCollectors = [DivCardID: DebugErrorCollector]()
|
||||
|
||||
public var updateCardSignal: Signal<[DivCardUpdateReason]> {
|
||||
updateCardPipe.signal
|
||||
}
|
||||
|
||||
/// You can create an instance of `DivKitComponents` with various optional parameters that allow
|
||||
/// you to customize the behavior and functionality of `DivKit` to suit your specific needs.
|
||||
///
|
||||
|
||||
@@ -6,6 +6,14 @@ public final class DivVisibilityCounter {
|
||||
|
||||
init() {}
|
||||
|
||||
public func reset() {
|
||||
storage.removeAll()
|
||||
}
|
||||
|
||||
public func reset(cardId: DivCardID) {
|
||||
storage = storage.filter { $0.key.root != cardId.rawValue }
|
||||
}
|
||||
|
||||
func visibilityCount(for path: UIElementPath) -> UInt {
|
||||
storage[path] ?? 0
|
||||
}
|
||||
@@ -14,11 +22,4 @@ public final class DivVisibilityCounter {
|
||||
storage[path] = visibilityCount(for: path) + 1
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
storage.removeAll()
|
||||
}
|
||||
|
||||
public func reset(cardId: DivCardID) {
|
||||
storage = storage.filter { $0.key.root != cardId.rawValue }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,6 @@ import Foundation
|
||||
import VGSL
|
||||
|
||||
struct CalcExpression {
|
||||
static func parse(_ expression: String) throws -> CalcExpression {
|
||||
var unicodeScalarView = UnicodeScalarView(expression.unicodeScalars)
|
||||
return try CalcExpression(root: unicodeScalarView.parseSubexpression(upTo: []))
|
||||
}
|
||||
|
||||
private let root: Subexpression
|
||||
|
||||
var variableNames: [String] {
|
||||
@@ -51,6 +46,11 @@ struct CalcExpression {
|
||||
}
|
||||
}
|
||||
|
||||
static func parse(_ expression: String) throws -> CalcExpression {
|
||||
var unicodeScalarView = UnicodeScalarView(expression.unicodeScalars)
|
||||
return try CalcExpression(root: unicodeScalarView.parseSubexpression(upTo: []))
|
||||
}
|
||||
|
||||
func extractDynamicVariableNames(_ context: ExpressionContext) throws -> [String] {
|
||||
try root.extractDynamicVariableNames(context)
|
||||
}
|
||||
@@ -78,18 +78,6 @@ enum Subexpression {
|
||||
}
|
||||
}
|
||||
|
||||
func evaluate(_ context: ExpressionContext) throws -> Any {
|
||||
switch self {
|
||||
case let .literal(value):
|
||||
return value
|
||||
case let .symbol(symbol, args):
|
||||
if let evaluator = context.evaluators(symbol) {
|
||||
return try evaluator.invoke(args: args, context: context)
|
||||
}
|
||||
throw ExpressionError("Undefined symbol: \(symbol.name).")
|
||||
}
|
||||
}
|
||||
|
||||
var symbols: Set<CalcExpression.Symbol> {
|
||||
switch self {
|
||||
case .literal:
|
||||
@@ -103,6 +91,18 @@ enum Subexpression {
|
||||
}
|
||||
}
|
||||
|
||||
func evaluate(_ context: ExpressionContext) throws -> Any {
|
||||
switch self {
|
||||
case let .literal(value):
|
||||
return value
|
||||
case let .symbol(symbol, args):
|
||||
if let evaluator = context.evaluators(symbol) {
|
||||
return try evaluator.invoke(args: args, context: context)
|
||||
}
|
||||
throw ExpressionError("Undefined symbol: \(symbol.name).")
|
||||
}
|
||||
}
|
||||
|
||||
func extractDynamicVariableNames(_ context: ExpressionContext) throws -> [String] {
|
||||
switch self {
|
||||
case .literal:
|
||||
@@ -129,14 +129,14 @@ enum Subexpression {
|
||||
fileprivate struct UnicodeScalarView {
|
||||
typealias Index = String.UnicodeScalarView.Index
|
||||
|
||||
private let characters: String.UnicodeScalarView
|
||||
private(set) var startIndex: Index
|
||||
private(set) var endIndex: Index
|
||||
|
||||
init(_ unicodeScalars: String.UnicodeScalarView) {
|
||||
characters = unicodeScalars
|
||||
startIndex = characters.startIndex
|
||||
endIndex = characters.endIndex
|
||||
private let characters: String.UnicodeScalarView
|
||||
|
||||
/// Returns the remaining characters
|
||||
var unicodeScalars: Substring.UnicodeScalarView {
|
||||
characters[startIndex..<endIndex]
|
||||
}
|
||||
|
||||
private var first: UnicodeScalar? {
|
||||
@@ -147,12 +147,10 @@ fileprivate struct UnicodeScalarView {
|
||||
startIndex >= endIndex
|
||||
}
|
||||
|
||||
private subscript(_ index: Index) -> UnicodeScalar {
|
||||
characters[index]
|
||||
}
|
||||
|
||||
private func index(after index: Index) -> Index {
|
||||
characters.index(after: index)
|
||||
init(_ unicodeScalars: String.UnicodeScalarView) {
|
||||
characters = unicodeScalars
|
||||
startIndex = characters.startIndex
|
||||
endIndex = characters.endIndex
|
||||
}
|
||||
|
||||
func prefix(upTo index: Index) -> UnicodeScalarView {
|
||||
@@ -169,6 +167,14 @@ fileprivate struct UnicodeScalarView {
|
||||
return view
|
||||
}
|
||||
|
||||
private subscript(_ index: Index) -> UnicodeScalar {
|
||||
characters[index]
|
||||
}
|
||||
|
||||
private func index(after index: Index) -> Index {
|
||||
characters.index(after: index)
|
||||
}
|
||||
|
||||
private mutating func popFirst() -> UnicodeScalar? {
|
||||
if isEmpty {
|
||||
return nil
|
||||
@@ -178,10 +184,6 @@ fileprivate struct UnicodeScalarView {
|
||||
return char
|
||||
}
|
||||
|
||||
/// Returns the remaining characters
|
||||
var unicodeScalars: Substring.UnicodeScalarView {
|
||||
characters[startIndex..<endIndex]
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class CustomFunction: SimpleFunction {
|
||||
var signature: FunctionSignature
|
||||
|
||||
struct Signature: Hashable {
|
||||
var name: String
|
||||
var arguments: [DivEvaluableType]
|
||||
@@ -13,7 +11,10 @@ final class CustomFunction: SimpleFunction {
|
||||
let type: DivEvaluableType
|
||||
}
|
||||
|
||||
var signature: FunctionSignature
|
||||
|
||||
let name: String
|
||||
|
||||
private let arguments: [Argument]
|
||||
private let body: String
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import VGSL
|
||||
|
||||
@_spi(Internal)
|
||||
public final class DivFunctionsStorage {
|
||||
let outerStorage: DivFunctionsStorage?
|
||||
|
||||
private var functions: [CustomFunction.Signature: CustomFunction] = [:]
|
||||
private var storages: [UIElementPath: DivFunctionsStorage] = [:]
|
||||
|
||||
private let reporter: DivReporter
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
let outerStorage: DivFunctionsStorage?
|
||||
|
||||
init(
|
||||
outerStorage: DivFunctionsStorage? = DivFunctionsStorage(outerStorage: nil),
|
||||
reporter: DivReporter = DefaultDivReporter()
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
import Foundation
|
||||
|
||||
public struct ExpressionError: LocalizedError, CustomStringConvertible {
|
||||
public let description: String
|
||||
|
||||
let message: String
|
||||
|
||||
public var errorDescription: String? {
|
||||
description
|
||||
}
|
||||
|
||||
init(
|
||||
_ message: String,
|
||||
expression: String? = nil
|
||||
) {
|
||||
self.message = message
|
||||
|
||||
description = if let expression {
|
||||
"\(message) Expression: \(expression)"
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
static func integerOverflow() -> Error {
|
||||
ExpressionError("Integer overflow.")
|
||||
}
|
||||
@@ -31,23 +52,4 @@ public struct ExpressionError: LocalizedError, CustomStringConvertible {
|
||||
)
|
||||
}
|
||||
|
||||
public let description: String
|
||||
let message: String
|
||||
|
||||
init(
|
||||
_ message: String,
|
||||
expression: String? = nil
|
||||
) {
|
||||
self.message = message
|
||||
|
||||
description = if let expression {
|
||||
"\(message) Expression: \(expression)"
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,21 @@ public final class ExpressionResolver {
|
||||
errorTracker: errorTracker
|
||||
)
|
||||
|
||||
@_spi(Legacy)
|
||||
public convenience init(
|
||||
variableValueProvider: @escaping (String) -> Any?,
|
||||
persistentValuesStorage: DivPersistentValuesStorage = DivPersistentValuesStorage(),
|
||||
errorTracker: ExpressionErrorTracker? = nil
|
||||
) {
|
||||
self.init(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: persistentValuesStorage
|
||||
),
|
||||
variableValueProvider: variableValueProvider,
|
||||
errorTracker: { errorTracker?($0) }
|
||||
)
|
||||
}
|
||||
|
||||
init(
|
||||
functionsProvider: FunctionsProvider,
|
||||
customFunctionsStorageProvider: @escaping (String) -> DivFunctionsStorage? = { _ in nil },
|
||||
@@ -50,21 +65,6 @@ public final class ExpressionResolver {
|
||||
self.errorTracker = reporter.asExpressionErrorTracker(cardId: path.cardId)
|
||||
}
|
||||
|
||||
@_spi(Legacy)
|
||||
public convenience init(
|
||||
variableValueProvider: @escaping (String) -> Any?,
|
||||
persistentValuesStorage: DivPersistentValuesStorage = DivPersistentValuesStorage(),
|
||||
errorTracker: ExpressionErrorTracker? = nil
|
||||
) {
|
||||
self.init(
|
||||
functionsProvider: FunctionsProvider(
|
||||
persistentValuesStorage: persistentValuesStorage
|
||||
),
|
||||
variableValueProvider: variableValueProvider,
|
||||
errorTracker: { errorTracker?($0) }
|
||||
)
|
||||
}
|
||||
|
||||
public func resolve(_ expression: String) -> Any? {
|
||||
if let link: ExpressionLink<Any> = makeLink(expression) {
|
||||
return resolveAnyLink(link)
|
||||
|
||||
@@ -34,17 +34,17 @@ extension SimpleFunction {
|
||||
struct NoMatchingSignatureError: Error {}
|
||||
|
||||
struct ConstantFunction<R>: SimpleFunction {
|
||||
let signature = FunctionSignature(
|
||||
arguments: [],
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let value: R
|
||||
|
||||
init(_ value: R) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [],
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
func invoke(_: [Any], context _: ExpressionContext) throws -> Any {
|
||||
value
|
||||
}
|
||||
@@ -67,13 +67,13 @@ struct LazyFunction: Function {
|
||||
}
|
||||
|
||||
struct FunctionNullary<R>: SimpleFunction {
|
||||
private let impl: (ExpressionContext) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [],
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (ExpressionContext) throws -> R
|
||||
|
||||
init(impl: @escaping () throws -> R) {
|
||||
self.impl = { _ in try impl() }
|
||||
}
|
||||
@@ -88,8 +88,6 @@ struct FunctionNullary<R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionUnary<T1, R>: SimpleFunction {
|
||||
private let impl: (T1) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self),
|
||||
@@ -97,6 +95,8 @@ struct FunctionUnary<T1, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (T1) throws -> R
|
||||
|
||||
init(impl: @escaping (T1) throws -> R) {
|
||||
self.impl = impl
|
||||
}
|
||||
@@ -108,8 +108,6 @@ struct FunctionUnary<T1, R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionBinary<T1, T2, R>: SimpleFunction {
|
||||
private let impl: (T1, T2, ExpressionContext) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self),
|
||||
@@ -118,6 +116,8 @@ struct FunctionBinary<T1, T2, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (T1, T2, ExpressionContext) throws -> R
|
||||
|
||||
init(impl: @escaping (T1, T2) throws -> R) {
|
||||
self.impl = { arg1, arg2, _ in try impl(arg1, arg2) }
|
||||
}
|
||||
@@ -137,8 +137,6 @@ struct FunctionBinary<T1, T2, R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionTernary<T1, T2, T3, R>: SimpleFunction {
|
||||
private let impl: (T1, T2, T3) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self),
|
||||
@@ -148,6 +146,8 @@ struct FunctionTernary<T1, T2, T3, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (T1, T2, T3) throws -> R
|
||||
|
||||
init(impl: @escaping (T1, T2, T3) throws -> R) {
|
||||
self.impl = impl
|
||||
}
|
||||
@@ -163,8 +163,6 @@ struct FunctionTernary<T1, T2, T3, R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionQuaternary<T1, T2, T3, T4, R>: SimpleFunction {
|
||||
private let impl: (T1, T2, T3, T4) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self),
|
||||
@@ -175,6 +173,8 @@ struct FunctionQuaternary<T1, T2, T3, T4, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (T1, T2, T3, T4) throws -> R
|
||||
|
||||
init(impl: @escaping (T1, T2, T3, T4) throws -> R) {
|
||||
self.impl = impl
|
||||
}
|
||||
@@ -191,8 +191,6 @@ struct FunctionQuaternary<T1, T2, T3, T4, R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionVarUnary<T1, R>: SimpleFunction {
|
||||
private let impl: ([T1]) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self, vararg: true),
|
||||
@@ -200,6 +198,8 @@ struct FunctionVarUnary<T1, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: ([T1]) throws -> R
|
||||
|
||||
init(impl: @escaping ([T1]) throws -> R) {
|
||||
self.impl = impl
|
||||
}
|
||||
@@ -211,8 +211,6 @@ struct FunctionVarUnary<T1, R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionVarBinary<T1, T2, R>: SimpleFunction {
|
||||
private let impl: (T1, [T2]) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self),
|
||||
@@ -221,6 +219,8 @@ struct FunctionVarBinary<T1, T2, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (T1, [T2]) throws -> R
|
||||
|
||||
init(impl: @escaping (T1, [T2]) throws -> R) {
|
||||
self.impl = impl
|
||||
}
|
||||
@@ -235,8 +235,6 @@ struct FunctionVarBinary<T1, T2, R>: SimpleFunction {
|
||||
}
|
||||
|
||||
struct FunctionVarTernary<T1, T2, T3, R>: SimpleFunction {
|
||||
private let impl: (T1, T2, [T3]) throws -> R
|
||||
|
||||
let signature = FunctionSignature(
|
||||
arguments: [
|
||||
.init(type: T1.self),
|
||||
@@ -246,6 +244,8 @@ struct FunctionVarTernary<T1, T2, T3, R>: SimpleFunction {
|
||||
resultType: R.self
|
||||
)
|
||||
|
||||
private let impl: (T1, T2, [T3]) throws -> R
|
||||
|
||||
init(impl: @escaping (T1, T2, [T3]) throws -> R) {
|
||||
self.impl = impl
|
||||
}
|
||||
@@ -262,6 +262,7 @@ struct FunctionVarTernary<T1, T2, T3, R>: SimpleFunction {
|
||||
|
||||
struct OverloadedFunction: Function {
|
||||
let functions: [SimpleFunction]
|
||||
|
||||
private let makeError: ([Any]) -> Error
|
||||
|
||||
init(functions: [SimpleFunction], makeError: (([Any]) -> Error)? = nil) {
|
||||
|
||||
@@ -2,15 +2,6 @@ import Foundation
|
||||
import VGSL
|
||||
|
||||
final class FunctionsProvider {
|
||||
private let persistentValuesStorage: DivPersistentValuesStorage
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
init(
|
||||
persistentValuesStorage: DivPersistentValuesStorage
|
||||
) {
|
||||
self.persistentValuesStorage = persistentValuesStorage
|
||||
}
|
||||
|
||||
static let methods: [String: Function] = {
|
||||
var methods: [String: Function] = [:]
|
||||
methods.addArrayMethods()
|
||||
@@ -64,6 +55,16 @@ final class FunctionsProvider {
|
||||
guard getValueFunctions.contains(symbol.name) else { return nil }
|
||||
return DynamicVariablesEvaluator()
|
||||
}
|
||||
|
||||
private let persistentValuesStorage: DivPersistentValuesStorage
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
init(
|
||||
persistentValuesStorage: DivPersistentValuesStorage
|
||||
) {
|
||||
self.persistentValuesStorage = persistentValuesStorage
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct CustomFunctionEvaluator: Function {
|
||||
|
||||
@@ -21,18 +21,6 @@ final class DivBaseBlockBuilder {
|
||||
private let identity: String
|
||||
private let isFocused: Bool
|
||||
|
||||
private var expressionResolver: ExpressionResolver {
|
||||
context.expressionResolver
|
||||
}
|
||||
|
||||
private var path: UIElementPath {
|
||||
context.path
|
||||
}
|
||||
|
||||
private var statePath: DivStatePath {
|
||||
context.parentDivStatePath ?? DivData.rootPath
|
||||
}
|
||||
|
||||
private lazy var rotation = self.div.transform?.resolveRotation(self.expressionResolver)
|
||||
|
||||
private lazy var border = isFocused ? div.focus?.border ?? div.border : div.border
|
||||
@@ -56,6 +44,18 @@ final class DivBaseBlockBuilder {
|
||||
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,
|
||||
@@ -74,77 +74,6 @@ final class DivBaseBlockBuilder {
|
||||
self.isFocused = isFocused
|
||||
}
|
||||
|
||||
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\"")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func applyExtensionHandlers(
|
||||
stage: ApplyExtensionHandlersStage
|
||||
) -> Self {
|
||||
@@ -162,7 +91,7 @@ final class DivBaseBlockBuilder {
|
||||
applyPaddings: Bool
|
||||
) -> Self {
|
||||
block = block.addingEdgeInsets(
|
||||
applyPaddings ? div.paddings.resolve(context) : .zero,
|
||||
applyPaddings ? div.paddings.resolve(context): .zero,
|
||||
clipsToBounds: clipToBounds
|
||||
)
|
||||
|
||||
@@ -371,4 +300,76 @@ final class DivBaseBlockBuilder {
|
||||
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\"")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,9 +29,13 @@ extension SizeProviderBlock {
|
||||
}
|
||||
|
||||
private final class SizeProviderBlockView: BlockView {
|
||||
var childMarginsSize = CGSize.zero
|
||||
|
||||
private var block: SizeProviderBlock!
|
||||
private var childView: BlockView!
|
||||
|
||||
var effectiveBackgroundColor: UIColor? { childView.backgroundColor }
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
@@ -41,9 +45,12 @@ private final class SizeProviderBlockView: BlockView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var effectiveBackgroundColor: UIColor? { childView.backgroundColor }
|
||||
|
||||
var childMarginsSize = CGSize.zero
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
childView.frame = bounds
|
||||
block.widthUpdater?(Int(bounds.width - childMarginsSize.width))
|
||||
block.heightUpdater?(Int(bounds.height - childMarginsSize.height))
|
||||
}
|
||||
|
||||
func configure(
|
||||
block: SizeProviderBlock,
|
||||
@@ -64,12 +71,6 @@ private final class SizeProviderBlockView: BlockView {
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
childView.frame = bounds
|
||||
block.widthUpdater?(Int(bounds.width - childMarginsSize.width))
|
||||
block.heightUpdater?(Int(bounds.height - childMarginsSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
extension SizeProviderBlockView: VisibleBoundsTrackingContainer {
|
||||
|
||||
@@ -10,6 +10,10 @@ public class DivPatchDownloader: DivPatchProvider {
|
||||
self.requestPerformer = requestPerformer
|
||||
}
|
||||
|
||||
deinit {
|
||||
cancelRequests()
|
||||
}
|
||||
|
||||
public func getPatch(
|
||||
url: URL,
|
||||
completion: @escaping DivPatchProviderCompletion
|
||||
@@ -33,9 +37,6 @@ public class DivPatchDownloader: DivPatchProvider {
|
||||
requests.removeAll()
|
||||
}
|
||||
|
||||
deinit {
|
||||
cancelRequests()
|
||||
}
|
||||
}
|
||||
|
||||
private func parseResult(
|
||||
|
||||
@@ -58,26 +58,6 @@ public class DivStateManager {
|
||||
self._items = items
|
||||
}
|
||||
|
||||
func get(stateBlockPath: DivStatePath) -> Item? {
|
||||
lock.withLock {
|
||||
_items[stateBlockPath]
|
||||
}
|
||||
}
|
||||
|
||||
func setState(stateBlockPath: DivStatePath, stateBinding: Binding<String>) {
|
||||
lock.withLock {
|
||||
_stateBindings[stateBlockPath] = stateBinding
|
||||
guard stateBinding.value != _items[stateBlockPath]?.currentStateID.rawValue else { return }
|
||||
updateState(path: stateBlockPath, stateID: DivStateID(rawValue: stateBinding.value))
|
||||
}
|
||||
}
|
||||
|
||||
func resetBinding(for stateBlockPath: DivStatePath) {
|
||||
lock.withLock {
|
||||
_ = _stateBindings.removeValue(forKey: stateBlockPath)
|
||||
}
|
||||
}
|
||||
|
||||
public func setState(stateBlockPath: DivStatePath, stateID: DivStateID) {
|
||||
lock.withLock {
|
||||
_stateBindings[stateBlockPath]?.value = stateID.rawValue
|
||||
@@ -94,21 +74,6 @@ public class DivStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateState(path: DivStatePath, stateID: DivStateID) {
|
||||
// need to take a write lock before
|
||||
let previousItem = _items[path]
|
||||
let previousState: PreviousState = if let previousStateID = previousItem?.currentStateID {
|
||||
.withID(previousStateID)
|
||||
} else {
|
||||
.initial
|
||||
}
|
||||
_stateBindings[path]?.value = stateID.rawValue
|
||||
_items[path] = Item(
|
||||
currentStateID: stateID,
|
||||
previousState: previousState
|
||||
)
|
||||
}
|
||||
|
||||
public func removeState(path: DivStatePath) {
|
||||
lock.withLock {
|
||||
_ = _items.removeValue(forKey: path)
|
||||
@@ -182,6 +147,35 @@ public class DivStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
lock.withLock {
|
||||
_items = [:]
|
||||
_blockIds = [:]
|
||||
_blockVisibility = [:]
|
||||
_stateBindings = [:]
|
||||
}
|
||||
}
|
||||
|
||||
func get(stateBlockPath: DivStatePath) -> Item? {
|
||||
lock.withLock {
|
||||
_items[stateBlockPath]
|
||||
}
|
||||
}
|
||||
|
||||
func setState(stateBlockPath: DivStatePath, stateBinding: Binding<String>) {
|
||||
lock.withLock {
|
||||
_stateBindings[stateBlockPath] = stateBinding
|
||||
guard stateBinding.value != _items[stateBlockPath]?.currentStateID.rawValue else { return }
|
||||
updateState(path: stateBlockPath, stateID: DivStateID(rawValue: stateBinding.value))
|
||||
}
|
||||
}
|
||||
|
||||
func resetBinding(for stateBlockPath: DivStatePath) {
|
||||
lock.withLock {
|
||||
_ = _stateBindings.removeValue(forKey: stateBlockPath)
|
||||
}
|
||||
}
|
||||
|
||||
func setBlockVisibility(
|
||||
statePath: DivStatePath,
|
||||
resolvedId: String,
|
||||
@@ -200,14 +194,21 @@ public class DivStateManager {
|
||||
}
|
||||
}
|
||||
|
||||
public func reset() {
|
||||
lock.withLock {
|
||||
_items = [:]
|
||||
_blockIds = [:]
|
||||
_blockVisibility = [:]
|
||||
_stateBindings = [:]
|
||||
private func updateState(path: DivStatePath, stateID: DivStateID) {
|
||||
// need to take a write lock before
|
||||
let previousItem = _items[path]
|
||||
let previousState: PreviousState = if let previousStateID = previousItem?.currentStateID {
|
||||
.withID(previousStateID)
|
||||
} else {
|
||||
.initial
|
||||
}
|
||||
_stateBindings[path]?.value = stateID.rawValue
|
||||
_items[path] = Item(
|
||||
currentStateID: stateID,
|
||||
previousState: previousState
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension DivStateManager: Equatable {
|
||||
|
||||
@@ -3,8 +3,17 @@ import VGSL
|
||||
|
||||
public final class DivPersistentValuesStorage {
|
||||
static let storageFileName = "divkit.values_storage"
|
||||
|
||||
private let timestampProvider: Variable<Milliseconds>
|
||||
|
||||
private let storage = Property<StoredValues>(
|
||||
fileName: storageFileName,
|
||||
initialValue: StoredValues(items: [:]),
|
||||
onError: { error in
|
||||
DivKitLogger.error("Failed to create storage: \(error)")
|
||||
}
|
||||
)
|
||||
|
||||
public init(
|
||||
timestampProvider: Variable<Milliseconds> = Variable {
|
||||
Date().timeIntervalSince1970.milliseconds
|
||||
@@ -14,14 +23,6 @@ public final class DivPersistentValuesStorage {
|
||||
removeOutdatedStoredValues()
|
||||
}
|
||||
|
||||
private let storage = Property<StoredValues>(
|
||||
fileName: storageFileName,
|
||||
initialValue: StoredValues(items: [:]),
|
||||
onError: { error in
|
||||
DivKitLogger.error("Failed to create storage: \(error)")
|
||||
}
|
||||
)
|
||||
|
||||
func set(value: DivStoredValue) {
|
||||
var items = storage.value.items
|
||||
items[value.name] = StoredValue(
|
||||
|
||||
@@ -3,11 +3,11 @@ import Serialization
|
||||
import VGSL
|
||||
|
||||
public struct DivTemplates: Deserializable, @unchecked Sendable {
|
||||
public static let empty = DivTemplates(dictionary: [:])
|
||||
|
||||
public let templates: [TemplateName: Any]
|
||||
public let templateToType: [TemplateName: String]
|
||||
|
||||
public static let empty = DivTemplates(dictionary: [:])
|
||||
|
||||
public init(
|
||||
templates: [TemplateName: Any],
|
||||
templatesToType: [TemplateName: String]
|
||||
|
||||
@@ -11,6 +11,8 @@ final class DivTimerController {
|
||||
case paused
|
||||
}
|
||||
|
||||
private(set) var state: State = .stopped
|
||||
|
||||
private let cardId: DivCardID
|
||||
private let divTimer: DivTimer
|
||||
private let timerScheduler: Scheduling
|
||||
@@ -22,8 +24,6 @@ final class DivTimerController {
|
||||
private let persistentValuesStorage: DivPersistentValuesStorage
|
||||
private let reporter: DivReporter
|
||||
|
||||
private(set) var state: State = .stopped
|
||||
|
||||
private var savedDuration: TimeInterval?
|
||||
private var savedInterval: TimeInterval?
|
||||
private var tickTimer: TimerType?
|
||||
|
||||
@@ -11,11 +11,15 @@ public final class DivVariableStorage {
|
||||
}
|
||||
|
||||
let initialPath: UIElementPath?
|
||||
let changeEvents: Signal<ChangeEvent>
|
||||
|
||||
private let outerStorage: DivVariableStorage?
|
||||
|
||||
private var _values = DivVariables()
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
private let changeEventsPipe = SignalPipe<ChangeEvent>()
|
||||
|
||||
/// Gets all available variables including variables from outer storage.
|
||||
public var allValues: DivVariables {
|
||||
lock.withLock {
|
||||
@@ -33,9 +37,6 @@ public final class DivVariableStorage {
|
||||
(outerStorage?.allValues ?? [:]) + _values
|
||||
}
|
||||
|
||||
private let changeEventsPipe = SignalPipe<ChangeEvent>()
|
||||
let changeEvents: Signal<ChangeEvent>
|
||||
|
||||
/// Initializes a new instance of ``DivVariableStorage``.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -186,6 +187,10 @@ public final class DivVariableStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public func addObserver(_ action: @escaping (ChangeEvent) -> Void) -> Disposable {
|
||||
changeEvents.addObserver(action)
|
||||
}
|
||||
|
||||
func update(
|
||||
name: DivVariableName,
|
||||
valueFactory: (DivVariableValue) -> DivVariableValue?
|
||||
@@ -218,10 +223,6 @@ public final class DivVariableStorage {
|
||||
return isUpdated
|
||||
}
|
||||
|
||||
public func addObserver(_ action: @escaping (ChangeEvent) -> Void) -> Disposable {
|
||||
changeEvents.addObserver(action)
|
||||
}
|
||||
|
||||
private func notify(_ event: ChangeEvent) {
|
||||
onMainThread { [weak self] in
|
||||
self?.changeEventsPipe.send(event)
|
||||
|
||||
@@ -12,28 +12,6 @@ public enum DivVariableValue: Hashable {
|
||||
case dict(DivDictionary)
|
||||
case array(DivArray)
|
||||
|
||||
@inlinable
|
||||
public func typedValue<T>() -> T? {
|
||||
switch self {
|
||||
case let .string(value):
|
||||
return value as? T
|
||||
case let .number(value):
|
||||
return value as? T
|
||||
case let .integer(value):
|
||||
return value as? T
|
||||
case let .bool(value):
|
||||
return value as? T
|
||||
case let .color(value):
|
||||
return value as? T
|
||||
case let .url(value):
|
||||
return value as? T
|
||||
case let .dict(value):
|
||||
return value as? T
|
||||
case let .array(value):
|
||||
return value as? T
|
||||
}
|
||||
}
|
||||
|
||||
init?(_ value: some Any) {
|
||||
switch value {
|
||||
case let value as String:
|
||||
@@ -57,4 +35,27 @@ public enum DivVariableValue: Hashable {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func typedValue<T>() -> T? {
|
||||
switch self {
|
||||
case let .string(value):
|
||||
return value as? T
|
||||
case let .number(value):
|
||||
return value as? T
|
||||
case let .integer(value):
|
||||
return value as? T
|
||||
case let .bool(value):
|
||||
return value as? T
|
||||
case let .color(value):
|
||||
return value as? T
|
||||
case let .url(value):
|
||||
return value as? T
|
||||
case let .dict(value):
|
||||
return value as? T
|
||||
case let .array(value):
|
||||
return value as? T
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@ public final class DivVariablesStorage {
|
||||
|
||||
public let kind: Kind
|
||||
|
||||
init(_ kind: Kind) {
|
||||
self.kind = kind
|
||||
}
|
||||
|
||||
public var changedVariables: Set<DivVariableName> {
|
||||
switch kind {
|
||||
case let .global(names):
|
||||
@@ -29,14 +25,20 @@ public final class DivVariablesStorage {
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
init(_ kind: Kind) {
|
||||
self.kind = kind
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public let changeEvents: Signal<ChangeEvent>
|
||||
|
||||
private let globalStorage: DivVariableStorage
|
||||
private var localStorages: [UIElementPath: DivVariableStorage] = [:]
|
||||
private let lock = AllocatedUnfairLock()
|
||||
|
||||
private let changeEventsPipe = SignalPipe<ChangeEvent>()
|
||||
public let changeEvents: Signal<ChangeEvent>
|
||||
|
||||
public convenience init() {
|
||||
self.init(outerStorage: nil)
|
||||
@@ -51,38 +53,6 @@ public final class DivVariablesStorage {
|
||||
changeEvents = Signal.merge(globalStorageEvents, changeEventsPipe.signal)
|
||||
}
|
||||
|
||||
func getOnlyElementVariables(cardId: DivCardID, elementId: String) -> DivVariables? {
|
||||
lock.withLock {
|
||||
let storages = localStorages.filter {
|
||||
$0.key.leaf == elementId && $0.key.cardId == cardId
|
||||
}.map(\.value)
|
||||
|
||||
guard let storage = storages.first else {
|
||||
DivKitLogger.error("Element with id \(elementId) not found")
|
||||
return nil
|
||||
}
|
||||
guard storages.count == 1 else {
|
||||
DivKitLogger.error("Found multiple elements that respond to id: \(elementId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard storage.initialPath?.leaf == elementId else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
return storage.values
|
||||
}
|
||||
}
|
||||
|
||||
func getVariableValue<T>(
|
||||
path: UIElementPath,
|
||||
name: DivVariableName
|
||||
) -> T? {
|
||||
lock.withLock {
|
||||
getNearestStorage(path).getValue(name)
|
||||
}
|
||||
}
|
||||
|
||||
public func getVariableValue<T>(
|
||||
cardId: DivCardID,
|
||||
name: DivVariableName
|
||||
@@ -106,24 +76,6 @@ public final class DivVariablesStorage {
|
||||
return localStorage?.hasValue(name) ?? globalStorage.hasValue(name)
|
||||
}
|
||||
|
||||
func initializeIfNeeded(path: UIElementPath, variables: DivVariables) {
|
||||
lock.withLock {
|
||||
if localStorages[path] != nil {
|
||||
// storage is already initialized
|
||||
return
|
||||
}
|
||||
let nearestStorage = getNearestStorage(path.parent)
|
||||
if variables.isEmpty {
|
||||
// optimization that allows to access the local storage for one operation
|
||||
localStorages[path] = nearestStorage
|
||||
} else {
|
||||
let localStorage = DivVariableStorage(outerStorage: nearestStorage, initialPath: path)
|
||||
localStorage.replaceAll(variables, notifyObservers: false)
|
||||
localStorages[path] = localStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces all card variables with new ones.
|
||||
/// Does not affect global variables.
|
||||
public func set(
|
||||
@@ -210,6 +162,56 @@ public final class DivVariablesStorage {
|
||||
changeEvents.addObserver(action)
|
||||
}
|
||||
|
||||
func getOnlyElementVariables(cardId: DivCardID, elementId: String) -> DivVariables? {
|
||||
lock.withLock {
|
||||
let storages = localStorages.filter {
|
||||
$0.key.leaf == elementId && $0.key.cardId == cardId
|
||||
}.map(\.value)
|
||||
|
||||
guard let storage = storages.first else {
|
||||
DivKitLogger.error("Element with id \(elementId) not found")
|
||||
return nil
|
||||
}
|
||||
guard storages.count == 1 else {
|
||||
DivKitLogger.error("Found multiple elements that respond to id: \(elementId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard storage.initialPath?.leaf == elementId else {
|
||||
return [:]
|
||||
}
|
||||
|
||||
return storage.values
|
||||
}
|
||||
}
|
||||
|
||||
func getVariableValue<T>(
|
||||
path: UIElementPath,
|
||||
name: DivVariableName
|
||||
) -> T? {
|
||||
lock.withLock {
|
||||
getNearestStorage(path).getValue(name)
|
||||
}
|
||||
}
|
||||
|
||||
func initializeIfNeeded(path: UIElementPath, variables: DivVariables) {
|
||||
lock.withLock {
|
||||
if localStorages[path] != nil {
|
||||
// storage is already initialized
|
||||
return
|
||||
}
|
||||
let nearestStorage = getNearestStorage(path.parent)
|
||||
if variables.isEmpty {
|
||||
// optimization that allows to access the local storage for one operation
|
||||
localStorages[path] = nearestStorage
|
||||
} else {
|
||||
let localStorage = DivVariableStorage(outerStorage: nearestStorage, initialPath: path)
|
||||
localStorage.replaceAll(variables, notifyObservers: false)
|
||||
localStorages[path] = localStorage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getNearestStorage(_ path: UIElementPath?) -> DivVariableStorage {
|
||||
var currentPath: UIElementPath? = path
|
||||
while let path = currentPath {
|
||||
|
||||
@@ -6,17 +6,6 @@ import VGSL
|
||||
|
||||
@MainActor
|
||||
final class DivBlockProvider {
|
||||
private let divKitComponents: DivKitComponents
|
||||
private let onCardSizeChanged: (DivCardID, DivViewSize) -> Void
|
||||
private let disposePool = AutodisposePool()
|
||||
|
||||
private var divData: DivData? {
|
||||
didSet {
|
||||
guard oldValue !== divData else { return }
|
||||
update(reasons: [])
|
||||
}
|
||||
}
|
||||
|
||||
private(set) var id: DivViewId!
|
||||
|
||||
private(set) var cardSize: DivViewSize? {
|
||||
@@ -27,16 +16,6 @@ final class DivBlockProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private var debugParams = DebugParams()
|
||||
private var dataErrors = [DeserializationError]()
|
||||
private var updateInProgress = false
|
||||
|
||||
private let measurements = DebugParams.Measurements(
|
||||
divDataParsingTime: TimeMeasure(),
|
||||
renderTime: TimeMeasure(),
|
||||
templateParsingTime: TimeMeasure()
|
||||
)
|
||||
|
||||
@ObservableProperty
|
||||
private(set) var block: Block = noDataBlock {
|
||||
didSet {
|
||||
@@ -51,15 +30,36 @@ final class DivBlockProvider {
|
||||
}
|
||||
}
|
||||
|
||||
var cardId: DivCardID {
|
||||
id.cardId
|
||||
}
|
||||
|
||||
@ObservableProperty
|
||||
private(set) var shouldRecalculateVisibility: Bool = true
|
||||
|
||||
var accessibilityElementsStorage: DivAccessibilityElementsStorage?
|
||||
|
||||
private let divKitComponents: DivKitComponents
|
||||
private let onCardSizeChanged: (DivCardID, DivViewSize) -> Void
|
||||
private let disposePool = AutodisposePool()
|
||||
|
||||
private var divData: DivData? {
|
||||
didSet {
|
||||
guard oldValue !== divData else { return }
|
||||
update(reasons: [])
|
||||
}
|
||||
}
|
||||
|
||||
private var debugParams = DebugParams()
|
||||
private var dataErrors = [DeserializationError]()
|
||||
private var updateInProgress = false
|
||||
|
||||
private let measurements = DebugParams.Measurements(
|
||||
divDataParsingTime: TimeMeasure(),
|
||||
renderTime: TimeMeasure(),
|
||||
templateParsingTime: TimeMeasure()
|
||||
)
|
||||
|
||||
var cardId: DivCardID {
|
||||
id.cardId
|
||||
}
|
||||
|
||||
init(
|
||||
divKitComponents: DivKitComponents,
|
||||
onCardSizeChanged: @escaping (DivCardID, DivViewSize) -> Void
|
||||
@@ -108,6 +108,80 @@ final class DivBlockProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func update(reasons: [DivCardUpdateReason]) {
|
||||
guard var divData else {
|
||||
block = debugParams.isDebugInfoEnabled ? makeErrorsBlock(dataErrors) : noDataBlock
|
||||
return
|
||||
}
|
||||
|
||||
guard needUpdateBlock(reasons: reasons) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !updateInProgress else {
|
||||
return
|
||||
}
|
||||
|
||||
updateInProgress = true
|
||||
defer {
|
||||
updateInProgress = false
|
||||
}
|
||||
|
||||
let context = makeCurrentContext()
|
||||
|
||||
reasons.compactMap { $0.patch(for: self.cardId) }.forEach { patch in
|
||||
divData = divData.applyPatchWithActions(
|
||||
patch,
|
||||
context: context
|
||||
)
|
||||
}
|
||||
|
||||
self.divData = divData
|
||||
|
||||
if reasons.filter(\.isVariable).isEmpty {
|
||||
context.layoutProviderHandler?.resetUpdatedVariables()
|
||||
shouldRecalculateVisibility = true
|
||||
}
|
||||
dataErrors.forEach { context.errorsStorage.add($0) }
|
||||
do {
|
||||
block = try measurements.renderTime.updateMeasure {
|
||||
try divData.makeBlock(
|
||||
context: context
|
||||
)
|
||||
}
|
||||
accessibilityElementsStorage = context.accessibilityElementsStorage
|
||||
debugParams.processMeasurements((cardId: cardId, measurements: measurements))
|
||||
for error in context.errorsStorage.errors {
|
||||
divKitComponents.reporter.reportError(cardId: cardId, error: error)
|
||||
}
|
||||
if !divKitComponents.flagsInfo.initializeTriggerOnSet {
|
||||
divKitComponents.triggersStorage.initialize(cardId: cardId)
|
||||
}
|
||||
} catch {
|
||||
divKitComponents.reporter.reportError(
|
||||
cardId: cardId,
|
||||
error: DivUnknownError(error, path: UIElementPath(cardId.rawValue))
|
||||
)
|
||||
block = handleError(error: error, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
func update(withStates blockStates: BlocksState) {
|
||||
do {
|
||||
block = try block.updated(withStates: blockStates)
|
||||
} catch {
|
||||
block = handleError(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func update(path: UIElementPath, isFocused: Bool) {
|
||||
do {
|
||||
block = try block.updated(path: path, isFocused: isFocused)
|
||||
} catch {
|
||||
block = handleError(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func update(data: Data) {
|
||||
dataErrors = []
|
||||
do {
|
||||
@@ -189,64 +263,6 @@ final class DivBlockProvider {
|
||||
self.divData = divData
|
||||
}
|
||||
|
||||
func update(reasons: [DivCardUpdateReason]) {
|
||||
guard var divData else {
|
||||
block = debugParams.isDebugInfoEnabled ? makeErrorsBlock(dataErrors) : noDataBlock
|
||||
return
|
||||
}
|
||||
|
||||
guard needUpdateBlock(reasons: reasons) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !updateInProgress else {
|
||||
return
|
||||
}
|
||||
|
||||
updateInProgress = true
|
||||
defer {
|
||||
updateInProgress = false
|
||||
}
|
||||
|
||||
let context = makeCurrentContext()
|
||||
|
||||
reasons.compactMap { $0.patch(for: self.cardId) }.forEach { patch in
|
||||
divData = divData.applyPatchWithActions(
|
||||
patch,
|
||||
context: context
|
||||
)
|
||||
}
|
||||
|
||||
self.divData = divData
|
||||
|
||||
if reasons.filter(\.isVariable).isEmpty {
|
||||
context.layoutProviderHandler?.resetUpdatedVariables()
|
||||
shouldRecalculateVisibility = true
|
||||
}
|
||||
dataErrors.forEach { context.errorsStorage.add($0) }
|
||||
do {
|
||||
block = try measurements.renderTime.updateMeasure {
|
||||
try divData.makeBlock(
|
||||
context: context
|
||||
)
|
||||
}
|
||||
accessibilityElementsStorage = context.accessibilityElementsStorage
|
||||
debugParams.processMeasurements((cardId: cardId, measurements: measurements))
|
||||
for error in context.errorsStorage.errors {
|
||||
divKitComponents.reporter.reportError(cardId: cardId, error: error)
|
||||
}
|
||||
if !divKitComponents.flagsInfo.initializeTriggerOnSet {
|
||||
divKitComponents.triggersStorage.initialize(cardId: cardId)
|
||||
}
|
||||
} catch {
|
||||
divKitComponents.reporter.reportError(
|
||||
cardId: cardId,
|
||||
error: DivUnknownError(error, path: UIElementPath(cardId.rawValue))
|
||||
)
|
||||
block = handleError(error: error, context: context)
|
||||
}
|
||||
}
|
||||
|
||||
private func needUpdateBlock(reasons: [DivCardUpdateReason]) -> Bool {
|
||||
guard !reasons.isEmpty else { return true }
|
||||
for reason in reasons {
|
||||
@@ -283,22 +299,6 @@ final class DivBlockProvider {
|
||||
)
|
||||
}
|
||||
|
||||
func update(withStates blockStates: BlocksState) {
|
||||
do {
|
||||
block = try block.updated(withStates: blockStates)
|
||||
} catch {
|
||||
block = handleError(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
func update(path: UIElementPath, isFocused: Bool) {
|
||||
do {
|
||||
block = try block.updated(path: path, isFocused: isFocused)
|
||||
} catch {
|
||||
block = handleError(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func parseDivDataWithTemplates(
|
||||
_ jsonDict: [String: Any],
|
||||
cardId: DivCardID
|
||||
|
||||
@@ -48,12 +48,12 @@ public struct DivHostingView: UIViewRepresentable {
|
||||
}
|
||||
|
||||
private class VisibilityTrackingView: UIView {
|
||||
let divView: DivView
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
divView.intrinsicContentSize
|
||||
}
|
||||
|
||||
let divView: DivView
|
||||
|
||||
init(divView: DivView) {
|
||||
self.divView = divView
|
||||
|
||||
|
||||
@@ -14,6 +14,34 @@ import VGSL
|
||||
/// the demo app.
|
||||
|
||||
public final class DivView: VisibleBoundsTrackingView {
|
||||
/// Returns the intrinsic content size of the ``DivView``.
|
||||
///
|
||||
/// - Returns: The calculated intrinsic content size.
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
guard let cardSize else {
|
||||
return CGSize(width: DivView.noIntrinsicMetric, height: DivView.noIntrinsicMetric)
|
||||
}
|
||||
let width: CGFloat
|
||||
switch cardSize.width {
|
||||
case .matchParent:
|
||||
width = bounds.width == 0 ? DivView.noIntrinsicMetric : bounds.width
|
||||
case let .desired(value):
|
||||
width = value
|
||||
case .dependsOnOtherDimensionSize:
|
||||
assertionFailure("Width depends on other dimension size")
|
||||
width = DivView.noIntrinsicMetric
|
||||
}
|
||||
let height: CGFloat = switch cardSize.height {
|
||||
case .matchParent:
|
||||
bounds.height == 0 ? DivView.noIntrinsicMetric : bounds.height
|
||||
case let .desired(value):
|
||||
value
|
||||
case let .dependsOnOtherDimensionSize(heightForWidth):
|
||||
heightForWidth(width)
|
||||
}
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
private let divKitComponents: DivKitComponents
|
||||
private let preloader: DivViewPreloader
|
||||
private var blockSubscription: Disposable?
|
||||
@@ -42,6 +70,13 @@ public final class DivView: VisibleBoundsTrackingView {
|
||||
|
||||
private let boundsTracker = BoundsTracker()
|
||||
|
||||
/// Returns ``DivCardSize`` of the ``DivView``.
|
||||
///
|
||||
/// - Returns: The calculated ``DivCardSize``.
|
||||
public var cardSize: DivViewSize? {
|
||||
blockProvider?.cardSize
|
||||
}
|
||||
|
||||
/// Initializes a new `DivView` instance.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -60,6 +95,43 @@ public final class DivView: VisibleBoundsTrackingView {
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard let blockView else { return }
|
||||
|
||||
blockView.frame = bounds
|
||||
blockView.layoutIfNeeded()
|
||||
|
||||
if shouldRecalculateVisibility {
|
||||
blockView.onVisibleBoundsChanged(
|
||||
from: boundsTracker.previousBounds,
|
||||
to: boundsTracker.lastVisibleBounds
|
||||
)
|
||||
|
||||
shouldRecalculateVisibility = false
|
||||
boundsTracker.updatePreviousBounds()
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSublayers(of layer: CALayer) {
|
||||
super.layoutSublayers(of: layer)
|
||||
|
||||
invalidateIntrinsicContentSizeIfBoundsChanged()
|
||||
}
|
||||
|
||||
public override func removeFromSuperview() {
|
||||
super.removeFromSuperview()
|
||||
blockView?.onVisibleBoundsChanged(
|
||||
from: boundsTracker.lastVisibleBounds,
|
||||
to: .zero
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the source of the ``DivView`` and updates the layout.
|
||||
/// - Parameters:
|
||||
/// - source: The source of the ``DivView``, specified using `JSON` data, a `Data` object, or
|
||||
@@ -149,50 +221,6 @@ public final class DivView: VisibleBoundsTrackingView {
|
||||
blockProvider?.update(reasons: [.patch(cardId, patch)])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard let blockView else { return }
|
||||
|
||||
blockView.frame = bounds
|
||||
blockView.layoutIfNeeded()
|
||||
|
||||
if shouldRecalculateVisibility {
|
||||
blockView.onVisibleBoundsChanged(
|
||||
from: boundsTracker.previousBounds,
|
||||
to: boundsTracker.lastVisibleBounds
|
||||
)
|
||||
|
||||
shouldRecalculateVisibility = false
|
||||
boundsTracker.updatePreviousBounds()
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSublayers(of layer: CALayer) {
|
||||
super.layoutSublayers(of: layer)
|
||||
|
||||
invalidateIntrinsicContentSizeIfBoundsChanged()
|
||||
}
|
||||
|
||||
public override func removeFromSuperview() {
|
||||
super.removeFromSuperview()
|
||||
blockView?.onVisibleBoundsChanged(
|
||||
from: boundsTracker.lastVisibleBounds,
|
||||
to: .zero
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns ``DivCardSize`` of the ``DivView``.
|
||||
///
|
||||
/// - Returns: The calculated ``DivCardSize``.
|
||||
public var cardSize: DivViewSize? {
|
||||
blockProvider?.cardSize
|
||||
}
|
||||
|
||||
/// Adds an observer to listen for ``DivView`` estimated size changes.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -208,32 +236,23 @@ public final class DivView: VisibleBoundsTrackingView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the intrinsic content size of the ``DivView``.
|
||||
///
|
||||
/// - Returns: The calculated intrinsic content size.
|
||||
public override var intrinsicContentSize: CGSize {
|
||||
guard let cardSize else {
|
||||
return CGSize(width: DivView.noIntrinsicMetric, height: DivView.noIntrinsicMetric)
|
||||
/// Notifies the DivView about changes in its visible bounds.
|
||||
/// - Parameters:
|
||||
/// - to: The new bounds rectangle.
|
||||
public func onVisibleBoundsChanged(to: CGRect) {
|
||||
guard boundsTracker.append(to) else { return }
|
||||
|
||||
shouldRecalculateVisibility = true
|
||||
if window == nil {
|
||||
forceLayout()
|
||||
} else {
|
||||
setNeedsLayout()
|
||||
}
|
||||
let width: CGFloat
|
||||
switch cardSize.width {
|
||||
case .matchParent:
|
||||
width = bounds.width == 0 ? DivView.noIntrinsicMetric : bounds.width
|
||||
case let .desired(value):
|
||||
width = value
|
||||
case .dependsOnOtherDimensionSize:
|
||||
assertionFailure("Width depends on other dimension size")
|
||||
width = DivView.noIntrinsicMetric
|
||||
}
|
||||
let height: CGFloat = switch cardSize.height {
|
||||
case .matchParent:
|
||||
bounds.height == 0 ? DivView.noIntrinsicMetric : bounds.height
|
||||
case let .desired(value):
|
||||
value
|
||||
case let .dependsOnOtherDimensionSize(heightForWidth):
|
||||
heightForWidth(width)
|
||||
}
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
/// Use ``onVisibleBoundsChanged(to:)`` instead.
|
||||
public func onVisibleBoundsChanged(from _: CGRect, to: CGRect) {
|
||||
onVisibleBoundsChanged(to: to)
|
||||
}
|
||||
|
||||
private func update(block: Block) {
|
||||
@@ -272,24 +291,6 @@ public final class DivView: VisibleBoundsTrackingView {
|
||||
clearFocus()
|
||||
}
|
||||
|
||||
/// Notifies the DivView about changes in its visible bounds.
|
||||
/// - Parameters:
|
||||
/// - to: The new bounds rectangle.
|
||||
public func onVisibleBoundsChanged(to: CGRect) {
|
||||
guard boundsTracker.append(to) else { return }
|
||||
|
||||
shouldRecalculateVisibility = true
|
||||
if window == nil {
|
||||
forceLayout()
|
||||
} else {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
/// Use ``onVisibleBoundsChanged(to:)`` instead.
|
||||
public func onVisibleBoundsChanged(from _: CGRect, to: CGRect) {
|
||||
onVisibleBoundsChanged(to: to)
|
||||
}
|
||||
}
|
||||
|
||||
extension DivView: ElementStateObserver {
|
||||
|
||||
@@ -14,10 +14,12 @@ public final class DivViewPreloader {
|
||||
public let estimatedSize: DivViewSize
|
||||
}
|
||||
|
||||
private(set) var setSourceTask: Task<Void, Error>?
|
||||
|
||||
private var blockProviders = [DivCardID: DivBlockProvider]()
|
||||
private let divKitComponents: DivKitComponents
|
||||
private let changeEventsPipe = SignalPipe<DivViewSizeChange>()
|
||||
private(set) var setSourceTask: Task<Void, Error>?
|
||||
|
||||
var changeEvents: Signal<DivViewSizeChange> {
|
||||
changeEventsPipe.signal
|
||||
}
|
||||
@@ -30,18 +32,6 @@ public final class DivViewPreloader {
|
||||
self.divKitComponents = divKitComponents
|
||||
}
|
||||
|
||||
func blockProvider(for cardId: DivCardID) -> DivBlockProvider {
|
||||
if let blockProvider = blockProviders[cardId] {
|
||||
return blockProvider
|
||||
} else {
|
||||
let blockProvider = DivBlockProvider(divKitComponents: divKitComponents) { [weak self] in
|
||||
self?.changeEventsPipe.send(DivViewSizeChange(cardId: $0, estimatedSize: $1))
|
||||
}
|
||||
blockProviders[cardId] = blockProvider
|
||||
return blockProvider
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the source for ``DivViewPreloader`` and updates the layout.
|
||||
/// - Parameters:
|
||||
/// - source: The source of the ``DivView``.
|
||||
@@ -131,6 +121,18 @@ public final class DivViewPreloader {
|
||||
changeEvents.addObserver(onCardSizeChanged)
|
||||
}
|
||||
|
||||
func blockProvider(for cardId: DivCardID) -> DivBlockProvider {
|
||||
if let blockProvider = blockProviders[cardId] {
|
||||
return blockProvider
|
||||
} else {
|
||||
let blockProvider = DivBlockProvider(divKitComponents: divKitComponents) { [weak self] in
|
||||
self?.changeEventsPipe.send(DivViewSizeChange(cardId: $0, estimatedSize: $1))
|
||||
}
|
||||
blockProviders[cardId] = blockProvider
|
||||
return blockProvider
|
||||
}
|
||||
}
|
||||
|
||||
func reset(cardId: DivCardID) {
|
||||
blockProviders.removeValue(forKey: cardId)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@ public struct DivViewSize: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public let width: DivDimension
|
||||
public let height: DivDimension
|
||||
|
||||
private let constrainedWidth: Bool
|
||||
private let constrainedHeight: Bool
|
||||
|
||||
public init(block: Block) {
|
||||
let width: DivDimension = block
|
||||
.isHorizontallyResizable ? .matchParent : .desired(block.widthOfHorizontallyNonResizableBlock)
|
||||
@@ -40,12 +46,6 @@ public struct DivViewSize: Equatable {
|
||||
self.constrainedHeight = block.isVerticallyConstrained
|
||||
}
|
||||
|
||||
public let width: DivDimension
|
||||
public let height: DivDimension
|
||||
|
||||
private let constrainedWidth: Bool
|
||||
private let constrainedHeight: Bool
|
||||
|
||||
/// Computes the actual size for a ``DivView`` given its parent's size.
|
||||
///
|
||||
/// - Parameters:
|
||||
|
||||
@@ -19,8 +19,6 @@ final class AnimationBlockView: BlockView {
|
||||
}
|
||||
}
|
||||
|
||||
private var animationRequest: Cancellable?
|
||||
|
||||
var isPlaying: Bool = true {
|
||||
didSet {
|
||||
guard oldValue != isPlaying else { return }
|
||||
@@ -57,21 +55,24 @@ final class AnimationBlockView: BlockView {
|
||||
}
|
||||
}
|
||||
|
||||
let effectiveBackgroundColor: UIColor? = nil
|
||||
|
||||
private var animationRequest: Cancellable?
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
animatableView?.frame = bounds
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
let effectiveBackgroundColor: UIColor? = nil
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
animatableView?.frame = bounds
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AnimationBlockView: VisibleBoundsTrackingLeaf {}
|
||||
|
||||
@@ -11,6 +11,8 @@ final class LottieAnimationBlock: SizeForwardingBlock {
|
||||
let scale: DivImageScale
|
||||
var isPlaying: Bool
|
||||
|
||||
let intrinsicContentWidth: CGFloat = 0
|
||||
|
||||
var debugDescription: String {
|
||||
"Animation Block playing animation with view: \(animatableView)"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,18 @@ public final class LottieExtensionHandler: DivExtensionHandler {
|
||||
self.localAnimationDataProvider = localAnimationDataProvider
|
||||
}
|
||||
|
||||
static func getPreloadURL(div: DivBase, expressionResolver: ExpressionResolver) -> URL? {
|
||||
let extensionData = div.extensions?.first { $0.id == "lottie" }
|
||||
guard let paramsDict = extensionData?.params,
|
||||
let params = LottieExtensionParams(
|
||||
paramsDictionary: paramsDict,
|
||||
expressionResolver: expressionResolver
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
return params.source.url
|
||||
}
|
||||
|
||||
public func applyAfterBaseProperties(
|
||||
to block: Block,
|
||||
div: DivBase,
|
||||
@@ -67,22 +79,20 @@ public final class LottieExtensionHandler: DivExtensionHandler {
|
||||
[Self.getPreloadURL(div: div, expressionResolver: expressionResolver)].compactMap { $0 }
|
||||
}
|
||||
|
||||
static func getPreloadURL(div: DivBase, expressionResolver: ExpressionResolver) -> URL? {
|
||||
let extensionData = div.extensions?.first { $0.id == "lottie" }
|
||||
guard let paramsDict = extensionData?.params,
|
||||
let params = LottieExtensionParams(
|
||||
paramsDictionary: paramsDict,
|
||||
expressionResolver: expressionResolver
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
return params.source.url
|
||||
}
|
||||
}
|
||||
|
||||
private class JSONAnimationHolder: AnimationHolder {
|
||||
let animation: AnimationSourceType?
|
||||
|
||||
var debugDescription: String {
|
||||
guard let animation = animation as? LottieAnimationSourceType,
|
||||
case let .json(json) = animation else {
|
||||
assertionFailure("JSONAnimation holder can hold only json ")
|
||||
return ""
|
||||
}
|
||||
return "JSON Animation holder with json \(json)"
|
||||
}
|
||||
|
||||
init(json: [String: Any]) {
|
||||
self.animation = LottieAnimationSourceType.json(json)
|
||||
}
|
||||
@@ -102,12 +112,4 @@ private class JSONAnimationHolder: AnimationHolder {
|
||||
return false
|
||||
}
|
||||
|
||||
var debugDescription: String {
|
||||
guard let animation = animation as? LottieAnimationSourceType,
|
||||
case let .json(json) = animation else {
|
||||
assertionFailure("JSONAnimation holder can hold only json ")
|
||||
return ""
|
||||
}
|
||||
return "JSON Animation holder with json \(json)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ public final class RemoteAnimationHolder: AnimationHolder {
|
||||
private typealias AsyncAnimationRequester = (@escaping (AnimationSourceType?) -> Void)
|
||||
-> Cancellable?
|
||||
|
||||
let url: URL
|
||||
public private(set) var animation: AnimationSourceType?
|
||||
|
||||
let url: URL
|
||||
|
||||
private let animationType: AnimationType
|
||||
private let resourceRequester: AsyncAnimationRequester
|
||||
|
||||
|
||||
@@ -4,15 +4,27 @@ import LayoutKit
|
||||
import VGSL
|
||||
|
||||
public final class RiveAnimationBlock: BlockWithTraits {
|
||||
let animationHolder: AnimationHolder
|
||||
let animatableView: Lazy<AsyncSourceAnimatableView>
|
||||
public let widthTrait: LayoutTrait
|
||||
public let heightTrait: LayoutTrait
|
||||
|
||||
let animationHolder: AnimationHolder
|
||||
let animatableView: Lazy<AsyncSourceAnimatableView>
|
||||
|
||||
public var debugDescription: String {
|
||||
"Sized Animation Block is playing animation with \(animationHolder)"
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(value):
|
||||
value
|
||||
case let .intrinsic(_, minSize, _):
|
||||
minSize
|
||||
case .weighted:
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
animationHolder: AnimationHolder,
|
||||
animatableView: Lazy<AsyncSourceAnimatableView>,
|
||||
@@ -25,17 +37,6 @@ public final class RiveAnimationBlock: BlockWithTraits {
|
||||
self.heightTrait = heightTrait
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(value):
|
||||
value
|
||||
case let .intrinsic(_, minSize, _):
|
||||
minSize
|
||||
case .weighted:
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat {
|
||||
switch heightTrait {
|
||||
case let .fixed(value):
|
||||
|
||||
@@ -5,6 +5,7 @@ import VGSL
|
||||
|
||||
public final class CustomImagePreviewExtensionHandler: DivExtensionHandler {
|
||||
public let id: String
|
||||
|
||||
private let viewProvider: ViewProvider
|
||||
|
||||
public init(id: String, viewProvider: ViewProvider) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import VGSL
|
||||
|
||||
public final class ImageExtensionHandler: DivExtensionHandler {
|
||||
public let id: String
|
||||
|
||||
private let image: ImageHolder?
|
||||
|
||||
public init(
|
||||
|
||||
@@ -5,6 +5,7 @@ import VGSL
|
||||
|
||||
public final class ImageThemeExtensionHandler: DivExtensionHandler {
|
||||
public let id = extensionID
|
||||
|
||||
private let theme: Variable<Theme>
|
||||
|
||||
public init(theme: Variable<Theme>) {
|
||||
|
||||
@@ -10,6 +10,7 @@ public protocol InputAccessoryViewProvider {
|
||||
|
||||
public final class InputAccessoryViewExtensionHandler: DivExtensionHandler {
|
||||
public let id = "input_accessory_view"
|
||||
|
||||
private let viewProvider: InputAccessoryViewProvider
|
||||
|
||||
public init(viewProvider: InputAccessoryViewProvider) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import LayoutKit
|
||||
import UIKit
|
||||
|
||||
public final class PinchToZoomExtensionHandler: DivExtensionHandler {
|
||||
private weak var overlayView: UIView?
|
||||
|
||||
public let id = "pinch-to-zoom"
|
||||
|
||||
private weak var overlayView: UIView?
|
||||
|
||||
public init(overlayView: UIView) {
|
||||
self.overlayView = overlayView
|
||||
}
|
||||
|
||||
@@ -4,8 +4,13 @@ import VGSL
|
||||
|
||||
public final class TextExtensionHandler: DivExtensionHandler {
|
||||
public let id: String
|
||||
|
||||
private let text: String?
|
||||
|
||||
public var accessibilityElement: AccessibilityElement? {
|
||||
.none(label: text)
|
||||
}
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
text: String?
|
||||
@@ -14,10 +19,6 @@ public final class TextExtensionHandler: DivExtensionHandler {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
public var accessibilityElement: AccessibilityElement? {
|
||||
.none(label: text)
|
||||
}
|
||||
|
||||
public func applyBeforeBaseProperties(
|
||||
to block: Block,
|
||||
div _: DivBase,
|
||||
|
||||
@@ -6,6 +6,7 @@ import VGSL
|
||||
|
||||
public final class ShimmerImagePreviewExtension: DivExtensionHandler {
|
||||
public let id: String = extensionID
|
||||
|
||||
private let effectBeginTime = CACurrentMediaTime()
|
||||
|
||||
public init() {}
|
||||
|
||||
@@ -85,6 +85,7 @@ final class LottieExtensionHandlerTests: XCTestCase {
|
||||
|
||||
private final class MockLottieAnimationFactory: AsyncSourceAnimatableViewFactory {
|
||||
var returnView = MockAnimatableView(frame: .zero)
|
||||
|
||||
func createAsyncSourceAnimatableView(withMode _: AnimationRepeatMode, repeatCount _: Float)
|
||||
-> AsyncSourceAnimatableView {
|
||||
returnView
|
||||
|
||||
@@ -9,6 +9,12 @@ import VGSL
|
||||
enum AppComponents {
|
||||
static let fontProvider = YSFontProvider()
|
||||
|
||||
static var debugParams: DebugParams {
|
||||
DebugParams(
|
||||
isDebugInfoEnabled: true
|
||||
)
|
||||
}
|
||||
|
||||
static func makeDivKitComponents(
|
||||
layoutDirection: UserInterfaceLayoutDirection = .system,
|
||||
reporter: DivReporter = PlaygroundReporter(),
|
||||
@@ -54,11 +60,6 @@ enum AppComponents {
|
||||
)
|
||||
}
|
||||
|
||||
static var debugParams: DebugParams {
|
||||
DebugParams(
|
||||
isDebugInfoEnabled: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCachingPlayerFactory(requester: URLResourceRequesting) -> PlayerFactory {
|
||||
|
||||
@@ -37,6 +37,7 @@ struct RiveDivCustomData {
|
||||
}
|
||||
|
||||
static let divCustomType = "rive_animation"
|
||||
|
||||
let url: URL
|
||||
let fit: Fit
|
||||
let alignment: Alignment
|
||||
|
||||
@@ -3,13 +3,14 @@ import SwiftUI
|
||||
|
||||
@main
|
||||
struct DivKitPlaygroundApp: App {
|
||||
init() {
|
||||
DivKitLogger.isEnabled = true
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainView()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
DivKitLogger.isEnabled = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,16 +5,17 @@ import VGSL
|
||||
typealias LogError = (String) -> Void
|
||||
|
||||
final class MetadataCaptureSession: NSObject, AVCaptureMetadataOutputObjectsDelegate {
|
||||
let result: ObservableProperty<String>
|
||||
|
||||
private let captureSession = AVCaptureSession()
|
||||
private let previewLayer: AVCaptureVideoPreviewLayer
|
||||
|
||||
let result: ObservableProperty<String>
|
||||
private let logError: LogError
|
||||
|
||||
var layer: CALayer { previewLayer }
|
||||
|
||||
private var isInitialized = false
|
||||
|
||||
var layer: CALayer { previewLayer }
|
||||
|
||||
init(
|
||||
result: ObservableProperty<String>,
|
||||
logError: @escaping LogError
|
||||
|
||||
@@ -2,10 +2,10 @@ import UIKit
|
||||
import VGSL
|
||||
|
||||
final class ScannerViewController: UIViewController {
|
||||
private let captureSession: Lazy<MetadataCaptureSession>
|
||||
|
||||
let result = ObservableProperty(initialValue: "")
|
||||
|
||||
private let captureSession: Lazy<MetadataCaptureSession>
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RegressionTestsModel: Decodable {
|
||||
let tests: [RegressionTestModel]
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
tests = try decoder
|
||||
.container(keyedBy: CodingKeys.self)
|
||||
.decode([SafeDecodable<RegressionTestModel>].self, forKey: .tests)
|
||||
.compactMap(\.value)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case tests
|
||||
}
|
||||
@@ -26,9 +17,50 @@ struct RegressionTestsModel: Decodable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tests: [RegressionTestModel]
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
tests = try decoder
|
||||
.container(keyedBy: CodingKeys.self)
|
||||
.decode([SafeDecodable<RegressionTestModel>].self, forKey: .tests)
|
||||
.compactMap(\.value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct RegressionTestModel: Decodable, Hashable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case expectedResults = "expected_results"
|
||||
case file
|
||||
case platforms
|
||||
case steps
|
||||
case tags
|
||||
case title
|
||||
}
|
||||
|
||||
private struct DecodingError: LocalizedError {
|
||||
let description: String
|
||||
|
||||
var errorDescription: String? {
|
||||
NSLocalizedString(description, comment: "DecodingError")
|
||||
}
|
||||
|
||||
init(_ description: String) {
|
||||
self.description = description
|
||||
}
|
||||
|
||||
init(key: CodingKeys, error: Error) {
|
||||
description = "Failed to read \(key.rawValue): \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
init(key: CodingKeys, title: String, error: Error) {
|
||||
description =
|
||||
"Failed to read \(key.rawValue) in test '\(title)': \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let expectedResults: [String]
|
||||
let platforms: [Platform]
|
||||
let steps: [String]
|
||||
@@ -36,6 +68,28 @@ struct RegressionTestModel: Decodable, Hashable {
|
||||
let title: String
|
||||
let url: URL
|
||||
|
||||
var description: String {
|
||||
var description = ""
|
||||
if !steps.isEmpty {
|
||||
description += "Steps:"
|
||||
for step in steps {
|
||||
description += "\n • \(step)"
|
||||
}
|
||||
}
|
||||
|
||||
if !expectedResults.isEmpty {
|
||||
if !steps.isEmpty {
|
||||
description += "\n\n"
|
||||
}
|
||||
description += "Expected results:"
|
||||
for result in expectedResults {
|
||||
description += "\n • \(result)"
|
||||
}
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
@@ -73,57 +127,6 @@ struct RegressionTestModel: Decodable, Hashable {
|
||||
tags = (try? container.decodeIfPresent([String].self, forKey: .tags)) ?? []
|
||||
}
|
||||
|
||||
var description: String {
|
||||
var description = ""
|
||||
if !steps.isEmpty {
|
||||
description += "Steps:"
|
||||
for step in steps {
|
||||
description += "\n • \(step)"
|
||||
}
|
||||
}
|
||||
|
||||
if !expectedResults.isEmpty {
|
||||
if !steps.isEmpty {
|
||||
description += "\n\n"
|
||||
}
|
||||
description += "Expected results:"
|
||||
for result in expectedResults {
|
||||
description += "\n • \(result)"
|
||||
}
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case expectedResults = "expected_results"
|
||||
case file
|
||||
case platforms
|
||||
case steps
|
||||
case tags
|
||||
case title
|
||||
}
|
||||
|
||||
private struct DecodingError: LocalizedError {
|
||||
let description: String
|
||||
|
||||
init(_ description: String) {
|
||||
self.description = description
|
||||
}
|
||||
|
||||
init(key: CodingKeys, error: Error) {
|
||||
description = "Failed to read \(key.rawValue): \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
init(key: CodingKeys, title: String, error: Error) {
|
||||
description =
|
||||
"Failed to read \(key.rawValue) in test '\(title)': \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
var errorDescription: String? {
|
||||
NSLocalizedString(description, comment: "DecodingError")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Platform: String, Decodable {
|
||||
|
||||
@@ -9,6 +9,15 @@ struct RegressionView: View {
|
||||
|
||||
private let divViewProvider: DivViewProvider
|
||||
|
||||
private var tests: [RegressionTestModel] {
|
||||
if query.isEmpty {
|
||||
return TestData.regressionTests
|
||||
}
|
||||
return TestData.regressionTests.filter {
|
||||
$0.title.range(of: query, options: .caseInsensitive) != nil
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
divViewProvider: DivViewProvider
|
||||
) {
|
||||
@@ -40,14 +49,6 @@ struct RegressionView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var tests: [RegressionTestModel] {
|
||||
if query.isEmpty {
|
||||
return TestData.regressionTests
|
||||
}
|
||||
return TestData.regressionTests.filter {
|
||||
$0.title.range(of: query, options: .caseInsensitive) != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationButton<Destination>: View where Destination: View {
|
||||
|
||||
@@ -14,6 +14,11 @@ open class DivViewController: UIViewController {
|
||||
|
||||
private let identifier: String = "baseDivView"
|
||||
|
||||
@available(*, unavailable)
|
||||
public required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
init(
|
||||
jsonPublisher: JsonPublisher,
|
||||
divKitComponents: DivKitComponents,
|
||||
@@ -33,11 +38,6 @@ open class DivViewController: UIViewController {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
public required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func loadView() {
|
||||
scrollView.backgroundColor = .white
|
||||
view = scrollView
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
|
||||
final class DivViewProvider {
|
||||
let jsonProvider: PlaygroundJsonProvider
|
||||
|
||||
private let divKitComponents: DivKitComponents
|
||||
private let layoutDirection: UIUserInterfaceLayoutDirection
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ struct SampleView: View {
|
||||
}
|
||||
|
||||
final class SamplesModel {
|
||||
private let divKitComponents = AppComponents.makeDivKitComponents()
|
||||
|
||||
private(set) var items: [SampleModel]!
|
||||
|
||||
private let divKitComponents = AppComponents.makeDivKitComponents()
|
||||
|
||||
init() {
|
||||
items = TestData.samples
|
||||
.map {
|
||||
|
||||
@@ -5,13 +5,6 @@ enum TestData {
|
||||
static let samplesPath = "samples"
|
||||
static let patchesPath = "regression_test_data/patches"
|
||||
|
||||
private static var cachedRegressionTests: [RegressionTestModel]?
|
||||
|
||||
static var samples: [URL] {
|
||||
getAllFiles(path: samplesPath)
|
||||
.map { url, _ in url }
|
||||
}
|
||||
|
||||
static var regressionTests: [RegressionTestModel] = {
|
||||
let indexFileUrl = Bundle.main
|
||||
.url(forResource: "index", withExtension: "json", subdirectory: regressionPath)!
|
||||
@@ -23,6 +16,13 @@ enum TestData {
|
||||
.sorted { $0.title < $1.title }
|
||||
}()
|
||||
|
||||
private static var cachedRegressionTests: [RegressionTestModel]?
|
||||
|
||||
static var samples: [URL] {
|
||||
getAllFiles(path: samplesPath)
|
||||
.map { url, _ in url }
|
||||
}
|
||||
|
||||
private static func getAllFiles(path: String) -> [(URL, String)] {
|
||||
getItems(path: path, extension: "")
|
||||
.flatMap { _, folderName in
|
||||
|
||||
@@ -18,17 +18,17 @@ enum UserPreferences {
|
||||
|
||||
static let isRTLEnabledDefault: Bool = UIUserInterfaceLayoutDirection
|
||||
.system == .rightToLeft ? true : false
|
||||
static let showRenderingTimeDefault = false
|
||||
static let playgroundThemeDefault = Theme.system
|
||||
|
||||
static var isRTLEnabled: Bool {
|
||||
defaults.value(forKey: isRTLEnabledKey) as? Bool ?? isRTLEnabledDefault
|
||||
}
|
||||
|
||||
static let showRenderingTimeDefault = false
|
||||
static var showRenderingTime: Bool {
|
||||
defaults.value(forKey: showRenderingTimeKey) as? Bool ?? showRenderingTimeDefault
|
||||
}
|
||||
|
||||
static let playgroundThemeDefault = Theme.system
|
||||
|
||||
static var playgroundTheme: Theme {
|
||||
let value = Theme(rawValue: defaults.value(forKey: playgroundThemeKey) as? String ?? "") ??
|
||||
playgroundThemeDefault
|
||||
|
||||
@@ -54,10 +54,6 @@ struct UIStatePayload: Encodable {
|
||||
}
|
||||
|
||||
struct RenderingTime: Encodable {
|
||||
let div_render_total: Time
|
||||
let div_parsing_data: Time
|
||||
let div_parsing_templates: Time
|
||||
|
||||
enum HistogramType: String, Encodable {
|
||||
case cold
|
||||
case warm
|
||||
@@ -67,6 +63,11 @@ struct UIStatePayload: Encodable {
|
||||
let value: Int
|
||||
let histogram_type: HistogramType
|
||||
}
|
||||
|
||||
let div_render_total: Time
|
||||
let div_parsing_data: Time
|
||||
let div_parsing_templates: Time
|
||||
|
||||
}
|
||||
|
||||
let type: String = "ui_state"
|
||||
|
||||
@@ -34,8 +34,9 @@ struct WebPreviewView: View {
|
||||
|
||||
private final class WebPreviewModel {
|
||||
let divKitComponents: DivKitComponents
|
||||
private let socket = WebPreviewSocket()
|
||||
private(set) var debugParams: DebugParams!
|
||||
|
||||
private let socket = WebPreviewSocket()
|
||||
private let payloadFactory: UIStatePayloadFactory
|
||||
private var renderingTime: UIStatePayload.RenderingTime?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
@@ -50,11 +50,6 @@ private final class WebPreviewViewController: DivViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
takeScreenshot(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_: Bool) {
|
||||
isAppeared = true
|
||||
}
|
||||
@@ -66,6 +61,11 @@ private final class WebPreviewViewController: DivViewController {
|
||||
isAppeared = false
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
takeScreenshot(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
override func onViewUpdated() {
|
||||
super.onViewUpdated()
|
||||
takeScreenshot(afterScreenUpdates: true)
|
||||
|
||||
@@ -76,6 +76,10 @@ private class LabelImagePreviewProvider: ViewProvider {
|
||||
return label!
|
||||
}
|
||||
|
||||
func equals(other: ViewProvider) -> Bool {
|
||||
loadView() == other.loadView()
|
||||
}
|
||||
|
||||
private func makeLabel() -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = "Preview"
|
||||
@@ -83,9 +87,6 @@ private class LabelImagePreviewProvider: ViewProvider {
|
||||
return label
|
||||
}
|
||||
|
||||
func equals(other: ViewProvider) -> Bool {
|
||||
loadView() == other.loadView()
|
||||
}
|
||||
}
|
||||
|
||||
private let defaultPagerViewState = [
|
||||
|
||||
@@ -21,11 +21,12 @@ enum ReferenceSet {
|
||||
}
|
||||
|
||||
private struct PlistContents: Decodable {
|
||||
let referenceSnapshotsPath: String
|
||||
let resultSnapshotsPath: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case referenceSnapshotsPath = "REFERENCE_SNAPSHOTS_PATH"
|
||||
case resultSnapshotsPath = "RESULT_SNAPSHOTS_PATH"
|
||||
}
|
||||
|
||||
let referenceSnapshotsPath: String
|
||||
let resultSnapshotsPath: String
|
||||
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ final class SnapshotTestRunner {
|
||||
let mode = TestMode.verify
|
||||
#endif
|
||||
|
||||
private let file: JsonFile
|
||||
|
||||
init(file: JsonFile) {
|
||||
self.file = file
|
||||
}
|
||||
|
||||
private let file: JsonFile
|
||||
|
||||
func run(
|
||||
caseName: String,
|
||||
blocksState: [IdAndCardId: ElementState] = [:],
|
||||
|
||||
@@ -87,16 +87,6 @@ final class CustomFunctionTests: XCTestCase {
|
||||
private lazy var functionsStorage = DivFunctionsStorage(reporter: mockReporter)
|
||||
private lazy var resolver = makeExpressionResolver(path: path)
|
||||
|
||||
private func makeExpressionResolver(path: UIElementPath) -> ExpressionResolver {
|
||||
ExpressionResolver(
|
||||
path: path,
|
||||
variablesStorage: variablesStorage,
|
||||
functionsStorage: functionsStorage,
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
reporter: mockReporter
|
||||
)
|
||||
}
|
||||
|
||||
override func setUp() {
|
||||
variablesStorage.set(cardId: "card_id", variables: variables)
|
||||
}
|
||||
@@ -315,6 +305,17 @@ final class CustomFunctionTests: XCTestCase {
|
||||
|
||||
XCTAssertNil(resolver.resolve("@{array_var.increment(2)}"))
|
||||
}
|
||||
|
||||
private func makeExpressionResolver(path: UIElementPath) -> ExpressionResolver {
|
||||
ExpressionResolver(
|
||||
path: path,
|
||||
variablesStorage: variablesStorage,
|
||||
functionsStorage: functionsStorage,
|
||||
persistentValuesStorage: DivPersistentValuesStorage(),
|
||||
reporter: mockReporter
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private let variables: DivVariables = [
|
||||
|
||||
@@ -66,11 +66,26 @@ private struct TestCases: Decodable {
|
||||
}
|
||||
|
||||
private struct ExpressionTestCase: Decodable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case expression, variables, expected, platforms
|
||||
}
|
||||
|
||||
let expression: String
|
||||
let variables: DivVariables
|
||||
let expected: ExpectedValue
|
||||
let platforms: [Platform]
|
||||
|
||||
var description: String {
|
||||
let formattedExpression = if expression.starts(with: "@{"),
|
||||
expression.last == "}",
|
||||
!expression.dropFirst(2).contains("@{") {
|
||||
String(expression.dropFirst(2).dropLast())
|
||||
} else {
|
||||
expression
|
||||
}
|
||||
return "\(formattedExpression) -> \(expected.description): \(hashValue)"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
expression = try container.decode(String.self, forKey: .expression)
|
||||
@@ -98,21 +113,6 @@ private struct ExpressionTestCase: Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
var description: String {
|
||||
let formattedExpression = if expression.starts(with: "@{"),
|
||||
expression.last == "}",
|
||||
!expression.dropFirst(2).contains("@{") {
|
||||
String(expression.dropFirst(2).dropLast())
|
||||
} else {
|
||||
expression
|
||||
}
|
||||
return "\(formattedExpression) -> \(expected.description): \(hashValue)"
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case expression, variables, expected, platforms
|
||||
}
|
||||
|
||||
func resolveValue(
|
||||
errorTracker: ExpressionErrorTracker? = nil
|
||||
) -> String? {
|
||||
@@ -194,6 +194,37 @@ enum ExpectedValue: Decodable {
|
||||
case unorderedArray(DivArray)
|
||||
case error(String)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, value
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .string(value):
|
||||
formatArgForError(value)
|
||||
case let .double(value):
|
||||
formatArgForError(value)
|
||||
case let .integer(value):
|
||||
formatArgForError(value)
|
||||
case let .bool(value):
|
||||
formatArgForError(value)
|
||||
case let .color(value):
|
||||
formatArgForError(value)
|
||||
case let .datetime(value):
|
||||
formatArgForError(value)
|
||||
case let .url(value):
|
||||
formatArgForError(value)
|
||||
case let .array(value):
|
||||
formatArgForError(value)
|
||||
case let .dict(value):
|
||||
formatArgForError(value)
|
||||
case let .unorderedArray(value):
|
||||
formatArgForError(value)
|
||||
case .error:
|
||||
"error"
|
||||
}
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
@@ -254,36 +285,6 @@ enum ExpectedValue: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .string(value):
|
||||
formatArgForError(value)
|
||||
case let .double(value):
|
||||
formatArgForError(value)
|
||||
case let .integer(value):
|
||||
formatArgForError(value)
|
||||
case let .bool(value):
|
||||
formatArgForError(value)
|
||||
case let .color(value):
|
||||
formatArgForError(value)
|
||||
case let .datetime(value):
|
||||
formatArgForError(value)
|
||||
case let .url(value):
|
||||
formatArgForError(value)
|
||||
case let .array(value):
|
||||
formatArgForError(value)
|
||||
case let .dict(value):
|
||||
formatArgForError(value)
|
||||
case let .unorderedArray(value):
|
||||
formatArgForError(value)
|
||||
case .error:
|
||||
"error"
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, value
|
||||
}
|
||||
}
|
||||
|
||||
extension DivVariable: Swift.Decodable {
|
||||
|
||||
@@ -48,21 +48,20 @@ private struct TestCases: Decodable {
|
||||
}
|
||||
|
||||
private struct SignatureTestCase: Decodable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case functionName = "function_name"
|
||||
case isMethod = "is_method"
|
||||
case arguments
|
||||
case resultType = "result_type"
|
||||
case platforms
|
||||
}
|
||||
|
||||
let functionName: String
|
||||
let isMethod: Bool
|
||||
let arguments: [ArgumentSignature]
|
||||
let resultType: Any.Type
|
||||
let platforms: [Platform]
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
functionName = try container.decode(String.self, forKey: .functionName)
|
||||
isMethod = (try? container.decode(Bool.self, forKey: .isMethod)) ?? false
|
||||
arguments = (try? container.decode([ArgumentSignature].self, forKey: .arguments)) ?? []
|
||||
resultType = try parseType(container.decode(String.self, forKey: .resultType))
|
||||
platforms = try container.decode([Platform].self, forKey: .platforms)
|
||||
}
|
||||
|
||||
var toSignature: FunctionSignature {
|
||||
FunctionSignature(arguments: arguments, resultType: resultType)
|
||||
}
|
||||
@@ -82,13 +81,15 @@ private struct SignatureTestCase: Decodable {
|
||||
return name
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case functionName = "function_name"
|
||||
case isMethod = "is_method"
|
||||
case arguments
|
||||
case resultType = "result_type"
|
||||
case platforms
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
functionName = try container.decode(String.self, forKey: .functionName)
|
||||
isMethod = (try? container.decode(Bool.self, forKey: .isMethod)) ?? false
|
||||
arguments = (try? container.decode([ArgumentSignature].self, forKey: .arguments)) ?? []
|
||||
resultType = try parseType(container.decode(String.self, forKey: .resultType))
|
||||
platforms = try container.decode([Platform].self, forKey: .platforms)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Function {
|
||||
|
||||
@@ -37,6 +37,10 @@ final class VisibilityTester {
|
||||
let timer: TestTimerScheduler
|
||||
let performer = UIActionEventPerformerMock()
|
||||
|
||||
var callsCount: Int {
|
||||
performer.callCount
|
||||
}
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
timer: TestTimerScheduler
|
||||
@@ -46,10 +50,6 @@ final class VisibilityTester {
|
||||
performer.addSubview(view)
|
||||
}
|
||||
|
||||
var callsCount: Int {
|
||||
performer.callCount
|
||||
}
|
||||
|
||||
func setViewAppear() {
|
||||
updateViewVisibility(isVisible: true)
|
||||
}
|
||||
|
||||
@@ -123,6 +123,10 @@ private struct IntegrationTestData {
|
||||
}
|
||||
|
||||
private struct IntegrationTest: Decodable, @unchecked Sendable {
|
||||
let description: String
|
||||
let divData: DivData
|
||||
let cases: [IntegrationTestCase]
|
||||
|
||||
init(_ data: Data) throws {
|
||||
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
|
||||
let casesJson = json["cases"]!
|
||||
@@ -138,25 +142,27 @@ private struct IntegrationTest: Decodable, @unchecked Sendable {
|
||||
)
|
||||
}
|
||||
|
||||
let description: String
|
||||
let divData: DivData
|
||||
let cases: [IntegrationTestCase]
|
||||
}
|
||||
|
||||
private struct IntegrationTestCase: Decodable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case divActions = "div_actions", expected, platforms
|
||||
}
|
||||
|
||||
let divActions: [DivAction]?
|
||||
let expected: [Expected]
|
||||
let platforms: [Platform]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case divActions = "div_actions", expected, platforms
|
||||
}
|
||||
}
|
||||
|
||||
private enum Expected: Decodable {
|
||||
case variable(String, ExpectedValue)
|
||||
case error(String)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case variableName = "variable_name", type, value
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
@@ -179,9 +185,6 @@ private enum Expected: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case variableName = "variable_name", type, value
|
||||
}
|
||||
}
|
||||
|
||||
extension DivAction: Swift.Decodable {
|
||||
|
||||
@@ -2,16 +2,6 @@
|
||||
import XCTest
|
||||
|
||||
final class TemplateToTypeTests: XCTestCase {
|
||||
private func performTestCase(
|
||||
json: String,
|
||||
templateToType: [TemplateName: String]
|
||||
) throws {
|
||||
let dict = try JSONSerialization.jsonObject(
|
||||
with: json.data(using: .utf8)!
|
||||
) as! [String: Any]
|
||||
XCTAssertEqual(calculateTemplateToType(in: dict), templateToType)
|
||||
}
|
||||
|
||||
func test_IndependentTypes() throws {
|
||||
let json = """
|
||||
{
|
||||
@@ -101,4 +91,15 @@ final class TemplateToTypeTests: XCTestCase {
|
||||
]
|
||||
try performTestCase(json: json, templateToType: templateToType)
|
||||
}
|
||||
|
||||
private func performTestCase(
|
||||
json: String,
|
||||
templateToType: [TemplateName: String]
|
||||
) throws {
|
||||
let dict = try JSONSerialization.jsonObject(
|
||||
with: json.data(using: .utf8)!
|
||||
) as! [String: Any]
|
||||
XCTAssertEqual(calculateTemplateToType(in: dict), templateToType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import XCTest
|
||||
|
||||
public final class TestTimer: TimerType {
|
||||
public let timeInterval: TimeInterval
|
||||
public var fireDate: Date
|
||||
public private(set) var isValid = true
|
||||
|
||||
private let block: () -> Void
|
||||
private let repeating: Bool
|
||||
public var fireDate: Date
|
||||
|
||||
public init(
|
||||
fireDate: Date = .distantFuture,
|
||||
@@ -21,8 +23,6 @@ public final class TestTimer: TimerType {
|
||||
self.repeating = repeating
|
||||
}
|
||||
|
||||
public private(set) var isValid = true
|
||||
|
||||
public func fire() {
|
||||
guard isValid else {
|
||||
XCTFail("Timer is not valid.")
|
||||
|
||||
@@ -4,6 +4,72 @@ import VGSL
|
||||
import XCTest
|
||||
|
||||
final class DivViewSizeTests: XCTestCase {
|
||||
private final class TestBlock: BlockWithTraits {
|
||||
var widthTrait: LayoutKit.LayoutTrait
|
||||
|
||||
var heightTrait: LayoutKit.LayoutTrait
|
||||
|
||||
var intrinsicContentWidth: CGFloat
|
||||
|
||||
var debugDescription: String = ""
|
||||
|
||||
init(
|
||||
widthTrait: LayoutTrait,
|
||||
heightTrait: LayoutTrait,
|
||||
intrinsicContentWidth: CGFloat
|
||||
) {
|
||||
self.widthTrait = widthTrait
|
||||
self.heightTrait = heightTrait
|
||||
self.intrinsicContentWidth = intrinsicContentWidth
|
||||
}
|
||||
|
||||
static func makeBlockView() -> LayoutKit.BlockView {
|
||||
TestView()
|
||||
}
|
||||
|
||||
func configureBlockView(
|
||||
_: LayoutKit.BlockView,
|
||||
observer _: LayoutKit.ElementStateObserver?,
|
||||
overscrollDelegate _: VGSLUI.ScrollDelegate?,
|
||||
renderingDelegate _: LayoutKit.RenderingDelegate?
|
||||
) {}
|
||||
|
||||
func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat {
|
||||
0.0
|
||||
}
|
||||
|
||||
func equals(_: LayoutKit.Block) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func canConfigureBlockView(_: LayoutKit.BlockView) -> Bool { false }
|
||||
|
||||
func getImageHolders() -> [VGSLUI.ImageHolder] {
|
||||
[]
|
||||
}
|
||||
|
||||
func laidOut(for _: CGFloat) -> LayoutKit.Block {
|
||||
self
|
||||
}
|
||||
|
||||
func laidOut(for _: CGSize) -> LayoutKit.Block {
|
||||
self
|
||||
}
|
||||
|
||||
func updated(withStates _: LayoutKit.BlocksState) throws -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestView: BlockView {
|
||||
var block: TestBlock?
|
||||
|
||||
var effectiveBackgroundColor: UIColor? { backgroundColor }
|
||||
|
||||
func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {}
|
||||
|
||||
}
|
||||
|
||||
func test_sizeForParentViewSizeForWeightedBlock() {
|
||||
let weightedBlock = TestBlock(
|
||||
widthTrait: LayoutTrait.weighted(.default),
|
||||
@@ -60,67 +126,4 @@ final class DivViewSizeTests: XCTestCase {
|
||||
XCTAssertTrue(size == CGSize(width: 500.0, height: 0.0))
|
||||
}
|
||||
|
||||
private final class TestBlock: BlockWithTraits {
|
||||
init(
|
||||
widthTrait: LayoutTrait,
|
||||
heightTrait: LayoutTrait,
|
||||
intrinsicContentWidth: CGFloat
|
||||
) {
|
||||
self.widthTrait = widthTrait
|
||||
self.heightTrait = heightTrait
|
||||
self.intrinsicContentWidth = intrinsicContentWidth
|
||||
}
|
||||
|
||||
func configureBlockView(
|
||||
_: LayoutKit.BlockView,
|
||||
observer _: LayoutKit.ElementStateObserver?,
|
||||
overscrollDelegate _: VGSLUI.ScrollDelegate?,
|
||||
renderingDelegate _: LayoutKit.RenderingDelegate?
|
||||
) {}
|
||||
|
||||
var widthTrait: LayoutKit.LayoutTrait
|
||||
|
||||
var heightTrait: LayoutKit.LayoutTrait
|
||||
|
||||
var intrinsicContentWidth: CGFloat
|
||||
|
||||
func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat {
|
||||
0.0
|
||||
}
|
||||
|
||||
func equals(_: LayoutKit.Block) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
var debugDescription: String = ""
|
||||
|
||||
static func makeBlockView() -> LayoutKit.BlockView {
|
||||
TestView()
|
||||
}
|
||||
|
||||
func canConfigureBlockView(_: LayoutKit.BlockView) -> Bool { false }
|
||||
|
||||
func getImageHolders() -> [VGSLUI.ImageHolder] {
|
||||
[]
|
||||
}
|
||||
|
||||
func laidOut(for _: CGFloat) -> LayoutKit.Block {
|
||||
self
|
||||
}
|
||||
|
||||
func laidOut(for _: CGSize) -> LayoutKit.Block {
|
||||
self
|
||||
}
|
||||
|
||||
func updated(withStates _: LayoutKit.BlocksState) throws -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
private final class TestView: BlockView {
|
||||
func onVisibleBoundsChanged(from _: CGRect, to _: CGRect) {}
|
||||
|
||||
var block: TestBlock?
|
||||
var effectiveBackgroundColor: UIColor? { backgroundColor }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ public final class FakeImageHolderFactory: DivImageHolderFactory {
|
||||
}
|
||||
|
||||
public final class FakeImageHolder: ImageHolder {
|
||||
public init() {}
|
||||
|
||||
public var image: Image? {
|
||||
nil
|
||||
}
|
||||
@@ -21,6 +19,12 @@ public final class FakeImageHolder: ImageHolder {
|
||||
nil
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"FakeImageHolder"
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
public func requestImageWithCompletion(_: @escaping @MainActor (Image?) -> Void) -> Cancellable? {
|
||||
nil
|
||||
}
|
||||
@@ -33,7 +37,4 @@ public final class FakeImageHolder: ImageHolder {
|
||||
true
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"FakeImageHolder"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import XCTest
|
||||
|
||||
final class TooltipsUITests: XCTestCase {
|
||||
private let app = XCUIApplication()
|
||||
private var elementsQuery: XCUIElementTypeQueryProvider {
|
||||
app.scrollViews.otherElements
|
||||
private enum TooltipTest {
|
||||
case tooltipPosition, closeOnSwitchOrientation
|
||||
}
|
||||
|
||||
private enum Position: CaseIterable {
|
||||
case bottom, top, right
|
||||
}
|
||||
|
||||
private let app = XCUIApplication()
|
||||
private lazy var baseDivView: XCUIElement = {
|
||||
let mainWindow = app.windows.element(boundBy: 0)
|
||||
return mainWindow.descendants(matching: .any).matching(identifier: "baseDivView").element
|
||||
@@ -18,6 +22,10 @@ final class TooltipsUITests: XCTestCase {
|
||||
|
||||
private lazy var closeTooltipButton: XCUIElement = app.buttons["close tooltip"]
|
||||
|
||||
private var elementsQuery: XCUIElementTypeQueryProvider {
|
||||
app.scrollViews.otherElements
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
try super.setUpWithError()
|
||||
|
||||
@@ -85,11 +93,4 @@ final class TooltipsUITests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private enum TooltipTest {
|
||||
case tooltipPosition, closeOnSwitchOrientation
|
||||
}
|
||||
|
||||
private enum Position: CaseIterable {
|
||||
case bottom, top, right
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,6 @@ import VGSL
|
||||
public struct UIElementPath: CustomStringConvertible, ExpressibleByStringLiteral, Codable {
|
||||
private let address: ListNode
|
||||
|
||||
private init(address: ListNode) {
|
||||
self.address = address
|
||||
}
|
||||
|
||||
public init(_ root: String) {
|
||||
address = ListNode(value: root)
|
||||
}
|
||||
|
||||
public init(parent: UIElementPath, child: String) {
|
||||
address = ListNode(value: child, next: parent.address)
|
||||
}
|
||||
|
||||
public init(stringLiteral value: StringLiteralType) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
address.joined(separator: "/")
|
||||
}
|
||||
@@ -39,11 +23,20 @@ public struct UIElementPath: CustomStringConvertible, ExpressibleByStringLiteral
|
||||
address.value
|
||||
}
|
||||
|
||||
public func starts(with path: UIElementPath) -> Bool {
|
||||
if path == self {
|
||||
return true
|
||||
}
|
||||
return parent?.starts(with: path) == true
|
||||
public init(_ root: String) {
|
||||
address = ListNode(value: root)
|
||||
}
|
||||
|
||||
public init(parent: UIElementPath, child: String) {
|
||||
address = ListNode(value: child, next: parent.address)
|
||||
}
|
||||
|
||||
public init(stringLiteral value: StringLiteralType) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
private init(address: ListNode) {
|
||||
self.address = address
|
||||
}
|
||||
|
||||
public static func parse(_ path: String) -> UIElementPath {
|
||||
@@ -53,6 +46,14 @@ public struct UIElementPath: CustomStringConvertible, ExpressibleByStringLiteral
|
||||
}
|
||||
return UIElementPath(path)
|
||||
}
|
||||
|
||||
public func starts(with path: UIElementPath) -> Bool {
|
||||
if path == self {
|
||||
return true
|
||||
}
|
||||
return parent?.starts(with: path) == true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UIElementPath: Hashable {
|
||||
@@ -102,8 +103,13 @@ extension UIElementPath {
|
||||
}
|
||||
|
||||
private final class ListNode: Codable {
|
||||
enum CodingKeys: CodingKey {
|
||||
case value, next
|
||||
}
|
||||
|
||||
let value: String
|
||||
let next: ListNode?
|
||||
|
||||
private var _root: String?
|
||||
|
||||
private lazy var cachedHash: Int = {
|
||||
@@ -114,11 +120,6 @@ private final class ListNode: Codable {
|
||||
return hasher.finalize()
|
||||
}()
|
||||
|
||||
init(value: String, next: ListNode? = nil) {
|
||||
self.value = value
|
||||
self.next = next
|
||||
}
|
||||
|
||||
var root: String {
|
||||
if let _root {
|
||||
return _root
|
||||
@@ -131,9 +132,11 @@ private final class ListNode: Codable {
|
||||
return result
|
||||
}
|
||||
|
||||
enum CodingKeys: CodingKey {
|
||||
case value, next
|
||||
init(value: String, next: ListNode? = nil) {
|
||||
self.value = value
|
||||
self.next = next
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ListNode: Hashable {
|
||||
|
||||
@@ -10,12 +10,13 @@ public class MaskedInputViewModel {
|
||||
case clearRange(range: Range<String.Index>)
|
||||
}
|
||||
|
||||
@ObservableProperty private var rawCursorPosition: CursorData?
|
||||
@ObservableVariable var text: String
|
||||
@ObservableVariable var cursorPosition: NSRange?
|
||||
@ObservableProperty var rawText: String
|
||||
@ObservableProperty var maskValidator: MaskValidator
|
||||
@ObservableProperty var typo: Typo?
|
||||
|
||||
@ObservableProperty private var rawCursorPosition: CursorData?
|
||||
private let disposePool = AutodisposePool()
|
||||
|
||||
init(
|
||||
|
||||
@@ -14,6 +14,10 @@ public final class MaskValidator: Equatable {
|
||||
self.formatter = formatter
|
||||
}
|
||||
|
||||
public static func ==(lhs: MaskValidator, rhs: MaskValidator) -> Bool {
|
||||
lhs.formatter.equals(rhs.formatter)
|
||||
}
|
||||
|
||||
public func formatted(rawText: String, rawCursorPosition: CursorData? = nil) -> InputData {
|
||||
formatter.formatted(rawText: rawText, rawCursorPosition: rawCursorPosition)
|
||||
}
|
||||
@@ -80,9 +84,6 @@ public final class MaskValidator: Equatable {
|
||||
)
|
||||
}
|
||||
|
||||
public static func ==(lhs: MaskValidator, rhs: MaskValidator) -> Bool {
|
||||
lhs.formatter.equals(rhs.formatter)
|
||||
}
|
||||
}
|
||||
|
||||
public enum CursorPositionTag {}
|
||||
@@ -102,6 +103,7 @@ public struct InputData {
|
||||
public let text: String
|
||||
public let cursorPosition: CursorPosition?
|
||||
public var rawData: [RawCharacter]
|
||||
|
||||
public var rawText: String {
|
||||
String(rawData.map(\.char))
|
||||
}
|
||||
|
||||
@@ -62,6 +62,13 @@ public final class PhoneMaskFormatter: MaskFormatter {
|
||||
return InputData(text: textString, cursorPosition: newCursorPosition, rawData: rawData)
|
||||
}
|
||||
|
||||
public func equals(_ other: MaskFormatter) -> Bool {
|
||||
guard let other = other as? PhoneMaskFormatter else {
|
||||
return false
|
||||
}
|
||||
return self.masksByCountryCode == other.masksByCountryCode
|
||||
}
|
||||
|
||||
private func findMask(for rawText: String) -> String {
|
||||
guard !rawText.isEmpty else {
|
||||
return ""
|
||||
@@ -95,12 +102,6 @@ public final class PhoneMaskFormatter: MaskFormatter {
|
||||
return resultMask + extraSymbols
|
||||
}
|
||||
|
||||
public func equals(_ other: MaskFormatter) -> Bool {
|
||||
guard let other = other as? PhoneMaskFormatter else {
|
||||
return false
|
||||
}
|
||||
return self.masksByCountryCode == other.masksByCountryCode
|
||||
}
|
||||
}
|
||||
|
||||
extension Character {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
public final class TextInputValidator {
|
||||
private let allowEmpty: Bool
|
||||
private let validator: (String) -> Bool
|
||||
|
||||
public let message: () -> String?
|
||||
public var isValid: Binding<Bool>
|
||||
|
||||
private let allowEmpty: Bool
|
||||
private let validator: (String) -> Bool
|
||||
|
||||
public init(
|
||||
isValid: Binding<Bool>,
|
||||
allowEmpty: Bool,
|
||||
|
||||
@@ -2,9 +2,10 @@ import Foundation
|
||||
import VGSL
|
||||
|
||||
public struct Binding<T: Equatable>: Equatable {
|
||||
private let name: String
|
||||
@Property public var value: T
|
||||
|
||||
private let name: String
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
value: Property<T>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
public struct ActionAnimation: Equatable {
|
||||
public static let empty = Self(touchDown: [.empty], touchUp: [.empty])
|
||||
|
||||
public let touchDown: [TransitioningAnimation]
|
||||
public let touchUp: [TransitioningAnimation]
|
||||
|
||||
public static let empty = Self(touchDown: [.empty], touchUp: [.empty])
|
||||
|
||||
public init(
|
||||
touchDown: [TransitioningAnimation],
|
||||
touchUp: [TransitioningAnimation]
|
||||
|
||||
@@ -44,16 +44,41 @@ private final class AnchorView: BlockView, VisibleBoundsTrackingContainer {
|
||||
private var preventLayout = false
|
||||
private weak var observer: ElementStateObserver?
|
||||
|
||||
private var block: AnchorBlock! {
|
||||
modelAndLastLayoutSize.model?.block
|
||||
}
|
||||
|
||||
var visibleBoundsTrackingSubviews: [VisibleBoundsTrackingView] {
|
||||
[leadingView, centerView, trailingView].compactMap { $0 }
|
||||
}
|
||||
|
||||
var effectiveBackgroundColor: UIColor? { backgroundColor }
|
||||
|
||||
private var block: AnchorBlock! {
|
||||
modelAndLastLayoutSize.model?.block
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
guard !preventLayout else { return }
|
||||
|
||||
super.layoutSubviews()
|
||||
|
||||
if let lastLayoutSize = modelAndLastLayoutSize.lastLayoutSize, bounds.size == lastLayoutSize {
|
||||
return
|
||||
}
|
||||
|
||||
guard let model = modelAndLastLayoutSize.model else {
|
||||
return
|
||||
}
|
||||
|
||||
let layout = model.layout ?? model.block.makeLayout(for: bounds.size)
|
||||
|
||||
leadingView?.frame = layout.leadingFrame
|
||||
centerView?.frame = layout.centerFrame
|
||||
trailingView?.frame = layout.trailingFrame
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result === self ? nil : result
|
||||
}
|
||||
|
||||
func configure(
|
||||
model: Model,
|
||||
observer: ElementStateObserver?,
|
||||
@@ -93,29 +118,5 @@ private final class AnchorView: BlockView, VisibleBoundsTrackingContainer {
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
guard !preventLayout else { return }
|
||||
|
||||
super.layoutSubviews()
|
||||
|
||||
if let lastLayoutSize = modelAndLastLayoutSize.lastLayoutSize, bounds.size == lastLayoutSize {
|
||||
return
|
||||
}
|
||||
|
||||
guard let model = modelAndLastLayoutSize.model else {
|
||||
return
|
||||
}
|
||||
|
||||
let layout = model.layout ?? model.block.makeLayout(for: bounds.size)
|
||||
|
||||
leadingView?.frame = layout.leadingFrame
|
||||
centerView?.frame = layout.centerFrame
|
||||
trailingView?.frame = layout.trailingFrame
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result === self ? nil : result
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -16,33 +16,32 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits {
|
||||
public let center: Block
|
||||
public let trailing: Block?
|
||||
|
||||
private var contents: [Block] {
|
||||
[leading, center, trailing].compactMap { $0 }
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
if case let .fixed(value) = widthTrait {
|
||||
return value
|
||||
}
|
||||
|
||||
let widths = contents.map(\.intrinsicContentWidth)
|
||||
var result: CGFloat = switch direction {
|
||||
case .horizontal:
|
||||
widths.reduce(0, +)
|
||||
case .vertical:
|
||||
widths.max()!
|
||||
}
|
||||
|
||||
if case let .intrinsic(_, minSize, maxSize) = widthTrait {
|
||||
result = clamp(result, min: minSize, max: maxSize)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private init(
|
||||
direction: ContainerBlock.LayoutDirection,
|
||||
widthTrait: LayoutTrait,
|
||||
heightTrait: LayoutTrait,
|
||||
crossAlignment: Alignment,
|
||||
leading: Block?,
|
||||
center: Block,
|
||||
trailing: Block?
|
||||
) {
|
||||
self.direction = direction
|
||||
self.widthTrait = widthTrait
|
||||
self.heightTrait = heightTrait
|
||||
self.crossAlignment = crossAlignment
|
||||
self.leading = leading
|
||||
self.center = center
|
||||
self.trailing = trailing
|
||||
public var debugDescription: String {
|
||||
"Anchored"
|
||||
}
|
||||
|
||||
for content in contents {
|
||||
switch direction {
|
||||
case .vertical: precondition(!content.isVerticallyResizable)
|
||||
case .horizontal: precondition(!content.isHorizontallyResizable)
|
||||
}
|
||||
}
|
||||
private var contents: [Block] {
|
||||
[leading, center, trailing].compactMap { $0 }
|
||||
}
|
||||
|
||||
public convenience init(
|
||||
@@ -76,24 +75,29 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits {
|
||||
)
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
if case let .fixed(value) = widthTrait {
|
||||
return value
|
||||
}
|
||||
private init(
|
||||
direction: ContainerBlock.LayoutDirection,
|
||||
widthTrait: LayoutTrait,
|
||||
heightTrait: LayoutTrait,
|
||||
crossAlignment: Alignment,
|
||||
leading: Block?,
|
||||
center: Block,
|
||||
trailing: Block?
|
||||
) {
|
||||
self.direction = direction
|
||||
self.widthTrait = widthTrait
|
||||
self.heightTrait = heightTrait
|
||||
self.crossAlignment = crossAlignment
|
||||
self.leading = leading
|
||||
self.center = center
|
||||
self.trailing = trailing
|
||||
|
||||
let widths = contents.map(\.intrinsicContentWidth)
|
||||
var result: CGFloat = switch direction {
|
||||
case .horizontal:
|
||||
widths.reduce(0, +)
|
||||
case .vertical:
|
||||
widths.max()!
|
||||
for content in contents {
|
||||
switch direction {
|
||||
case .vertical: precondition(!content.isVerticallyResizable)
|
||||
case .horizontal: precondition(!content.isHorizontallyResizable)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .intrinsic(_, minSize, maxSize) = widthTrait {
|
||||
result = clamp(result, min: minSize, max: maxSize)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat {
|
||||
@@ -118,17 +122,6 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits {
|
||||
return result
|
||||
}
|
||||
|
||||
func makeLayout(for size: CGSize) -> Layout {
|
||||
Layout(
|
||||
size: size,
|
||||
direction: direction,
|
||||
crossAlignment: crossAlignment,
|
||||
leading: leading,
|
||||
center: center,
|
||||
trailing: trailing
|
||||
)
|
||||
}
|
||||
|
||||
public func laidOutHierarchy(for size: CGSize) -> (AnchorBlock, Layout) {
|
||||
let layout = makeLayout(for: size)
|
||||
let laidOutSelf = AnchorBlock(
|
||||
@@ -155,10 +148,6 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits {
|
||||
&& trailing == other.trailing
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"Anchored"
|
||||
}
|
||||
|
||||
public func getImageHolders() -> [ImageHolder] {
|
||||
contents.flatMap { $0.getImageHolders() }
|
||||
}
|
||||
@@ -186,4 +175,16 @@ public final class AnchorBlock: BlockWithLayout, BlockWithTraits {
|
||||
trailing: trailing?.updated(path: path, isFocused: isFocused)
|
||||
)
|
||||
}
|
||||
|
||||
func makeLayout(for size: CGSize) -> Layout {
|
||||
Layout(
|
||||
size: size,
|
||||
direction: direction,
|
||||
crossAlignment: crossAlignment,
|
||||
leading: leading,
|
||||
center: center,
|
||||
trailing: trailing
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import CoreGraphics
|
||||
|
||||
public struct AnchorPoint: Equatable {
|
||||
let x, y: AnchorValue
|
||||
|
||||
public init(x: AnchorValue, y: AnchorValue) {
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
@@ -6,14 +6,21 @@ public final class AspectBlock<Content: Block>: WrapperBlock {
|
||||
|
||||
public var child: Block { content }
|
||||
|
||||
public var isVerticallyResizable: Bool { false }
|
||||
|
||||
public var debugDescription: String {
|
||||
"""
|
||||
Aspect \(aspectRatio):
|
||||
\(child)
|
||||
"""
|
||||
}
|
||||
|
||||
public init(content: Content, aspectRatio: CGFloat) {
|
||||
assert(content.isVerticallyResizable)
|
||||
self.content = content
|
||||
self.aspectRatio = aspectRatio
|
||||
}
|
||||
|
||||
public var isVerticallyResizable: Bool { false }
|
||||
|
||||
public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat {
|
||||
width * aspectRatio
|
||||
}
|
||||
@@ -31,13 +38,6 @@ public final class AspectBlock<Content: Block>: WrapperBlock {
|
||||
return child == other.child && aspectRatio == other.aspectRatio
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
"""
|
||||
Aspect \(aspectRatio):
|
||||
\(child)
|
||||
"""
|
||||
}
|
||||
|
||||
public func laidOut(for width: CGFloat) -> Block {
|
||||
makeCopy(wrapping: child.laidOut(for: width))
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ public final class BackgroundBlock: BlockWithLayout, WrapperBlock {
|
||||
self.cornerRadius = cornerRadius
|
||||
}
|
||||
|
||||
public static func ==(lhs: BackgroundBlock, rhs: BackgroundBlock) -> Bool {
|
||||
lhs.background == rhs.background &&
|
||||
lhs.child == rhs.child &&
|
||||
lhs.cornerRadius == rhs.cornerRadius
|
||||
}
|
||||
|
||||
public func equals(_ other: Block) -> Bool {
|
||||
guard let other = other as? BackgroundBlock else {
|
||||
return false
|
||||
@@ -35,12 +41,6 @@ public final class BackgroundBlock: BlockWithLayout, WrapperBlock {
|
||||
)
|
||||
}
|
||||
|
||||
public static func ==(lhs: BackgroundBlock, rhs: BackgroundBlock) -> Bool {
|
||||
lhs.background == rhs.background &&
|
||||
lhs.child == rhs.child &&
|
||||
lhs.cornerRadius == rhs.cornerRadius
|
||||
}
|
||||
|
||||
func laidOutHierarchy(for size: CGSize) -> (BackgroundBlock, Layout) {
|
||||
let laidOutChild = child.laidOut(for: size)
|
||||
let block = BackgroundBlock(
|
||||
|
||||
@@ -12,6 +12,23 @@ public func ==(lhs: CATransform3D, rhs: CATransform3D) -> Bool {
|
||||
}
|
||||
|
||||
public struct BlockAnimation: Equatable {
|
||||
public struct KeyTime: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, Equatable {
|
||||
public let value: Double
|
||||
|
||||
public init(_ value: Double) {
|
||||
precondition(value >= 0 && value <= 1.0, "KeyTime value must be in [0; 1]")
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public init(floatLiteral value: Double) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(integerLiteral value: IntegerLiteralType) {
|
||||
self.init(Double(value))
|
||||
}
|
||||
}
|
||||
|
||||
public let changes: AnimationChanges
|
||||
public let keyTimes: [KeyTime]
|
||||
public let duration: TimeInterval
|
||||
@@ -38,22 +55,6 @@ public struct BlockAnimation: Equatable {
|
||||
self.timingFunction = timingFunction
|
||||
}
|
||||
|
||||
public struct KeyTime: ExpressibleByFloatLiteral, ExpressibleByIntegerLiteral, Equatable {
|
||||
public let value: Double
|
||||
|
||||
public init(_ value: Double) {
|
||||
precondition(value >= 0 && value <= 1.0, "KeyTime value must be in [0; 1]")
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public init(floatLiteral value: Double) {
|
||||
self.init(value)
|
||||
}
|
||||
|
||||
public init(integerLiteral value: IntegerLiteralType) {
|
||||
self.init(Double(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension BlockAnimation {
|
||||
|
||||
@@ -2,8 +2,6 @@ import CoreGraphics
|
||||
import VGSL
|
||||
|
||||
public struct BlockAlignment2D: Equatable {
|
||||
public let horizontal: Alignment
|
||||
public let vertical: Alignment
|
||||
public static let `default` = BlockAlignment2D.topLeft
|
||||
public static let topLeft = BlockAlignment2D(horizontal: .leading, vertical: .leading)
|
||||
public static let topCenter = BlockAlignment2D(horizontal: .center, vertical: .leading)
|
||||
@@ -15,6 +13,9 @@ public struct BlockAlignment2D: Equatable {
|
||||
public static let bottomCenter = BlockAlignment2D(horizontal: .center, vertical: .trailing)
|
||||
public static let bottomRight = BlockAlignment2D(horizontal: .trailing, vertical: .trailing)
|
||||
|
||||
public let horizontal: Alignment
|
||||
public let vertical: Alignment
|
||||
|
||||
public init(
|
||||
horizontal: Alignment = .leading,
|
||||
vertical: Alignment = .leading
|
||||
|
||||
@@ -34,6 +34,10 @@ public struct BlockTooltip: Equatable {
|
||||
public let useLegacyWidth: Bool
|
||||
public let tooltipViewFactory: TooltipViewFactory?
|
||||
|
||||
public var id: String {
|
||||
params.id
|
||||
}
|
||||
|
||||
public init(
|
||||
block: Block,
|
||||
params: BlockTooltipParams,
|
||||
@@ -50,10 +54,6 @@ public struct BlockTooltip: Equatable {
|
||||
self.params = params
|
||||
}
|
||||
|
||||
public var id: String {
|
||||
params.id
|
||||
}
|
||||
|
||||
public static func ==(lhs: BlockTooltip, rhs: BlockTooltip) -> Bool {
|
||||
lhs.params == rhs.params &&
|
||||
lhs.offset == rhs.offset &&
|
||||
|
||||
@@ -5,11 +5,12 @@ public struct BlockTooltipParams: Equatable {
|
||||
public let mode: BlockTooltip.Mode
|
||||
public let duration: TimeInterval
|
||||
public let closeByTapOutside: Bool
|
||||
let tapOutsideActions: [UserInterfaceAction]
|
||||
public let backgroundAccessibilityDescription: String?
|
||||
public let animationIn: [TransitioningAnimation]?
|
||||
public let animationOut: [TransitioningAnimation]?
|
||||
|
||||
let tapOutsideActions: [UserInterfaceAction]
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
mode: BlockTooltip.Mode,
|
||||
|
||||
@@ -9,12 +9,6 @@ import AppKit
|
||||
#endif
|
||||
|
||||
public final class ContainerBlock: BlockWithLayout {
|
||||
typealias Layout = ContainerBlockLayout
|
||||
public static let defaultAnchorPoint = AnchorPoint(
|
||||
x: .relative(value: 50),
|
||||
y: .relative(value: 50)
|
||||
)
|
||||
|
||||
/// Determines direction in which child blocks are laid out in a container
|
||||
@frozen
|
||||
public enum LayoutDirection: CaseIterable {
|
||||
@@ -82,12 +76,19 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
}
|
||||
}
|
||||
|
||||
typealias Layout = ContainerBlockLayout
|
||||
|
||||
private struct CachedSizes {
|
||||
var intrinsicWidth: CGFloat?
|
||||
var intrinsicHeight: (width: CGFloat, height: CGFloat)?
|
||||
var nonResizableSize: (width: CGFloat, height: CGFloat?)?
|
||||
}
|
||||
|
||||
public static let defaultAnchorPoint = AnchorPoint(
|
||||
x: .relative(value: 50),
|
||||
y: .relative(value: 50)
|
||||
)
|
||||
|
||||
public let blockLayoutDirection: UserInterfaceLayoutDirection
|
||||
public let layoutDirection: LayoutDirection
|
||||
public let layoutMode: LayoutMode
|
||||
@@ -108,6 +109,98 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
|
||||
private var cached = CachedSizes()
|
||||
|
||||
public var isVerticallyResizable: Bool { heightTrait.isResizable }
|
||||
public var isHorizontallyResizable: Bool { widthTrait.isResizable }
|
||||
|
||||
public var calculateWidthFirst: Bool {
|
||||
switch widthTrait {
|
||||
case .fixed, .weighted:
|
||||
true
|
||||
case .intrinsic:
|
||||
!(layoutDirection == .vertical && layoutMode == .wrap)
|
||||
}
|
||||
}
|
||||
|
||||
public var isVerticallyConstrained: Bool { heightTrait.isConstrained }
|
||||
public var isHorizontallyConstrained: Bool { widthTrait.isConstrained }
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
if case let .fixed(width) = widthTrait {
|
||||
return width
|
||||
}
|
||||
|
||||
if let cached = cached.intrinsicWidth {
|
||||
return cached
|
||||
}
|
||||
|
||||
var result: CGFloat = switch layoutDirection {
|
||||
case .horizontal:
|
||||
(children.map(\.content.intrinsicContentWidth) + gaps).reduce(0, +)
|
||||
case .vertical:
|
||||
children.map(\.content.intrinsicContentWidth).max() ?? 0
|
||||
}
|
||||
|
||||
if case let .intrinsic(_, minSize, maxSize) = widthTrait {
|
||||
result = clamp(result, min: minSize, max: maxSize)
|
||||
}
|
||||
|
||||
cached.intrinsicWidth = result
|
||||
return result
|
||||
}
|
||||
|
||||
public var widthOfHorizontallyNonResizableBlock: CGFloat {
|
||||
if case let .fixed(value) = widthTrait {
|
||||
return value
|
||||
}
|
||||
|
||||
guard case .intrinsic = widthTrait else {
|
||||
assertionFailure("cannot get widthOfHorizontallyNonResizableBlock for resizable block")
|
||||
return 0
|
||||
}
|
||||
|
||||
if let cached = cached.nonResizableSize, cached.height == nil {
|
||||
return cached.width
|
||||
}
|
||||
|
||||
let result: CGFloat = switch layoutDirection {
|
||||
case .horizontal:
|
||||
(children.map(\.content.widthOfHorizontallyNonResizableBlock) + gaps)
|
||||
.reduce(0, +)
|
||||
case .vertical:
|
||||
// MOBYANDEXIOS-1092: Only non-resizable children can influence the width of a container
|
||||
// because the widths of resizable children depend on the width of container itself
|
||||
children.filter { !$0.content.isHorizontallyResizable }
|
||||
.map(\.content.widthOfHorizontallyNonResizableBlock).max() ?? 0
|
||||
}
|
||||
|
||||
cached.nonResizableSize = (width: result, height: nil)
|
||||
return result
|
||||
}
|
||||
|
||||
public var heightOfVerticallyNonResizableBlock: CGFloat {
|
||||
assert(
|
||||
layoutMode == .wrap && layoutDirection == .vertical,
|
||||
"First height calculation should only be used for vertical container with wrap layout mode"
|
||||
)
|
||||
return heightOfVerticallyNonResizableBlock(forWidth: .zero)
|
||||
}
|
||||
|
||||
public var weightOfVerticallyResizableBlock: LayoutTrait.Weight {
|
||||
guard case let .weighted(value) = heightTrait else {
|
||||
assertionFailure("try to get weight for non resizable block")
|
||||
return LayoutTrait.Weight.default
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight {
|
||||
guard case let .weighted(value) = widthTrait else {
|
||||
assertionFailure("try to get weight for non resizable block")
|
||||
return LayoutTrait.Weight.default
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public init(
|
||||
blockLayoutDirection: UserInterfaceLayoutDirection = .leftToRight,
|
||||
layoutDirection: LayoutDirection,
|
||||
@@ -186,94 +279,6 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
return layout.ascent
|
||||
}
|
||||
|
||||
private func validateLayoutTraits() throws {
|
||||
if layoutMode == .wrap {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
guard children.map(\.content).allVerticallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: horizontal wrap container has children with resizable height"
|
||||
)
|
||||
}
|
||||
case .vertical:
|
||||
guard children.map(\.content).allHorizontallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: vertical wrap container has children with resizable width"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .intrinsic = widthTrait {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
guard children.map(\.content).allHorizontallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: horizontal intrinsic-width container has children with resizable width"
|
||||
)
|
||||
}
|
||||
case .vertical:
|
||||
guard children.map(\.content).hasHorizontallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: in vertical intrinsic-width container all children have resizable width"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .intrinsic = heightTrait {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
break // this is currently a valid case, see `.max() ?? 0` on line 163
|
||||
case .vertical:
|
||||
guard children.map(\.content).allVerticallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: vertical intrinsic-height container has children with resizable height"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var isVerticallyResizable: Bool { heightTrait.isResizable }
|
||||
public var isHorizontallyResizable: Bool { widthTrait.isResizable }
|
||||
|
||||
public var calculateWidthFirst: Bool {
|
||||
switch widthTrait {
|
||||
case .fixed, .weighted:
|
||||
true
|
||||
case .intrinsic:
|
||||
!(layoutDirection == .vertical && layoutMode == .wrap)
|
||||
}
|
||||
}
|
||||
|
||||
public var isVerticallyConstrained: Bool { heightTrait.isConstrained }
|
||||
public var isHorizontallyConstrained: Bool { widthTrait.isConstrained }
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
if case let .fixed(width) = widthTrait {
|
||||
return width
|
||||
}
|
||||
|
||||
if let cached = cached.intrinsicWidth {
|
||||
return cached
|
||||
}
|
||||
|
||||
var result: CGFloat = switch layoutDirection {
|
||||
case .horizontal:
|
||||
(children.map(\.content.intrinsicContentWidth) + gaps).reduce(0, +)
|
||||
case .vertical:
|
||||
children.map(\.content.intrinsicContentWidth).max() ?? 0
|
||||
}
|
||||
|
||||
if case let .intrinsic(_, minSize, maxSize) = widthTrait {
|
||||
result = clamp(result, min: minSize, max: maxSize)
|
||||
}
|
||||
|
||||
cached.intrinsicWidth = result
|
||||
return result
|
||||
}
|
||||
|
||||
public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat {
|
||||
if case let .fixed(height) = heightTrait {
|
||||
return height
|
||||
@@ -320,43 +325,6 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
return result
|
||||
}
|
||||
|
||||
public var widthOfHorizontallyNonResizableBlock: CGFloat {
|
||||
if case let .fixed(value) = widthTrait {
|
||||
return value
|
||||
}
|
||||
|
||||
guard case .intrinsic = widthTrait else {
|
||||
assertionFailure("cannot get widthOfHorizontallyNonResizableBlock for resizable block")
|
||||
return 0
|
||||
}
|
||||
|
||||
if let cached = cached.nonResizableSize, cached.height == nil {
|
||||
return cached.width
|
||||
}
|
||||
|
||||
let result: CGFloat = switch layoutDirection {
|
||||
case .horizontal:
|
||||
(children.map(\.content.widthOfHorizontallyNonResizableBlock) + gaps)
|
||||
.reduce(0, +)
|
||||
case .vertical:
|
||||
// MOBYANDEXIOS-1092: Only non-resizable children can influence the width of a container
|
||||
// because the widths of resizable children depend on the width of container itself
|
||||
children.filter { !$0.content.isHorizontallyResizable }
|
||||
.map(\.content.widthOfHorizontallyNonResizableBlock).max() ?? 0
|
||||
}
|
||||
|
||||
cached.nonResizableSize = (width: result, height: nil)
|
||||
return result
|
||||
}
|
||||
|
||||
public var heightOfVerticallyNonResizableBlock: CGFloat {
|
||||
assert(
|
||||
layoutMode == .wrap && layoutDirection == .vertical,
|
||||
"First height calculation should only be used for vertical container with wrap layout mode"
|
||||
)
|
||||
return heightOfVerticallyNonResizableBlock(forWidth: .zero)
|
||||
}
|
||||
|
||||
public func widthOfHorizontallyNonResizableBlock(forHeight height: CGFloat) -> CGFloat {
|
||||
assert(
|
||||
layoutMode == .wrap && layoutDirection == .vertical,
|
||||
@@ -406,22 +374,6 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public var weightOfVerticallyResizableBlock: LayoutTrait.Weight {
|
||||
guard case let .weighted(value) = heightTrait else {
|
||||
assertionFailure("try to get weight for non resizable block")
|
||||
return LayoutTrait.Weight.default
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public var weightOfHorizontallyResizableBlock: LayoutTrait.Weight {
|
||||
guard case let .weighted(value) = widthTrait else {
|
||||
assertionFailure("try to get weight for non resizable block")
|
||||
return LayoutTrait.Weight.default
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
public func equals(_ other: Block) -> Bool {
|
||||
guard let other = other as? ContainerBlock else {
|
||||
return false
|
||||
@@ -450,6 +402,56 @@ public final class ContainerBlock: BlockWithLayout {
|
||||
|
||||
return (block, layout)
|
||||
}
|
||||
|
||||
private func validateLayoutTraits() throws {
|
||||
if layoutMode == .wrap {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
guard children.map(\.content).allVerticallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: horizontal wrap container has children with resizable height"
|
||||
)
|
||||
}
|
||||
case .vertical:
|
||||
guard children.map(\.content).allHorizontallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: vertical wrap container has children with resizable width"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .intrinsic = widthTrait {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
guard children.map(\.content).allHorizontallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: horizontal intrinsic-width container has children with resizable width"
|
||||
)
|
||||
}
|
||||
case .vertical:
|
||||
guard children.map(\.content).hasHorizontallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: in vertical intrinsic-width container all children have resizable width"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .intrinsic = heightTrait {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
break // this is currently a valid case, see `.max() ?? 0` on line 163
|
||||
case .vertical:
|
||||
guard children.map(\.content).allVerticallyNonResizable else {
|
||||
throw BlockError(
|
||||
"Container block error: vertical intrinsic-height container has children with resizable height"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func makeGapsWithSeparators(
|
||||
|
||||
@@ -7,6 +7,15 @@ struct ContainerBlockLayout {
|
||||
case fits
|
||||
case doesNotFit(notFittingPartSize: CGFloat)
|
||||
|
||||
var insetValue: CGFloat {
|
||||
switch self {
|
||||
case .fits:
|
||||
0
|
||||
case let .doesNotFit(notFittingPartSize: inset):
|
||||
inset
|
||||
}
|
||||
}
|
||||
|
||||
init(offsets: [CGFloat], margin: CGFloat) {
|
||||
let minOffset = (offsets.min() ?? 0) - margin
|
||||
if minOffset.isApproximatelyLessThan(0) {
|
||||
@@ -16,19 +25,12 @@ struct ContainerBlockLayout {
|
||||
}
|
||||
}
|
||||
|
||||
var insetValue: CGFloat {
|
||||
switch self {
|
||||
case .fits:
|
||||
0
|
||||
case let .doesNotFit(notFittingPartSize: inset):
|
||||
inset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var childrenWithSeparators: [ContainerBlock.Child] = []
|
||||
public private(set) var blockFrames: [CGRect] = []
|
||||
public private(set) var ascent: CGFloat?
|
||||
|
||||
let gaps: [CGFloat]
|
||||
let blockLayoutDirection: UserInterfaceLayoutDirection
|
||||
let layoutDirection: ContainerBlock.LayoutDirection
|
||||
@@ -38,6 +40,32 @@ struct ContainerBlockLayout {
|
||||
let needCompressConstrainedBlocks: Bool
|
||||
let axialAlignmentManager: AxialAlignmentManager
|
||||
|
||||
public var leftInset: CGFloat {
|
||||
let leftMargin = layoutDirection == .horizontal ? gaps.first! : 0
|
||||
return ContentFitting(
|
||||
offsets: blockFrames.map(\.minX),
|
||||
margin: leftMargin
|
||||
).insetValue
|
||||
}
|
||||
|
||||
public var topInset: CGFloat {
|
||||
let topMargin = layoutDirection == .vertical ? gaps.first! : 0
|
||||
return ContentFitting(
|
||||
offsets: blockFrames.map(\.minY),
|
||||
margin: topMargin
|
||||
).insetValue
|
||||
}
|
||||
|
||||
public var bottomInset: CGFloat { layoutDirection == .vertical ? gaps.last! : 0 }
|
||||
public var rightInset: CGFloat { layoutDirection == .horizontal ? gaps.last! : 0 }
|
||||
|
||||
public var contentSize: CGSize {
|
||||
CGSize(
|
||||
width: blockFrames.map(\.maxX).max() ?? 0,
|
||||
height: blockFrames.map(\.maxY).max() ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
private var sizeInDirection: CGFloat {
|
||||
switch layoutDirection {
|
||||
case .horizontal:
|
||||
@@ -380,32 +408,6 @@ struct ContainerBlockLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public var leftInset: CGFloat {
|
||||
let leftMargin = layoutDirection == .horizontal ? gaps.first! : 0
|
||||
return ContentFitting(
|
||||
offsets: blockFrames.map(\.minX),
|
||||
margin: leftMargin
|
||||
).insetValue
|
||||
}
|
||||
|
||||
public var topInset: CGFloat {
|
||||
let topMargin = layoutDirection == .vertical ? gaps.first! : 0
|
||||
return ContentFitting(
|
||||
offsets: blockFrames.map(\.minY),
|
||||
margin: topMargin
|
||||
).insetValue
|
||||
}
|
||||
|
||||
public var bottomInset: CGFloat { layoutDirection == .vertical ? gaps.last! : 0 }
|
||||
public var rightInset: CGFloat { layoutDirection == .horizontal ? gaps.last! : 0 }
|
||||
|
||||
public var contentSize: CGSize {
|
||||
CGSize(
|
||||
width: blockFrames.map(\.maxX).max() ?? 0,
|
||||
height: blockFrames.map(\.maxY).max() ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
private func getMaxAscent(
|
||||
current containerAscent: CGFloat?,
|
||||
child: ContainerBlock.Child,
|
||||
|
||||
@@ -20,6 +20,23 @@ struct WrapLayoutGroups {
|
||||
private var offset: CGFloat = 0
|
||||
private var separatorAdded = false
|
||||
|
||||
private var separatorOffset: CGFloat {
|
||||
guard let separator else {
|
||||
return 0
|
||||
}
|
||||
let separatorSize = separatorSize[keyPath: keyPath]
|
||||
var offset: CGFloat = 0
|
||||
if line.count > 0, separator.showBetween {
|
||||
offset = separatorSize
|
||||
} else if separator.showAtStart {
|
||||
offset = separatorSize
|
||||
}
|
||||
if separator.showAtEnd {
|
||||
offset = offset + separatorSize
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
init(
|
||||
blockLayoutDirection: UserInterfaceLayoutDirection,
|
||||
children: [ContainerBlock.Child],
|
||||
@@ -93,23 +110,6 @@ struct WrapLayoutGroups {
|
||||
childrenWithSeparators = groups.flatMap { $0 }.map(\.child)
|
||||
}
|
||||
|
||||
private var separatorOffset: CGFloat {
|
||||
guard let separator else {
|
||||
return 0
|
||||
}
|
||||
let separatorSize = separatorSize[keyPath: keyPath]
|
||||
var offset: CGFloat = 0
|
||||
if line.count > 0, separator.showBetween {
|
||||
offset = separatorSize
|
||||
} else if separator.showAtStart {
|
||||
offset = separatorSize
|
||||
}
|
||||
if separator.showAtEnd {
|
||||
offset = offset + separatorSize
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
private mutating func addChild(child: ContainerBlock.Child, size: CGSize) {
|
||||
if line.count > 0 {
|
||||
addBetweenSeparator()
|
||||
|
||||
@@ -34,6 +34,14 @@ final class DecoratingBlock: WrapperBlock {
|
||||
let isFocused: Bool
|
||||
let captureFocusOnAction: Bool
|
||||
|
||||
var intrinsicContentWidth: CGFloat {
|
||||
child.intrinsicContentWidth.roundedToScreenScale + paddings.horizontal.sum
|
||||
}
|
||||
|
||||
var widthOfHorizontallyNonResizableBlock: CGFloat {
|
||||
intrinsicContentWidth
|
||||
}
|
||||
|
||||
init(
|
||||
child: Block,
|
||||
backgroundColor: Color = DecoratingBlock.defaultBackgroundColor,
|
||||
@@ -86,8 +94,11 @@ final class DecoratingBlock: WrapperBlock {
|
||||
self.captureFocusOnAction = captureFocusOnAction
|
||||
}
|
||||
|
||||
var intrinsicContentWidth: CGFloat {
|
||||
child.intrinsicContentWidth.roundedToScreenScale + paddings.horizontal.sum
|
||||
public func ascent(forWidth width: CGFloat) -> CGFloat? {
|
||||
guard let childAscent = child.ascent(forWidth: width) else {
|
||||
return nil
|
||||
}
|
||||
return childAscent + paddings.vertical.leading
|
||||
}
|
||||
|
||||
func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat {
|
||||
@@ -96,21 +107,10 @@ final class DecoratingBlock: WrapperBlock {
|
||||
+ paddings.vertical.sum
|
||||
}
|
||||
|
||||
var widthOfHorizontallyNonResizableBlock: CGFloat {
|
||||
intrinsicContentWidth
|
||||
}
|
||||
|
||||
func heightOfVerticallyNonResizableBlock(forWidth width: CGFloat) -> CGFloat {
|
||||
intrinsicContentHeight(forWidth: width)
|
||||
}
|
||||
|
||||
public func ascent(forWidth width: CGFloat) -> CGFloat? {
|
||||
guard let childAscent = child.ascent(forWidth: width) else {
|
||||
return nil
|
||||
}
|
||||
return childAscent + paddings.vertical.leading
|
||||
}
|
||||
|
||||
func laidOut(for width: CGFloat) -> Block {
|
||||
let childWidth = width - paddings.horizontal.sum
|
||||
return updatingChild(child.laidOut(for: childWidth))
|
||||
|
||||
@@ -6,6 +6,14 @@ public final class EmptyBlock: BlockWithTraits {
|
||||
public let widthTrait: LayoutTrait
|
||||
public let heightTrait: LayoutTrait
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
widthTrait.intrinsicSize
|
||||
}
|
||||
|
||||
public var isEmpty: Bool {
|
||||
self == EmptyBlock.zeroSized
|
||||
}
|
||||
|
||||
public init(
|
||||
widthTrait: LayoutTrait = .resizable,
|
||||
heightTrait: LayoutTrait = .resizable
|
||||
@@ -14,20 +22,12 @@ public final class EmptyBlock: BlockWithTraits {
|
||||
self.heightTrait = heightTrait
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
widthTrait.intrinsicSize
|
||||
}
|
||||
|
||||
public func intrinsicContentHeight(forWidth _: CGFloat) -> CGFloat {
|
||||
heightTrait.intrinsicSize
|
||||
}
|
||||
|
||||
public func getImageHolders() -> [ImageHolder] { [] }
|
||||
|
||||
public var isEmpty: Bool {
|
||||
self == EmptyBlock.zeroSized
|
||||
}
|
||||
|
||||
public func equals(_ other: Block) -> Bool {
|
||||
guard let other = other as? EmptyBlock else {
|
||||
return false
|
||||
|
||||
@@ -3,17 +3,29 @@ import Foundation
|
||||
import VGSL
|
||||
|
||||
public final class GalleryBlock: BlockWithTraits {
|
||||
private lazy var contentSize: CGSize = model.intrinsicSize
|
||||
|
||||
public let model: GalleryViewModel
|
||||
public let state: GalleryViewState
|
||||
public let widthTrait: LayoutTrait
|
||||
public let heightTrait: LayoutTrait
|
||||
|
||||
private lazy var contentSize: CGSize = model.intrinsicSize
|
||||
|
||||
public var path: UIElementPath? {
|
||||
model.path
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(value):
|
||||
return value
|
||||
case let .intrinsic(_, minSize, maxSize):
|
||||
let width = contentSize.width
|
||||
return clamp(width, min: minSize, max: maxSize)
|
||||
case .weighted:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
model: GalleryViewModel,
|
||||
state: GalleryViewState,
|
||||
@@ -34,18 +46,6 @@ public final class GalleryBlock: BlockWithTraits {
|
||||
}
|
||||
}
|
||||
|
||||
public var intrinsicContentWidth: CGFloat {
|
||||
switch widthTrait {
|
||||
case let .fixed(value):
|
||||
return value
|
||||
case let .intrinsic(_, minSize, maxSize):
|
||||
let width = contentSize.width
|
||||
return clamp(width, min: minSize, max: maxSize)
|
||||
case .weighted:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
public func intrinsicContentHeight(forWidth width: CGFloat) -> CGFloat {
|
||||
switch heightTrait {
|
||||
case let .fixed(value):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user