diff --git a/.mapping.json b/.mapping.json index 4d8b99b..dee4c08 100644 --- a/.mapping.json +++ b/.mapping.json @@ -475,6 +475,12 @@ "DivKit/generated_sources/DivStretchIndicatorItemPlacement.swift":"divkit/public-ios/DivKit/generated_sources/DivStretchIndicatorItemPlacement.swift", "DivKit/generated_sources/DivStretchIndicatorItemPlacementTemplate.swift":"divkit/public-ios/DivKit/generated_sources/DivStretchIndicatorItemPlacementTemplate.swift", "DivKit/generated_sources/DivStroke.swift":"divkit/public-ios/DivKit/generated_sources/DivStroke.swift", + "DivKit/generated_sources/DivStrokeStyle.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeStyle.swift", + "DivKit/generated_sources/DivStrokeStyleDashed.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeStyleDashed.swift", + "DivKit/generated_sources/DivStrokeStyleDashedTemplate.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeStyleDashedTemplate.swift", + "DivKit/generated_sources/DivStrokeStyleSolid.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeStyleSolid.swift", + "DivKit/generated_sources/DivStrokeStyleSolidTemplate.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeStyleSolidTemplate.swift", + "DivKit/generated_sources/DivStrokeStyleTemplate.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeStyleTemplate.swift", "DivKit/generated_sources/DivStrokeTemplate.swift":"divkit/public-ios/DivKit/generated_sources/DivStrokeTemplate.swift", "DivKit/generated_sources/DivSwitch.swift":"divkit/public-ios/DivKit/generated_sources/DivSwitch.swift", "DivKit/generated_sources/DivSwitchTemplate.swift":"divkit/public-ios/DivKit/generated_sources/DivSwitchTemplate.swift", @@ -627,6 +633,7 @@ "LayoutKit/LayoutKit/Blocks/BlockError.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlockError.swift", "LayoutKit/LayoutKit/Blocks/BlockTooltip+Layout.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlockTooltip+Layout.swift", "LayoutKit/LayoutKit/Blocks/BlockTooltip.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift", + "LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift", "LayoutKit/LayoutKit/Blocks/BlockWithLayout.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlockWithLayout.swift", "LayoutKit/LayoutKit/Blocks/BlockWithTraits.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlockWithTraits.swift", "LayoutKit/LayoutKit/Blocks/BlocksStateExtensions.swift":"divkit/public-ios/LayoutKit/LayoutKit/Blocks/BlocksStateExtensions.swift", @@ -1040,6 +1047,7 @@ "Specs/DivKit/31.1.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/31.1.0/DivKit.podspec", "Specs/DivKit/31.2.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/31.2.0/DivKit.podspec", "Specs/DivKit/31.3.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/31.3.0/DivKit.podspec", + "Specs/DivKit/31.4.0/DivKit.podspec":"divkit/public-ios/Specs/DivKit/31.4.0/DivKit.podspec", "Specs/DivKitExtensions/24.3.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/24.3.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/25.0.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/25.0.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/25.1.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/25.1.0/DivKitExtensions.podspec", @@ -1129,6 +1137,7 @@ "Specs/DivKitExtensions/31.1.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/31.1.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/31.2.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/31.2.0/DivKitExtensions.podspec", "Specs/DivKitExtensions/31.3.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/31.3.0/DivKitExtensions.podspec", + "Specs/DivKitExtensions/31.4.0/DivKitExtensions.podspec":"divkit/public-ios/Specs/DivKitExtensions/31.4.0/DivKitExtensions.podspec", "Specs/DivKit_LayoutKit/28.0.1/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/28.0.1/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/28.1.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/28.1.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/28.10.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/28.10.0/DivKit_LayoutKit.podspec", @@ -1200,6 +1209,7 @@ "Specs/DivKit_LayoutKit/31.1.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/31.1.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/31.2.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/31.2.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKit/31.3.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/31.3.0/DivKit_LayoutKit.podspec", + "Specs/DivKit_LayoutKit/31.4.0/DivKit_LayoutKit.podspec":"divkit/public-ios/Specs/DivKit_LayoutKit/31.4.0/DivKit_LayoutKit.podspec", "Specs/DivKit_LayoutKitInterface/28.0.1/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/28.0.1/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/28.1.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/28.1.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/28.10.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/28.10.0/DivKit_LayoutKitInterface.podspec", @@ -1271,6 +1281,7 @@ "Specs/DivKit_LayoutKitInterface/31.1.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/31.1.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/31.2.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/31.2.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_LayoutKitInterface/31.3.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/31.3.0/DivKit_LayoutKitInterface.podspec", + "Specs/DivKit_LayoutKitInterface/31.4.0/DivKit_LayoutKitInterface.podspec":"divkit/public-ios/Specs/DivKit_LayoutKitInterface/31.4.0/DivKit_LayoutKitInterface.podspec", "Specs/DivKit_Serialization/28.0.1/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/28.0.1/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/28.1.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/28.1.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/28.10.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/28.10.0/DivKit_Serialization.podspec", @@ -1342,6 +1353,7 @@ "Specs/DivKit_Serialization/31.1.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/31.1.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/31.2.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/31.2.0/DivKit_Serialization.podspec", "Specs/DivKit_Serialization/31.3.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/31.3.0/DivKit_Serialization.podspec", + "Specs/DivKit_Serialization/31.4.0/DivKit_Serialization.podspec":"divkit/public-ios/Specs/DivKit_Serialization/31.4.0/DivKit_Serialization.podspec", "Specs/LayoutKit/24.3.0/LayoutKit.podspec":"divkit/public-ios/Specs/LayoutKit/24.3.0/LayoutKit.podspec", "Specs/LayoutKit/25.0.0/LayoutKit.podspec":"divkit/public-ios/Specs/LayoutKit/25.0.0/LayoutKit.podspec", "Specs/LayoutKit/25.1.0/LayoutKit.podspec":"divkit/public-ios/Specs/LayoutKit/25.1.0/LayoutKit.podspec", diff --git a/DivKit/DivKitInfo.swift b/DivKit/DivKitInfo.swift index c9e6606..82bbc98 100644 --- a/DivKit/DivKitInfo.swift +++ b/DivKit/DivKitInfo.swift @@ -1,3 +1,3 @@ public enum DivKitInfo { - public static let version = "31.3.0" + public static let version = "31.4.0" } diff --git a/DivKit/Expressions/Serialization/FieldExtensions.swift b/DivKit/Expressions/Serialization/FieldExtensions.swift index ce8d888..9494d0d 100644 --- a/DivKit/Expressions/Serialization/FieldExtensions.swift +++ b/DivKit/Expressions/Serialization/FieldExtensions.swift @@ -124,4 +124,23 @@ extension Field { } return result } + + @inlinable + func resolveOptionalValue( + context: TemplatesContext, + transform: (U) -> E?, + validator: AnyArrayValueValidator> + ) -> DeserializationResult where T == [Expression] { + let result = resolveValue( + context: context, + transform: transform, + validator: validator + ) + if case let .failure(errors) = result, + errors.count == 1, + case .noData = errors.first { + return .noValue + } + return result + } } diff --git a/DivKit/Extensions/DivBackgroundExtensions.swift b/DivKit/Extensions/DivBackgroundExtensions.swift index 1352781..ac3db00 100644 --- a/DivKit/Extensions/DivBackgroundExtensions.swift +++ b/DivKit/Extensions/DivBackgroundExtensions.swift @@ -10,10 +10,7 @@ extension DivBackground { let expressionResolver = context.expressionResolver switch self { case let .divLinearGradient(gradient): - return Gradient.Linear( - colors: gradient.resolveColors(expressionResolver) ?? [], - angle: gradient.resolveAngle(expressionResolver) - ).map { .gradient(.linear($0)) } + return gradient.makeBlockLinearGradient(context).map { .gradient(.linear($0)) } case let .divRadialGradient(gradient): return Gradient.Radial( colors: gradient.resolveColors(expressionResolver) ?? [], diff --git a/DivKit/Extensions/DivBase/DivBaseExtensions.swift b/DivKit/Extensions/DivBase/DivBaseExtensions.swift index 6a354b0..b947b8a 100644 --- a/DivKit/Extensions/DivBase/DivBaseExtensions.swift +++ b/DivKit/Extensions/DivBase/DivBaseExtensions.swift @@ -395,6 +395,7 @@ extension DivBorder { return nil } return BlockBorder( + style: stroke.resolveStyle(expressionResolver), color: stroke.resolveColor(expressionResolver) ?? .black, width: stroke.resolveUnit(expressionResolver) .makeScaledValue(stroke.resolveWidth(expressionResolver)) @@ -440,6 +441,15 @@ extension DivBorder { } } +extension DivStroke { + fileprivate func resolveStyle(_: ExpressionResolver) -> BlockBorder.Style { + switch style { + case .divStrokeStyleDashed: BlockBorder.Style.dashed + case .divStrokeStyleSolid: BlockBorder.Style.solid + } + } +} + extension DivBlockModelingContext { fileprivate func makeVisibilityParams( actions: [VisibilityAction], diff --git a/DivKit/Extensions/DivTextExtensions.swift b/DivKit/Extensions/DivTextExtensions.swift index 0b2af5e..9fb4c4c 100644 --- a/DivKit/Extensions/DivTextExtensions.swift +++ b/DivKit/Extensions/DivTextExtensions.swift @@ -109,7 +109,7 @@ extension DivText: DivBlockModeling { widthTrait: resolveContentWidthTrait(context), heightTrait: resolveContentHeightTrait(context), text: attributedString, - textGradient: resolveGradient(expressionResolver), + textGradient: resolveGradient(context), verticalAlignment: resolveTextAlignmentVertical(expressionResolver).alignment, maxIntrinsicNumberOfLines: resolveMaxLines(expressionResolver) ?? .max, minNumberOfHiddenLines: resolveMinHiddenLines(expressionResolver) ?? 0, @@ -120,7 +120,8 @@ extension DivText: DivBlockModeling { additionalTextInsets: additionalTextInsets, canSelect: resolveSelectable(expressionResolver), tightenWidth: resolveTightenWidth(expressionResolver), - autoEllipsize: resolveAutoEllipsize(expressionResolver) ?? context.flagsInfo.defaultTextAutoEllipsize + autoEllipsize: resolveAutoEllipsize(expressionResolver) ?? context.flagsInfo + .defaultTextAutoEllipsize ) } @@ -152,11 +153,11 @@ extension DivText: DivBlockModeling { guard let text else { return [] } - return (images ?? []).compactMap { + return (images ?? []).compactMap { $0.makeImage( - context: context, + context: context, textLength: CFAttributedStringGetLength(text) - ) + ) } } @@ -231,16 +232,14 @@ extension DivText: DivBlockModeling { } } - private func resolveGradient(_ expressionResolver: ExpressionResolver) -> Gradient? { + private func resolveGradient(_ context: DivBlockModelingContext) -> Gradient? { + let expressionResolver = context.expressionResolver guard let textGradient else { return nil } switch textGradient { case let .divLinearGradient(gradient): - return Gradient.Linear( - colors: gradient.resolveColors(expressionResolver) ?? [], - angle: gradient.resolveAngle(expressionResolver) - ).map { .linear($0) } + return gradient.makeBlockLinearGradient(context).map { .linear($0) } case let .divRadialGradient(gradient): return Gradient.Radial( colors: gradient.resolveColors(expressionResolver) ?? [], @@ -317,19 +316,19 @@ extension DivText.Image { ) -> TextBlock.InlineImage? { let expressionResolver = context.expressionResolver let start = resolveStart(expressionResolver) ?? 0 - + guard start <= textLength else { return nil } - + let indexingDirection = resolveIndexingDirection(expressionResolver) let location = switch indexingDirection { - case .normal: + case .normal: start case .reversed: textLength - start } - + return TextBlock.InlineImage( size: CGSize( width: CGFloat(width.resolveValue(expressionResolver) ?? 0), diff --git a/DivKit/Extensions/DivTooltipExtensions.swift b/DivKit/Extensions/DivTooltipExtensions.swift index dfad607..02d72c3 100644 --- a/DivKit/Extensions/DivTooltipExtensions.swift +++ b/DivKit/Extensions/DivTooltipExtensions.swift @@ -26,17 +26,19 @@ extension DivTooltip { } return try BlockTooltip( - id: id, // Legacy behavior. Views should be created with tooltipViewFactory. block: div.value.makeBlock(context: context), - duration: TimeInterval(milliseconds: resolveDuration(expressionResolver)), + params: BlockTooltipParams( + id: id, + mode: mode, + duration: TimeInterval(milliseconds: resolveDuration(expressionResolver)), + closeByTapOutside: resolveCloseByTapOutside(expressionResolver), + tapOutsideActions: tapOutsideActions?.uiActions(context: context) ?? [] + ), offset: offset?.resolve(expressionResolver) ?? .zero, position: position, useLegacyWidth: context.flagsInfo.useTooltipLegacyWidth, - tooltipViewFactory: tooltipViewFactory, - closeByTapOutside: resolveCloseByTapOutside(expressionResolver), - tapOutsideActions: tapOutsideActions?.uiActions(context: context) ?? [], - mode: mode + tooltipViewFactory: tooltipViewFactory ) } } diff --git a/DivKit/Extensions/GradientExtentions.swift b/DivKit/Extensions/GradientExtentions.swift index c0adbc7..58eabd8 100644 --- a/DivKit/Extensions/GradientExtentions.swift +++ b/DivKit/Extensions/GradientExtentions.swift @@ -3,6 +3,21 @@ import Foundation import VGSL extension Gradient.Linear { + init?(points: [Gradient.Point], angle: Int) { + guard points.count >= 2 else { return nil } + let sortedPoints = points.stableSort(isLessOrEqual: { $0.location <= $1.location }) + + guard let start = sortedPoints.first, let end = sortedPoints.last + else { return nil } + + self.init( + startColor: start.color, + intermediatePoints: sortedPoints, + endColor: end.color, + direction: .init(angle: angle) + ) + } + init?(colors: [Color], angle: Int) { guard colors.count >= 2 else { return nil @@ -65,6 +80,39 @@ extension Gradient.Radial { } } +extension DivLinearGradient { + func makeBlockLinearGradient( + _ context: DivBlockModelingContext + ) -> Gradient.Linear? { + let expressionResolver = context.expressionResolver + + if let colorMap { + let points: [Gradient.Point] = colorMap.compactMap { positionedColor -> Gradient.Point? in + if let color = positionedColor.resolveColor(expressionResolver), + let position = positionedColor.resolvePosition(expressionResolver) { + return (color: color, location: CGFloat(position)) + } + return nil + } + + return Gradient.Linear( + points: points, + angle: resolveAngle(expressionResolver) + ) + } else if let colors = resolveColors(expressionResolver) { + return Gradient.Linear( + colors: colors, + angle: resolveAngle(expressionResolver) + ) + } else { + context.addError( + message: "No colors specified in the linear gradient in the `colors` or `color_map` fields" + ) + return nil + } + } +} + extension DivRadialGradient { func resolveRadius(_ resolver: ExpressionResolver) -> Gradient.Radial.Radius { switch radius { diff --git a/DivKit/generated_sources/DivLinearGradient.swift b/DivKit/generated_sources/DivLinearGradient.swift index 9ec1613..7923be3 100644 --- a/DivKit/generated_sources/DivLinearGradient.swift +++ b/DivKit/generated_sources/DivLinearGradient.swift @@ -5,29 +5,59 @@ import Serialization import VGSL public final class DivLinearGradient: Sendable { + public final class ColorPoint: Sendable { + public let color: Expression + public let position: Expression // constraint: number >= 0.0 && number <= 1.0 + + public func resolveColor(_ resolver: ExpressionResolver) -> Color? { + resolver.resolveColor(color) + } + + public func resolvePosition(_ resolver: ExpressionResolver) -> Double? { + resolver.resolveNumeric(position) + } + + static let positionValidator: AnyValueValidator = + makeValueValidator(valueValidator: { $0 >= 0.0 && $0 <= 1.0 }) + + init( + color: Expression, + position: Expression + ) { + self.color = color + self.position = position + } + } + public static let type: String = "gradient" public let angle: Expression // constraint: number >= 0 && number <= 360; default value: 0 - public let colors: [Expression] // at least 2 elements + public let colorMap: [ColorPoint]? // at least 2 elements + public let colors: [Expression]? // at least 2 elements public func resolveAngle(_ resolver: ExpressionResolver) -> Int { resolver.resolveNumeric(angle) ?? 0 } public func resolveColors(_ resolver: ExpressionResolver) -> [Color]? { - colors.map { resolver.resolveColor($0) }.compactMap { $0 } + colors?.map { resolver.resolveColor($0) }.compactMap { $0 } } static let angleValidator: AnyValueValidator = makeValueValidator(valueValidator: { $0 >= 0 && $0 <= 360 }) + static let colorMapValidator: AnyArrayValueValidator = + makeArrayValidator(minItems: 2) + static let colorsValidator: AnyArrayValueValidator> = makeArrayValidator(minItems: 2) init( angle: Expression? = nil, - colors: [Expression] + colorMap: [ColorPoint]? = nil, + colors: [Expression]? = nil ) { self.angle = angle ?? .value(0) + self.colorMap = colorMap self.colors = colors } } @@ -37,6 +67,7 @@ extension DivLinearGradient: Equatable { public static func ==(lhs: DivLinearGradient, rhs: DivLinearGradient) -> Bool { guard lhs.angle == rhs.angle, + lhs.colorMap == rhs.colorMap, lhs.colors == rhs.colors else { return false @@ -51,7 +82,31 @@ extension DivLinearGradient: Serializable { var result: [String: ValidSerializationValue] = [:] result["type"] = Self.type result["angle"] = angle.toValidSerializationValue() - result["colors"] = colors.map { $0.toValidSerializationValue() } + result["color_map"] = colorMap?.map { $0.toDictionary() } + result["colors"] = colors?.map { $0.toValidSerializationValue() } + return result + } +} + +#if DEBUG +extension DivLinearGradient.ColorPoint: Equatable { + public static func ==(lhs: DivLinearGradient.ColorPoint, rhs: DivLinearGradient.ColorPoint) -> Bool { + guard + lhs.color == rhs.color, + lhs.position == rhs.position + else { + return false + } + return true + } +} +#endif + +extension DivLinearGradient.ColorPoint: Serializable { + public func toDictionary() -> [String: ValidSerializationValue] { + var result: [String: ValidSerializationValue] = [:] + result["color"] = color.toValidSerializationValue() + result["position"] = position.toValidSerializationValue() return result } } diff --git a/DivKit/generated_sources/DivLinearGradientTemplate.swift b/DivKit/generated_sources/DivLinearGradientTemplate.swift index f98f0d3..68a1b35 100644 --- a/DivKit/generated_sources/DivLinearGradientTemplate.swift +++ b/DivKit/generated_sources/DivLinearGradientTemplate.swift @@ -5,15 +5,127 @@ import Serialization import VGSL public final class DivLinearGradientTemplate: TemplateValue, Sendable { + public final class ColorPointTemplate: TemplateValue, Sendable { + public let color: Field>? + public let position: Field>? // constraint: number >= 0.0 && number <= 1.0 + + public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + self.init( + color: dictionary.getOptionalExpressionField("color", transform: Color.color(withHexString:)), + position: dictionary.getOptionalExpressionField("position") + ) + } + + init( + color: Field>? = nil, + position: Field>? = nil + ) { + self.color = color + self.position = position + } + + private static func resolveOnlyLinks(context: TemplatesContext, parent: ColorPointTemplate?) -> DeserializationResult { + let colorValue = { parent?.color?.resolveValue(context: context, transform: Color.color(withHexString:)) ?? .noValue }() + let positionValue = { parent?.position?.resolveValue(context: context, validator: ResolvedValue.positionValidator) ?? .noValue }() + var errors = mergeErrors( + colorValue.errorsOrWarnings?.map { .nestedObjectError(field: "color", error: $0) }, + positionValue.errorsOrWarnings?.map { .nestedObjectError(field: "position", error: $0) } + ) + if case .noValue = colorValue { + errors.append(.requiredFieldIsMissing(field: "color")) + } + if case .noValue = positionValue { + errors.append(.requiredFieldIsMissing(field: "position")) + } + guard + let colorNonNil = colorValue.value, + let positionNonNil = positionValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivLinearGradient.ColorPoint( + color: { colorNonNil }(), + position: { positionNonNil }() + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + public static func resolveValue(context: TemplatesContext, parent: ColorPointTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + if useOnlyLinks { + return resolveOnlyLinks(context: context, parent: parent) + } + var colorValue: DeserializationResult> = { parent?.color?.value() ?? .noValue }() + var positionValue: DeserializationResult> = { parent?.position?.value() ?? .noValue }() + _ = { + // Each field is parsed in its own lambda to keep the stack size managable + // Otherwise the compiler will allocate stack for each intermediate variable + // upfront even when we don't actually visit a relevant branch + for (key, __dictValue) in context.templateData { + _ = { + if key == "color" { + colorValue = deserialize(__dictValue, transform: Color.color(withHexString:)).merged(with: colorValue) + } + }() + _ = { + if key == "position" { + positionValue = deserialize(__dictValue, validator: ResolvedValue.positionValidator).merged(with: positionValue) + } + }() + _ = { + if key == parent?.color?.link { + colorValue = colorValue.merged(with: { deserialize(__dictValue, transform: Color.color(withHexString:)) }) + } + }() + _ = { + if key == parent?.position?.link { + positionValue = positionValue.merged(with: { deserialize(__dictValue, validator: ResolvedValue.positionValidator) }) + } + }() + } + }() + var errors = mergeErrors( + colorValue.errorsOrWarnings?.map { .nestedObjectError(field: "color", error: $0) }, + positionValue.errorsOrWarnings?.map { .nestedObjectError(field: "position", error: $0) } + ) + if case .noValue = colorValue { + errors.append(.requiredFieldIsMissing(field: "color")) + } + if case .noValue = positionValue { + errors.append(.requiredFieldIsMissing(field: "position")) + } + guard + let colorNonNil = colorValue.value, + let positionNonNil = positionValue.value + else { + return .failure(NonEmptyArray(errors)!) + } + let result = DivLinearGradient.ColorPoint( + color: { colorNonNil }(), + position: { positionNonNil }() + ) + return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) + } + + private func mergedWithParent(templates: [TemplateName: Any]) throws -> ColorPointTemplate { + return self + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> ColorPointTemplate { + return try mergedWithParent(templates: templates) + } + } + public static let type: String = "gradient" public let parent: String? public let angle: Field>? // constraint: number >= 0 && number <= 360; default value: 0 + public let colorMap: Field<[ColorPointTemplate]>? // at least 2 elements public let colors: Field<[Expression]>? // at least 2 elements public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { self.init( parent: dictionary["type"] as? String, angle: dictionary.getOptionalExpressionField("angle"), + colorMap: dictionary.getOptionalArray("color_map", templateToType: templateToType), colors: dictionary.getOptionalExpressionArray("colors", transform: Color.color(withHexString:)) ) } @@ -21,31 +133,28 @@ public final class DivLinearGradientTemplate: TemplateValue, Sendable { init( parent: String?, angle: Field>? = nil, + colorMap: Field<[ColorPointTemplate]>? = nil, colors: Field<[Expression]>? = nil ) { self.parent = parent self.angle = angle + self.colorMap = colorMap self.colors = colors } private static func resolveOnlyLinks(context: TemplatesContext, parent: DivLinearGradientTemplate?) -> DeserializationResult { let angleValue = { parent?.angle?.resolveOptionalValue(context: context, validator: ResolvedValue.angleValidator) ?? .noValue }() - let colorsValue = { parent?.colors?.resolveValue(context: context, transform: Color.color(withHexString:), validator: ResolvedValue.colorsValidator) ?? .noValue }() - var errors = mergeErrors( + let colorMapValue = { parent?.colorMap?.resolveOptionalValue(context: context, validator: ResolvedValue.colorMapValidator, useOnlyLinks: true) ?? .noValue }() + let colorsValue = { parent?.colors?.resolveOptionalValue(context: context, transform: Color.color(withHexString:), validator: ResolvedValue.colorsValidator) ?? .noValue }() + let errors = mergeErrors( angleValue.errorsOrWarnings?.map { .nestedObjectError(field: "angle", error: $0) }, + colorMapValue.errorsOrWarnings?.map { .nestedObjectError(field: "color_map", error: $0) }, colorsValue.errorsOrWarnings?.map { .nestedObjectError(field: "colors", error: $0) } ) - if case .noValue = colorsValue { - errors.append(.requiredFieldIsMissing(field: "colors")) - } - guard - let colorsNonNil = colorsValue.value - else { - return .failure(NonEmptyArray(errors)!) - } let result = DivLinearGradient( angle: { angleValue.value }(), - colors: { colorsNonNil }() + colorMap: { colorMapValue.value }(), + colors: { colorsValue.value }() ) return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) } @@ -55,6 +164,7 @@ public final class DivLinearGradientTemplate: TemplateValue, Sendable { return resolveOnlyLinks(context: context, parent: parent) } var angleValue: DeserializationResult> = { parent?.angle?.value() ?? .noValue }() + var colorMapValue: DeserializationResult<[DivLinearGradient.ColorPoint]> = .noValue var colorsValue: DeserializationResult<[Expression]> = { parent?.colors?.value() ?? .noValue }() _ = { // Each field is parsed in its own lambda to keep the stack size managable @@ -66,6 +176,11 @@ public final class DivLinearGradientTemplate: TemplateValue, Sendable { angleValue = deserialize(__dictValue, validator: ResolvedValue.angleValidator).merged(with: angleValue) } }() + _ = { + if key == "color_map" { + colorMapValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, validator: ResolvedValue.colorMapValidator, type: DivLinearGradientTemplate.ColorPointTemplate.self).merged(with: colorMapValue) + } + }() _ = { if key == "colors" { colorsValue = deserialize(__dictValue, transform: Color.color(withHexString:), validator: ResolvedValue.colorsValidator).merged(with: colorsValue) @@ -76,6 +191,11 @@ public final class DivLinearGradientTemplate: TemplateValue, Sendable { angleValue = angleValue.merged(with: { deserialize(__dictValue, validator: ResolvedValue.angleValidator) }) } }() + _ = { + if key == parent?.colorMap?.link { + colorMapValue = colorMapValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, validator: ResolvedValue.colorMapValidator, type: DivLinearGradientTemplate.ColorPointTemplate.self) }) + } + }() _ = { if key == parent?.colors?.link { colorsValue = colorsValue.merged(with: { deserialize(__dictValue, transform: Color.color(withHexString:), validator: ResolvedValue.colorsValidator) }) @@ -83,21 +203,18 @@ public final class DivLinearGradientTemplate: TemplateValue, Sendable { }() } }() - var errors = mergeErrors( + if let parent = parent { + _ = { colorMapValue = colorMapValue.merged(with: { parent.colorMap?.resolveOptionalValue(context: context, validator: ResolvedValue.colorMapValidator, useOnlyLinks: true) }) }() + } + let errors = mergeErrors( angleValue.errorsOrWarnings?.map { .nestedObjectError(field: "angle", error: $0) }, + colorMapValue.errorsOrWarnings?.map { .nestedObjectError(field: "color_map", error: $0) }, colorsValue.errorsOrWarnings?.map { .nestedObjectError(field: "colors", error: $0) } ) - if case .noValue = colorsValue { - errors.append(.requiredFieldIsMissing(field: "colors")) - } - guard - let colorsNonNil = colorsValue.value - else { - return .failure(NonEmptyArray(errors)!) - } let result = DivLinearGradient( angle: { angleValue.value }(), - colors: { colorsNonNil }() + colorMap: { colorMapValue.value }(), + colors: { colorsValue.value }() ) return errors.isEmpty ? .success(result) : .partialSuccess(result, warnings: NonEmptyArray(errors)!) } @@ -112,11 +229,19 @@ public final class DivLinearGradientTemplate: TemplateValue, Sendable { return DivLinearGradientTemplate( parent: nil, angle: angle ?? mergedParent.angle, + colorMap: colorMap ?? mergedParent.colorMap, colors: colors ?? mergedParent.colors ) } public func resolveParent(templates: [TemplateName: Any]) throws -> DivLinearGradientTemplate { - return try mergedWithParent(templates: templates) + let merged = try mergedWithParent(templates: templates) + + return DivLinearGradientTemplate( + parent: nil, + angle: merged.angle, + colorMap: merged.colorMap?.tryResolveParent(templates: templates), + colors: merged.colors + ) } } diff --git a/DivKit/generated_sources/DivStroke.swift b/DivKit/generated_sources/DivStroke.swift index 7c0d7fa..212938d 100644 --- a/DivKit/generated_sources/DivStroke.swift +++ b/DivKit/generated_sources/DivStroke.swift @@ -6,6 +6,7 @@ import VGSL public final class DivStroke: Sendable { public let color: Expression + public let style: DivStrokeStyle // default value: .divStrokeStyleSolid(DivStrokeStyleSolid()) public let unit: Expression // default value: dp public let width: Expression // constraint: number >= 0; default value: 1 @@ -26,10 +27,12 @@ public final class DivStroke: Sendable { init( color: Expression, + style: DivStrokeStyle? = nil, unit: Expression? = nil, width: Expression? = nil ) { self.color = color + self.style = style ?? .divStrokeStyleSolid(DivStrokeStyleSolid()) self.unit = unit ?? .value(.dp) self.width = width ?? .value(1) } @@ -40,7 +43,12 @@ extension DivStroke: Equatable { public static func ==(lhs: DivStroke, rhs: DivStroke) -> Bool { guard lhs.color == rhs.color, - lhs.unit == rhs.unit, + lhs.style == rhs.style, + lhs.unit == rhs.unit + else { + return false + } + guard lhs.width == rhs.width else { return false @@ -54,6 +62,7 @@ extension DivStroke: Serializable { public func toDictionary() -> [String: ValidSerializationValue] { var result: [String: ValidSerializationValue] = [:] result["color"] = color.toValidSerializationValue() + result["style"] = style.toDictionary() result["unit"] = unit.toValidSerializationValue() result["width"] = width.toValidSerializationValue() return result diff --git a/DivKit/generated_sources/DivStrokeStyle.swift b/DivKit/generated_sources/DivStrokeStyle.swift new file mode 100644 index 0000000..0400227 --- /dev/null +++ b/DivKit/generated_sources/DivStrokeStyle.swift @@ -0,0 +1,41 @@ +// Generated code. Do not modify. + +import Foundation +import Serialization +import VGSL + +@frozen +public enum DivStrokeStyle: Sendable { + case divStrokeStyleSolid(DivStrokeStyleSolid) + case divStrokeStyleDashed(DivStrokeStyleDashed) + + public var value: Serializable { + switch self { + case let .divStrokeStyleSolid(value): + return value + case let .divStrokeStyleDashed(value): + return value + } + } +} + +#if DEBUG +extension DivStrokeStyle: Equatable { + public static func ==(lhs: DivStrokeStyle, rhs: DivStrokeStyle) -> Bool { + switch (lhs, rhs) { + case let (.divStrokeStyleSolid(l), .divStrokeStyleSolid(r)): + return l == r + case let (.divStrokeStyleDashed(l), .divStrokeStyleDashed(r)): + return l == r + default: + return false + } + } +} +#endif + +extension DivStrokeStyle: Serializable { + public func toDictionary() -> [String: ValidSerializationValue] { + return value.toDictionary() + } +} diff --git a/DivKit/generated_sources/DivStrokeStyleDashed.swift b/DivKit/generated_sources/DivStrokeStyleDashed.swift new file mode 100644 index 0000000..da18aa7 --- /dev/null +++ b/DivKit/generated_sources/DivStrokeStyleDashed.swift @@ -0,0 +1,27 @@ +// Generated code. Do not modify. + +import Foundation +import Serialization +import VGSL + +public final class DivStrokeStyleDashed: Sendable { + public static let type: String = "dashed" + + init() {} +} + +#if DEBUG +extension DivStrokeStyleDashed: Equatable { + public static func ==(lhs: DivStrokeStyleDashed, rhs: DivStrokeStyleDashed) -> Bool { + return true + } +} +#endif + +extension DivStrokeStyleDashed: Serializable { + public func toDictionary() -> [String: ValidSerializationValue] { + var result: [String: ValidSerializationValue] = [:] + result["type"] = Self.type + return result + } +} diff --git a/DivKit/generated_sources/DivStrokeStyleDashedTemplate.swift b/DivKit/generated_sources/DivStrokeStyleDashedTemplate.swift new file mode 100644 index 0000000..7efaf9d --- /dev/null +++ b/DivKit/generated_sources/DivStrokeStyleDashedTemplate.swift @@ -0,0 +1,38 @@ +// Generated code. Do not modify. + +import Foundation +import Serialization +import VGSL + +public final class DivStrokeStyleDashedTemplate: TemplateValue, Sendable { + public static let type: String = "dashed" + public let parent: String? + + public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + self.init( + parent: dictionary["type"] as? String + ) + } + + init( + parent: String? + ) { + self.parent = parent + } + + private static func resolveOnlyLinks(context: TemplatesContext, parent: DivStrokeStyleDashedTemplate?) -> DeserializationResult { + return .success(DivStrokeStyleDashed()) + } + + public static func resolveValue(context: TemplatesContext, parent: DivStrokeStyleDashedTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + return .success(DivStrokeStyleDashed()) + } + + private func mergedWithParent(templates: [TemplateName: Any]) throws -> DivStrokeStyleDashedTemplate { + return self + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> DivStrokeStyleDashedTemplate { + return self + } +} diff --git a/DivKit/generated_sources/DivStrokeStyleSolid.swift b/DivKit/generated_sources/DivStrokeStyleSolid.swift new file mode 100644 index 0000000..4955564 --- /dev/null +++ b/DivKit/generated_sources/DivStrokeStyleSolid.swift @@ -0,0 +1,27 @@ +// Generated code. Do not modify. + +import Foundation +import Serialization +import VGSL + +public final class DivStrokeStyleSolid: Sendable { + public static let type: String = "solid" + + init() {} +} + +#if DEBUG +extension DivStrokeStyleSolid: Equatable { + public static func ==(lhs: DivStrokeStyleSolid, rhs: DivStrokeStyleSolid) -> Bool { + return true + } +} +#endif + +extension DivStrokeStyleSolid: Serializable { + public func toDictionary() -> [String: ValidSerializationValue] { + var result: [String: ValidSerializationValue] = [:] + result["type"] = Self.type + return result + } +} diff --git a/DivKit/generated_sources/DivStrokeStyleSolidTemplate.swift b/DivKit/generated_sources/DivStrokeStyleSolidTemplate.swift new file mode 100644 index 0000000..9b7c0a6 --- /dev/null +++ b/DivKit/generated_sources/DivStrokeStyleSolidTemplate.swift @@ -0,0 +1,38 @@ +// Generated code. Do not modify. + +import Foundation +import Serialization +import VGSL + +public final class DivStrokeStyleSolidTemplate: TemplateValue, Sendable { + public static let type: String = "solid" + public let parent: String? + + public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + self.init( + parent: dictionary["type"] as? String + ) + } + + init( + parent: String? + ) { + self.parent = parent + } + + private static func resolveOnlyLinks(context: TemplatesContext, parent: DivStrokeStyleSolidTemplate?) -> DeserializationResult { + return .success(DivStrokeStyleSolid()) + } + + public static func resolveValue(context: TemplatesContext, parent: DivStrokeStyleSolidTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + return .success(DivStrokeStyleSolid()) + } + + private func mergedWithParent(templates: [TemplateName: Any]) throws -> DivStrokeStyleSolidTemplate { + return self + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> DivStrokeStyleSolidTemplate { + return self + } +} diff --git a/DivKit/generated_sources/DivStrokeStyleTemplate.swift b/DivKit/generated_sources/DivStrokeStyleTemplate.swift new file mode 100644 index 0000000..035e57f --- /dev/null +++ b/DivKit/generated_sources/DivStrokeStyleTemplate.swift @@ -0,0 +1,110 @@ +// Generated code. Do not modify. + +import Foundation +import Serialization +import VGSL + +@frozen +public enum DivStrokeStyleTemplate: TemplateValue, Sendable { + case divStrokeStyleSolidTemplate(DivStrokeStyleSolidTemplate) + case divStrokeStyleDashedTemplate(DivStrokeStyleDashedTemplate) + + public var value: Any { + switch self { + case let .divStrokeStyleSolidTemplate(value): + return value + case let .divStrokeStyleDashedTemplate(value): + return value + } + } + + public func resolveParent(templates: [TemplateName: Any]) throws -> DivStrokeStyleTemplate { + switch self { + case let .divStrokeStyleSolidTemplate(value): + return .divStrokeStyleSolidTemplate(try value.resolveParent(templates: templates)) + case let .divStrokeStyleDashedTemplate(value): + return .divStrokeStyleDashedTemplate(try value.resolveParent(templates: templates)) + } + } + + public static func resolveValue(context: TemplatesContext, parent: DivStrokeStyleTemplate?, useOnlyLinks: Bool) -> DeserializationResult { + guard let parent = parent else { + if useOnlyLinks { + return .failure(NonEmptyArray(.missingType(representation: context.templateData))) + } else { + return resolveUnknownValue(context: context, useOnlyLinks: useOnlyLinks) + } + } + + return { + var result: DeserializationResult! + result = result ?? { + if case let .divStrokeStyleSolidTemplate(value) = parent { + let result = value.resolveValue(context: context, useOnlyLinks: useOnlyLinks) + switch result { + case let .success(value): return .success(.divStrokeStyleSolid(value)) + case let .partialSuccess(value, warnings): return .partialSuccess(.divStrokeStyleSolid(value), warnings: warnings) + case let .failure(errors): return .failure(errors) + case .noValue: return .noValue + } + } else { return nil } + }() + result = result ?? { + if case let .divStrokeStyleDashedTemplate(value) = parent { + let result = value.resolveValue(context: context, useOnlyLinks: useOnlyLinks) + switch result { + case let .success(value): return .success(.divStrokeStyleDashed(value)) + case let .partialSuccess(value, warnings): return .partialSuccess(.divStrokeStyleDashed(value), warnings: warnings) + case let .failure(errors): return .failure(errors) + case .noValue: return .noValue + } + } else { return nil } + }() + return result + }() + } + + private static func resolveUnknownValue(context: TemplatesContext, useOnlyLinks: Bool) -> DeserializationResult { + guard let type = (context.templateData["type"] as? String).flatMap({ context.templateToType[$0] ?? $0 }) else { + return .failure(NonEmptyArray(.requiredFieldIsMissing(field: "type"))) + } + + return { + var result: DeserializationResult? + result = result ?? { if type == DivStrokeStyleSolid.type { + let result = { DivStrokeStyleSolidTemplate.resolveValue(context: context, useOnlyLinks: useOnlyLinks) }() + switch result { + case let .success(value): return .success(.divStrokeStyleSolid(value)) + case let .partialSuccess(value, warnings): return .partialSuccess(.divStrokeStyleSolid(value), warnings: warnings) + case let .failure(errors): return .failure(errors) + case .noValue: return .noValue + } + } else { return nil } }() + result = result ?? { if type == DivStrokeStyleDashed.type { + let result = { DivStrokeStyleDashedTemplate.resolveValue(context: context, useOnlyLinks: useOnlyLinks) }() + switch result { + case let .success(value): return .success(.divStrokeStyleDashed(value)) + case let .partialSuccess(value, warnings): return .partialSuccess(.divStrokeStyleDashed(value), warnings: warnings) + case let .failure(errors): return .failure(errors) + case .noValue: return .noValue + } + } else { return nil } }() + return result ?? .failure(NonEmptyArray(.requiredFieldIsMissing(field: "type"))) + }() + } +} + +extension DivStrokeStyleTemplate { + public init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { + let receivedType = try dictionary.getField("type") as String + let blockType = templateToType[receivedType] ?? receivedType + switch blockType { + case DivStrokeStyleSolidTemplate.type: + self = .divStrokeStyleSolidTemplate(try DivStrokeStyleSolidTemplate(dictionary: dictionary, templateToType: templateToType)) + case DivStrokeStyleDashedTemplate.type: + self = .divStrokeStyleDashedTemplate(try DivStrokeStyleDashedTemplate(dictionary: dictionary, templateToType: templateToType)) + default: + throw DeserializationError.invalidFieldRepresentation(field: "div-stroke-style_template", representation: dictionary) + } + } +} diff --git a/DivKit/generated_sources/DivStrokeTemplate.swift b/DivKit/generated_sources/DivStrokeTemplate.swift index 447d3fe..6a33486 100644 --- a/DivKit/generated_sources/DivStrokeTemplate.swift +++ b/DivKit/generated_sources/DivStrokeTemplate.swift @@ -6,12 +6,14 @@ import VGSL public final class DivStrokeTemplate: TemplateValue, Sendable { public let color: Field>? + public let style: Field? // default value: .divStrokeStyleSolid(DivStrokeStyleSolid()) public let unit: Field>? // default value: dp public let width: Field>? // constraint: number >= 0; default value: 1 public convenience init(dictionary: [String: Any], templateToType: [TemplateName: String]) throws { self.init( color: dictionary.getOptionalExpressionField("color", transform: Color.color(withHexString:)), + style: dictionary.getOptionalField("style", templateToType: templateToType), unit: dictionary.getOptionalExpressionField("unit"), width: dictionary.getOptionalExpressionField("width") ) @@ -19,20 +21,24 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { init( color: Field>? = nil, + style: Field? = nil, unit: Field>? = nil, width: Field>? = nil ) { self.color = color + self.style = style self.unit = unit self.width = width } private static func resolveOnlyLinks(context: TemplatesContext, parent: DivStrokeTemplate?) -> DeserializationResult { let colorValue = { parent?.color?.resolveValue(context: context, transform: Color.color(withHexString:)) ?? .noValue }() + let styleValue = { parent?.style?.resolveOptionalValue(context: context, useOnlyLinks: true) ?? .noValue }() let unitValue = { parent?.unit?.resolveOptionalValue(context: context) ?? .noValue }() let widthValue = { parent?.width?.resolveOptionalValue(context: context, validator: ResolvedValue.widthValidator) ?? .noValue }() var errors = mergeErrors( colorValue.errorsOrWarnings?.map { .nestedObjectError(field: "color", error: $0) }, + styleValue.errorsOrWarnings?.map { .nestedObjectError(field: "style", error: $0) }, unitValue.errorsOrWarnings?.map { .nestedObjectError(field: "unit", error: $0) }, widthValue.errorsOrWarnings?.map { .nestedObjectError(field: "width", error: $0) } ) @@ -46,6 +52,7 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { } let result = DivStroke( color: { colorNonNil }(), + style: { styleValue.value }(), unit: { unitValue.value }(), width: { widthValue.value }() ) @@ -57,6 +64,7 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { return resolveOnlyLinks(context: context, parent: parent) } var colorValue: DeserializationResult> = { parent?.color?.value() ?? .noValue }() + var styleValue: DeserializationResult = .noValue var unitValue: DeserializationResult> = { parent?.unit?.value() ?? .noValue }() var widthValue: DeserializationResult> = { parent?.width?.value() ?? .noValue }() _ = { @@ -69,6 +77,11 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { colorValue = deserialize(__dictValue, transform: Color.color(withHexString:)).merged(with: colorValue) } }() + _ = { + if key == "style" { + styleValue = deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivStrokeStyleTemplate.self).merged(with: styleValue) + } + }() _ = { if key == "unit" { unitValue = deserialize(__dictValue).merged(with: unitValue) @@ -84,6 +97,11 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { colorValue = colorValue.merged(with: { deserialize(__dictValue, transform: Color.color(withHexString:)) }) } }() + _ = { + if key == parent?.style?.link { + styleValue = styleValue.merged(with: { deserialize(__dictValue, templates: context.templates, templateToType: context.templateToType, type: DivStrokeStyleTemplate.self) }) + } + }() _ = { if key == parent?.unit?.link { unitValue = unitValue.merged(with: { deserialize(__dictValue) }) @@ -96,8 +114,12 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { }() } }() + if let parent = parent { + _ = { styleValue = styleValue.merged(with: { parent.style?.resolveOptionalValue(context: context, useOnlyLinks: true) }) }() + } var errors = mergeErrors( colorValue.errorsOrWarnings?.map { .nestedObjectError(field: "color", error: $0) }, + styleValue.errorsOrWarnings?.map { .nestedObjectError(field: "style", error: $0) }, unitValue.errorsOrWarnings?.map { .nestedObjectError(field: "unit", error: $0) }, widthValue.errorsOrWarnings?.map { .nestedObjectError(field: "width", error: $0) } ) @@ -111,6 +133,7 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { } let result = DivStroke( color: { colorNonNil }(), + style: { styleValue.value }(), unit: { unitValue.value }(), width: { widthValue.value }() ) @@ -122,6 +145,13 @@ public final class DivStrokeTemplate: TemplateValue, Sendable { } public func resolveParent(templates: [TemplateName: Any]) throws -> DivStrokeTemplate { - return try mergedWithParent(templates: templates) + let merged = try mergedWithParent(templates: templates) + + return DivStrokeTemplate( + color: merged.color, + style: merged.style?.tryResolveParent(templates: templates), + unit: merged.unit, + width: merged.width + ) } } diff --git a/LayoutKit/LayoutKit/Blocks/Block+Debugging.swift b/LayoutKit/LayoutKit/Blocks/Block+Debugging.swift index c7f3cc0..0e4b268 100644 --- a/LayoutKit/LayoutKit/Blocks/Block+Debugging.swift +++ b/LayoutKit/LayoutKit/Blocks/Block+Debugging.swift @@ -473,7 +473,7 @@ extension BlockTooltip: CustomDebugStringConvertible { """ BlockTooltip { id: \(id) - duration: \(duration) + duration: \(params.duration) offset: \(offset.x) x \(offset.y) position: \(position.rawValue) block: \(block.debugDescription.indented()) diff --git a/LayoutKit/LayoutKit/Blocks/BlockBorder.swift b/LayoutKit/LayoutKit/Blocks/BlockBorder.swift index ccc7f44..007ab8a 100644 --- a/LayoutKit/LayoutKit/Blocks/BlockBorder.swift +++ b/LayoutKit/LayoutKit/Blocks/BlockBorder.swift @@ -3,13 +3,23 @@ import Foundation import VGSL public struct BlockBorder: Equatable { + public enum Style { + case solid + case dashed + + public static let `default` = BlockBorder.Style.solid + } + + public let style: Style public let color: Color public let width: CGFloat public init( + style: Style = .default, color: Color, width: CGFloat = 1 ) { + self.style = style self.color = color self.width = width } diff --git a/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift b/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift index 203e175..09baa63 100644 --- a/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift +++ b/LayoutKit/LayoutKit/Blocks/BlockTooltip.swift @@ -26,50 +26,39 @@ public struct BlockTooltip: Equatable { case nonModal } - public let id: String + public let params: BlockTooltipParams + public let block: Block - public let duration: TimeInterval public let offset: CGPoint public let position: Position public let useLegacyWidth: Bool public let tooltipViewFactory: TooltipViewFactory? - public let closeByTapOutside: Bool - public let tapOutsideActions: [UserInterfaceAction] - public let mode: Mode public init( - id: String, block: Block, - duration: TimeInterval, + params: BlockTooltipParams, offset: CGPoint, position: BlockTooltip.Position, useLegacyWidth: Bool = true, - tooltipViewFactory: TooltipViewFactory? = nil, - closeByTapOutside: Bool = true, - tapOutsideActions: [UserInterfaceAction] = [], - mode: Mode = .modal + tooltipViewFactory: TooltipViewFactory? = nil ) { - self.id = id self.block = block - self.duration = duration self.offset = offset self.position = position self.useLegacyWidth = useLegacyWidth self.tooltipViewFactory = tooltipViewFactory - self.closeByTapOutside = closeByTapOutside - self.tapOutsideActions = tapOutsideActions - self.mode = mode + self.params = params + } + + public var id: String { + params.id } public static func ==(lhs: BlockTooltip, rhs: BlockTooltip) -> Bool { - lhs.id == rhs.id && - lhs.duration == rhs.duration && + lhs.params == rhs.params && lhs.offset == rhs.offset && lhs.position == rhs.position && lhs.useLegacyWidth == rhs.useLegacyWidth && - lhs.block.equals(rhs.block) && - lhs.closeByTapOutside == rhs.closeByTapOutside && - lhs.tapOutsideActions == rhs.tapOutsideActions && - lhs.mode == rhs.mode + lhs.block.equals(rhs.block) } } diff --git a/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift b/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift new file mode 100644 index 0000000..48c60d0 --- /dev/null +++ b/LayoutKit/LayoutKit/Blocks/BlockTooltipParams.swift @@ -0,0 +1,23 @@ +import Foundation + +public struct BlockTooltipParams: Equatable { + public let id: String + public let mode: BlockTooltip.Mode + public let duration: TimeInterval + public let closeByTapOutside: Bool + let tapOutsideActions: [UserInterfaceAction] + + public init( + id: String, + mode: BlockTooltip.Mode, + duration: TimeInterval, + closeByTapOutside: Bool, + tapOutsideActions: [UserInterfaceAction] = [] + ) { + self.id = id + self.mode = mode + self.duration = duration + self.closeByTapOutside = closeByTapOutside + self.tapOutsideActions = tapOutsideActions + } +} diff --git a/LayoutKit/LayoutKit/Blocks/BoundaryTraitExtensions.swift b/LayoutKit/LayoutKit/Blocks/BoundaryTraitExtensions.swift index ef90ef0..31b8e2f 100644 --- a/LayoutKit/LayoutKit/Blocks/BoundaryTraitExtensions.swift +++ b/LayoutKit/LayoutKit/Blocks/BoundaryTraitExtensions.swift @@ -47,9 +47,11 @@ extension BoundaryTrait { guard let path = makeMaskPath(for: size, inset: border.width / 2) else { return nil } + let rawPattern = border.style.rawPattern let layer = CAShapeLayer() layer.path = path + layer.lineDashPattern = rawPattern?.calculateDashPattern(for: getLength(for: size)) layer.strokeColor = border.color.cgColor layer.fillColor = Color.clear.cgColor layer.lineWidth = border.width @@ -70,4 +72,38 @@ extension BoundaryTrait { nil } } + + private func getLength(for size: CGSize) -> CGFloat { + switch self { + case .noClip, .clipPath: (size.width + size.height) * 2 + case let .clipCorner(cornerRadius): + (size.width + size.height) * 2 + (.pi / 2 - 2) * cornerRadius.sum + } + } +} + +extension BlockBorder.Style { + fileprivate var rawPattern: [CGFloat]? { + switch self { + case .dashed: [6, 2] + case .solid: nil + } + } +} + +extension [CGFloat] { + fileprivate func calculateDashPattern(for length: CGFloat) -> [NSNumber] { + guard !isEmpty else { return [] } + let sum = reduce(0, +) + + let n = Swift.max(1, Int(round(length / sum))) + let factor = length / (CGFloat(n) * sum) + return map { NSNumber(value: $0 * factor) } + } +} + +extension CornerRadii { + fileprivate var sum: CGFloat { + topLeft + topRight + bottomLeft + bottomRight + } } diff --git a/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift b/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift index 20dd00a..e16ef01 100644 --- a/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift +++ b/LayoutKit/LayoutKit/Tooltips/TooltipManager.swift @@ -64,11 +64,7 @@ extension TooltipManager { public class DefaultTooltipManager: TooltipManager { public struct Tooltip { - public let id: String - public let duration: TimeInterval - public let closeByTapOutside: Bool - public let tapOutsideActions: [UserInterfaceAction] - public let isModal: Bool + public let params: BlockTooltipParams public let view: VisibleBoundsTrackingView } @@ -116,21 +112,18 @@ public class DefaultTooltipManager: TooltipManager { transform: { await $0?.makeTooltip(id: info.id, in: windowBounds) } ).first else { return } let view = TooltipContainerView( - tooltipView: tooltip.view, - closeByTapOutside: tooltip.closeByTapOutside, - tapOutsideActions: tooltip.tapOutsideActions, - isModal: tooltip.isModal, + tooltip: tooltip, handleAction: handleAction, onCloseAction: { [weak self] in guard let self else { return } - showingTooltips.removeValue(forKey: tooltip.id) + showingTooltips.removeValue(forKey: tooltip.params.id) if !showingTooltips.contains(where: { $1.isModal }) { modalTooltipWindow.isHidden = true } } ) - if tooltip.isModal { + if tooltip.params.mode == .modal { // Passing the statusBarStyle control to `rootViewController` of the main window let vc = ProxyViewController( viewController: UIApplication.shared.delegate?.window?? @@ -148,9 +141,10 @@ public class DefaultTooltipManager: TooltipManager { } showingTooltips[info.id] = view - if !tooltip.duration.isZero { - try await Task.sleep(nanoseconds: UInt64(tooltip.duration.nanoseconds)) - hideTooltip(id: tooltip.id) + let duration = tooltip.params.duration + if !duration.isZero { + try await Task.sleep(nanoseconds: UInt64(duration.nanoseconds)) + hideTooltip(id: tooltip.params.id) } } } @@ -227,11 +221,7 @@ extension TooltipAnchorView { ) return DefaultTooltipManager.Tooltip( - id: tooltip.id, - duration: tooltip.duration, - closeByTapOutside: tooltip.closeByTapOutside, - tapOutsideActions: tooltip.tapOutsideActions, - isModal: tooltip.mode == .modal, + params: tooltip.params, view: tooltipView ) } diff --git a/LayoutKit/LayoutKit/UI/Actions/TooltipEventPerforming.swift b/LayoutKit/LayoutKit/UI/Actions/TooltipEventPerforming.swift index 23bcc59..bdb36ef 100644 --- a/LayoutKit/LayoutKit/UI/Actions/TooltipEventPerforming.swift +++ b/LayoutKit/LayoutKit/UI/Actions/TooltipEventPerforming.swift @@ -6,22 +6,21 @@ public protocol TooltipEventPerforming { } public final class TooltipEvent: AppActionEventProtocol { - public let tooltipID: String public let tooltipView: VisibleBoundsTrackingView - public let duration: TimeInterval - public let showsOnStart: Bool - public let multiple: Bool + public private(set) weak var tooltipAnchorView: ViewType? + public let info: TooltipInfo + public let params: BlockTooltipParams public init( info: TooltipInfo, + params: BlockTooltipParams, tooltipView: VisibleBoundsTrackingView, - duration: TimeInterval + tooltipAnchorView: ViewType? ) { - self.tooltipID = info.id self.tooltipView = tooltipView - self.duration = duration - self.showsOnStart = info.showsOnStart - self.multiple = info.multiple + self.tooltipAnchorView = tooltipAnchorView + self.info = info + self.params = params } public func makeHandler(responder: UIResponder) -> Handler? { diff --git a/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift b/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift index 38b1593..44e2783 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/DecoratingBlock+UIViewRenderableBlock.swift @@ -317,7 +317,8 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT let shouldMakeBorderLayer: Bool let boundary = model.boundary.makeInfo(for: bounds.size) - shouldMakeBorderLayer = boundary.layer != nil + shouldMakeBorderLayer = boundary.layer != nil || model.border?.style + .shouldMakeBorderLayer == true layer.cornerRadius = boundary.radius layer.maskedCorners = boundary.corners layer.mask = boundary.layer @@ -515,8 +516,9 @@ private final class DecoratingView: UIControl, BlockViewProtocol, VisibleBoundsT ) return TooltipEvent( info: info, + params: tooltipModel.params, tooltipView: tooltipView, - duration: tooltipModel.duration + tooltipAnchorView: self ) } @@ -590,3 +592,12 @@ extension BlurEffect { } } } + +extension BlockBorder.Style { + fileprivate var shouldMakeBorderLayer: Bool { + switch self { + case .dashed: true + case .solid: false + } + } +} diff --git a/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift b/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift index f1212d6..9b41d6d 100644 --- a/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift +++ b/LayoutKit/LayoutKit/UI/Blocks/TooltipContainerView.swift @@ -3,10 +3,7 @@ import UIKit import VGSL public final class TooltipContainerView: UIView, UIActionEventPerforming { - let isModal: Bool - private let tooltipView: VisibleBoundsTrackingView - private let closeByTapOutside: Bool - private let tapOutsideActions: [UserInterfaceAction] + private let tooltip: DefaultTooltipManager.Tooltip private let handleAction: (LayoutKit.UIActionEvent) -> Void private let onCloseAction: Action @@ -15,19 +12,14 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { private var onVisibleBoundsChanged: Action? public init( - tooltipView: VisibleBoundsTrackingView, - closeByTapOutside: Bool, - tapOutsideActions: [UserInterfaceAction], - isModal: Bool, + tooltip: DefaultTooltipManager.Tooltip, handleAction: @escaping (LayoutKit.UIActionEvent) -> Void, onCloseAction: @escaping Action ) { - self.tooltipView = tooltipView - self.closeByTapOutside = closeByTapOutside - self.tapOutsideActions = tapOutsideActions - self.isModal = isModal + self.tooltip = tooltip self.handleAction = handleAction self.onCloseAction = onCloseAction + let tooltipView = tooltip.view let tooltipBounds = tooltipView.bounds onVisibleBoundsChanged = { [weak tooltipView] in tooltipView?.onVisibleBoundsChanged(from: .zero, to: tooltipBounds) @@ -40,7 +32,7 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { addGestureRecognizer(tapRecognizer) } - addSubview(tooltipView) + addSubview(tooltip.view) } @available(*, unavailable) @@ -48,6 +40,10 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { fatalError() } + var isModal: Bool { + tooltip.params.mode == .modal + } + @objc private func handleTap(_ sender: UITapGestureRecognizer) { let point = sender.location(in: self) if !isPointInsideTooltip(point) { @@ -57,7 +53,7 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isPointInsideTooltip(point, event: event), !isModal { - if closeByTapOutside { + if tooltip.params.closeByTapOutside { DispatchQueue.main.async { self.close() } @@ -91,24 +87,24 @@ public final class TooltipContainerView: UIView, UIActionEventPerforming { public func close() { guard !isClosing else { return } isClosing = true - self.tooltipView.onVisibleBoundsChanged(from: tooltipView.bounds, to: .zero) + tooltip.view.onVisibleBoundsChanged(from: tooltip.view.bounds, to: .zero) removeFromParentAnimated(completion: { self.onCloseAction() }) } private func performTapOutsideActions() { - let uiActionEvents = tapOutsideActions.map { + let uiActionEvents = tooltip.params.tapOutsideActions.map { UIActionEvent(uiAction: $0, originalSender: self) } perform(uiActionEvents: uiActionEvents, from: self) - if closeByTapOutside { + if tooltip.params.closeByTapOutside { close() } } private func isPointInsideTooltip(_ point: CGPoint, event: UIEvent? = nil) -> Bool { - tooltipView.point(inside: tooltipView.convert(point, from: self), with: event) + tooltip.view.point(inside: tooltip.view.convert(point, from: self), with: event) } } diff --git a/LayoutKit/LayoutKit/ViewModels/TooltipInfo.swift b/LayoutKit/LayoutKit/ViewModels/TooltipInfo.swift index 8a8f77e..b81d441 100644 --- a/LayoutKit/LayoutKit/ViewModels/TooltipInfo.swift +++ b/LayoutKit/LayoutKit/ViewModels/TooltipInfo.swift @@ -1,9 +1,9 @@ import Foundation public struct TooltipInfo: Equatable { - let id: String - let showsOnStart: Bool - let multiple: Bool + public let id: String + public let showsOnStart: Bool + public let multiple: Bool public init(id: String, showsOnStart: Bool, multiple: Bool) { self.id = id diff --git a/Specs/DivKit/31.4.0/DivKit.podspec b/Specs/DivKit/31.4.0/DivKit.podspec new file mode 100644 index 0000000..4620cc7 --- /dev/null +++ b/Specs/DivKit/31.4.0/DivKit.podspec @@ -0,0 +1,24 @@ +Pod::Spec.new do |s| + s.name = 'DivKit' + s.version = '31.4.0' + s.summary = 'DivKit framework' + s.description = 'DivKit is a backend-driven UI framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'DivKit_LayoutKit', s.version.to_s + s.dependency 'DivKit_Serialization', s.version.to_s + s.dependency 'VGSL', '~> 6.18' + + s.source_files = [ + 'DivKit/**/*' + ] +end diff --git a/Specs/DivKitExtensions/31.4.0/DivKitExtensions.podspec b/Specs/DivKitExtensions/31.4.0/DivKitExtensions.podspec new file mode 100644 index 0000000..c093d33 --- /dev/null +++ b/Specs/DivKitExtensions/31.4.0/DivKitExtensions.podspec @@ -0,0 +1,22 @@ +Pod::Spec.new do |s| + s.name = 'DivKitExtensions' + s.version = '31.4.0' + s.summary = 'DivKit framework extensions' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'DivKit', s.version.to_s + + s.source_files = [ + 'DivKitExtensions/**/*' + ] +end diff --git a/Specs/DivKit_LayoutKit/31.4.0/DivKit_LayoutKit.podspec b/Specs/DivKit_LayoutKit/31.4.0/DivKit_LayoutKit.podspec new file mode 100644 index 0000000..ccfac9e --- /dev/null +++ b/Specs/DivKit_LayoutKit/31.4.0/DivKit_LayoutKit.podspec @@ -0,0 +1,24 @@ +Pod::Spec.new do |s| + s.name = 'DivKit_LayoutKit' + s.module_name = 'LayoutKit' + s.version = '31.4.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'DivKit_LayoutKitInterface', s.version.to_s + s.dependency 'VGSL', '~> 6.18' + + s.source_files = [ + 'LayoutKit/LayoutKit/**/*' + ] +end diff --git a/Specs/DivKit_LayoutKitInterface/31.4.0/DivKit_LayoutKitInterface.podspec b/Specs/DivKit_LayoutKitInterface/31.4.0/DivKit_LayoutKitInterface.podspec new file mode 100644 index 0000000..6a5bfc7 --- /dev/null +++ b/Specs/DivKit_LayoutKitInterface/31.4.0/DivKit_LayoutKitInterface.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'DivKit_LayoutKitInterface' + s.module_name = 'LayoutKitInterface' + s.version = '31.4.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'VGSL', '~> 6.18' + + s.source_files = [ + 'LayoutKit/Interface/**/*' + ] +end diff --git a/Specs/DivKit_Serialization/31.4.0/DivKit_Serialization.podspec b/Specs/DivKit_Serialization/31.4.0/DivKit_Serialization.podspec new file mode 100644 index 0000000..2729f82 --- /dev/null +++ b/Specs/DivKit_Serialization/31.4.0/DivKit_Serialization.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'DivKit_Serialization' + s.module_name = 'Serialization' + s.version = '31.4.0' + s.summary = 'Part of DivKit framework' + s.description = 'Part of DivKit framework' + s.homepage = 'https://divkit.tech' + + s.license = { :type => 'Apache License, Version 2.0', :file => 'LICENSE' } + s.author = { 'divkit' => 'divkit@yandex-team.ru' } + s.source = { :git => 'https://github.com/divkit/divkit-ios.git', :tag => s.version.to_s } + + s.swift_version = '5.9' + s.requires_arc = true + s.prefix_header_file = false + s.platforms = { :ios => '13.0' } + + s.dependency 'VGSL', '~> 6.18' + + s.source_files = [ + 'Serialization/**/*' + ] +end