Files
divkit/client/ios/DivKit/Extensions/DivTextExtensions.swift
T
babaevmm 6797545374 update paths
commit_hash:8ab26a6d1a605474126fdbc3cc4314923edbc77f
2025-03-19 17:28:58 +03:00

408 lines
13 KiB
Swift

import CoreFoundation
import Foundation
import LayoutKit
import VGSL
extension DivText: DivBlockModeling {
public func makeBlock(context: DivBlockModelingContext) throws -> Block {
let context = modifiedContextParentPath(context)
let expressionResolver = context.expressionResolver
let lazyText = Lazy(getter: { [unowned self] in
resolveText(expressionResolver) ?? ""
})
return try applyBaseProperties(
to: { try makeBaseBlock(context: context, text: lazyText) },
context: context,
actionsHolder: self,
customAccessibilityParams: CustomAccessibilityParams(
defaultTraits: .staticText
) { [unowned self] in
accessibility?.resolveDescription(expressionResolver) ?? lazyText.value
}
)
}
private func makeBaseBlock(
context: DivBlockModelingContext,
text: Lazy<String>
) throws -> Block {
let expressionResolver = context.expressionResolver
let fontParams = resolveFontParams(expressionResolver)
var typo = Typo(font: context.fontProvider.font(fontParams)).allowHeightOverrun
let alignment = resolveTextAlignmentHorizontal(expressionResolver)
.makeTextAlignment(uiLayoutDirection: context.layoutDirection)
if alignment != .left {
typo = typo.with(alignment: alignment)
}
let kern = CGFloat(resolveLetterSpacing(expressionResolver))
if !kern.isApproximatelyEqualTo(0) {
typo = typo.kerned(kern)
}
let resolvedColor: Color = resolveTextColor(expressionResolver)
if resolvedColor != .black {
typo = typo.with(color: resolvedColor)
}
if let lineHeight = resolveLineHeight(expressionResolver) {
typo = typo.with(height: CGFloat(lineHeight))
}
switch resolveStrike(expressionResolver) {
case .none: break
case .single: typo = typo.struckThrough(.single)
}
switch resolveUnderline(expressionResolver) {
case .none: break
case .single: typo = typo.underlined(.single)
}
if let textShadow = textShadow?.resolve(expressionResolver) {
typo = typo.shaded(textShadow.typoShadow)
}
let attributedString = makeAttributedString(
text: text.value as CFString,
typo: typo,
ranges: ranges,
actions: nil,
context: context,
fontParams: fontParams
)
let images = makeInlineImages(
context: context,
images: self.images,
text: attributedString
)
let truncationToken = ellipsis.map {
makeAttributedString(
text: ($0.resolveText(expressionResolver) ?? "") as CFString,
typo: typo,
ranges: $0.ranges,
actions: $0.actions?.uiActions(context: context),
context: context,
fontParams: fontParams
)
}
let truncationImages = makeInlineImages(
context: context,
images: ellipsis?.images,
text: truncationToken
)
if let id {
context.blockStateStorage.setState(
id: id,
cardId: context.cardId,
state: TextBlockViewState(text: attributedString.string)
)
}
let additionalTextInsets = additionalTextInsets(context: context)
return TextBlock(
widthTrait: resolveContentWidthTrait(context),
heightTrait: resolveContentHeightTrait(context),
text: attributedString,
textGradient: resolveGradient(context),
verticalAlignment: resolveTextAlignmentVertical(expressionResolver).alignment,
maxIntrinsicNumberOfLines: resolveMaxLines(expressionResolver) ?? .max,
minNumberOfHiddenLines: resolveMinHiddenLines(expressionResolver) ?? 0,
images: images,
accessibilityElement: nil,
truncationToken: truncationToken,
truncationImages: truncationImages,
additionalTextInsets: additionalTextInsets,
canSelect: resolveSelectable(expressionResolver),
tightenWidth: resolveTightenWidth(expressionResolver),
autoEllipsize: resolveAutoEllipsize(expressionResolver)
?? context.flagsInfo.defaultTextAutoEllipsize,
path: context.path
)
}
private func makeAttributedString(
text: CFString,
typo: Typo,
ranges: [Range]?,
actions: [UserInterfaceAction]?,
context: DivBlockModelingContext,
fontParams: FontParams
) -> NSAttributedString {
let length = CFStringGetLength(text)
let attributedString = CFAttributedStringCreateMutable(kCFAllocatorDefault, length)!
CFAttributedStringBeginEditing(attributedString)
CFAttributedStringReplaceString(attributedString, CFRange(), text)
let fullRange = CFRange(location: 0, length: length)
typo.apply(to: attributedString, at: fullRange)
ranges?.forEach { apply($0, to: attributedString, context: context, fontParams: fontParams) }
attributedString.apply(actions, at: fullRange)
CFAttributedStringEndEditing(attributedString)
return attributedString
}
private func makeInlineImages(
context: DivBlockModelingContext,
images: [Image]?,
text: NSAttributedString?
) -> [TextBlock.InlineImage] {
guard let text else {
return []
}
return (images ?? []).compactMap {
$0.makeImage(
context: context,
textLength: CFAttributedStringGetLength(text)
)
}
}
private func apply(
_ range: DivText.Range,
to string: CFMutableAttributedString,
context: DivBlockModelingContext,
fontParams: FontParams
) {
let expressionResolver = context.expressionResolver
let stringLength = CFAttributedStringGetLength(string)
let start = range.resolveStart(expressionResolver)
let end = range.resolveEnd(expressionResolver) ?? stringLength
guard end > start, start < stringLength else {
return
}
let rangeFontParams = FontParams(
family: range.resolveFontFamily(expressionResolver) ?? fontParams.family,
weight: range.resolveFontWeightValue(expressionResolver)
?? range.resolveFontWeight(expressionResolver)?.toInt()
?? fontParams.weight,
size: range.resolveFontSize(expressionResolver) ?? fontParams.size,
unit: range.resolveFontSizeUnit(expressionResolver),
featureSettings: range.resolveFontFeatureSettings(expressionResolver)
?? fontParams.featureSettings
)
let fontTypo = rangeFontParams == fontParams
? nil
: Typo(font: context.fontProvider.font(rangeFontParams))
let colorTypo = range.resolveTextColor(expressionResolver)
.map { Typo(color: $0) }
let heightTypo = range.resolveLineHeight(expressionResolver)
.map { Typo(height: CGFloat($0)) }
let spacingTypo = range.resolveLetterSpacing(expressionResolver)
.map { Typo(kern: CGFloat($0)) }
let strikethroughTypo = range.resolveStrike(expressionResolver)
.map { Typo(strikethrough: $0.underlineStyle) }
let underlineTypo = range.resolveUnderline(expressionResolver)
.map { Typo(underline: $0.underlineStyle) }
let baselineOffsetTypo = Typo(
baselineOffset: range.resolveBaselineOffset(expressionResolver)
)
let blockShadow = range.textShadow?.resolve(expressionResolver).typoShadow
let shadowTypo = blockShadow.map { Typo(shadow: $0) }
let typos = [
fontTypo,
colorTypo,
heightTypo,
spacingTypo,
strikethroughTypo,
underlineTypo,
baselineOffsetTypo,
shadowTypo,
].compactMap { $0 }
let actions = range.actions?.uiActions(context: context)
if typos.isEmpty, actions == nil, range.background == nil, range.border == nil {
return
}
let actualEnd = min(end, stringLength)
let cfRange = CFRange(location: start, length: actualEnd - start)
typos.forEach { $0.apply(to: string, at: cfRange) }
string.apply(actions, at: cfRange)
[
range.makeBackground(context: context, range: cfRange),
range.makeBorder(range: cfRange, resolver: expressionResolver),
].forEach {
$0?.apply(to: string, at: cfRange)
}
}
private func resolveGradient(_ context: DivBlockModelingContext) -> Gradient? {
let expressionResolver = context.expressionResolver
guard let textGradient else {
return nil
}
switch textGradient {
case let .divLinearGradient(gradient):
return gradient.makeBlockLinearGradient(context).map { .linear($0) }
case let .divRadialGradient(gradient):
return Gradient.Radial(
colors: gradient.resolveColors(expressionResolver) ?? [],
end: gradient.resolveRadius(expressionResolver),
centerX: gradient.resolveCenterX(expressionResolver),
centerY: gradient.resolveCenterY(expressionResolver)
).map { .radial($0) }
}
}
private func additionalTextInsets(
context: DivBlockModelingContext
) -> EdgeInsets {
var additionalInsets: EdgeInsets = .zero
ranges?.forEach {
if case let .divCloudBackground(background) = $0.background {
let backgroundInsets = background.paddings?.resolve(context) ?? .zero
additionalInsets = EdgeInsets(
top: max(additionalInsets.top, backgroundInsets.top),
left: max(additionalInsets.left, backgroundInsets.left),
bottom: max(additionalInsets.bottom, backgroundInsets.bottom),
right: max(additionalInsets.right, backgroundInsets.right)
)
}
}
return additionalInsets
}
}
extension DivText: FontParamsProvider {}
extension CFMutableAttributedString {
fileprivate func apply(
_ actions: [UserInterfaceAction]?,
at range: CFRange
) {
if let actions {
ActionsAttribute(actions: actions).apply(to: self, at: range)
}
}
}
extension DivAlignmentHorizontal {
fileprivate func makeTextAlignment(
uiLayoutDirection: UserInterfaceLayoutDirection =
.system
) -> TextAlignment {
switch self {
case .start:
uiLayoutDirection == .leftToRight ? .left : .right
case .end:
uiLayoutDirection == .leftToRight ? .right : .left
case .left:
.left
case .center:
.center
case .right:
.right
}
}
}
extension URLQueryItem {
init(name: String, value: DivCardID?) {
self.init(name: name, value: value?.rawValue)
}
}
extension DivText.Image {
fileprivate func makeImage(
context: DivBlockModelingContext,
textLength: Int
) -> 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:
start
case .reversed:
textLength - start
}
return TextBlock.InlineImage(
size: CGSize(
width: CGFloat(width.resolveValue(expressionResolver) ?? 0),
height: CGFloat(height.resolveValue(expressionResolver) ?? 0)
),
holder: context.imageHolderFactory.make(resolveUrl(expressionResolver)),
location: location,
tintColor: resolveTintColor(expressionResolver),
tintMode: resolveTintMode(expressionResolver).tintMode
)
}
}
extension DivText.Range {
fileprivate func makeBorder(
range: CFRange,
resolver: ExpressionResolver
) -> BorderAttribute? {
guard let border else { return nil }
let color = border.stroke?.resolveColor(resolver)
let width = border.stroke?.resolveWidth(resolver)
let cornerRadius = border.resolveCornerRadius(resolver)
return BorderAttribute(
color: color?.cgColor,
width: width.flatMap(CGFloat.init),
cornerRadius:
cornerRadius.flatMap(CGFloat.init),
range: range
)
}
fileprivate func makeBackground(
context: DivBlockModelingContext,
range: CFRange
) -> StringAttribute? {
guard let background else { return nil }
let resolver = context.expressionResolver
switch background {
case let .divSolidBackground(solid):
let color = solid.resolveColor(resolver) ?? .clear
return BackgroundAttribute(color: color.cgColor, range: range)
case let .divCloudBackground(cloud):
guard let color = cloud.resolveColor(resolver),
let cornerRadius = cloud.resolveCornerRadius(resolver)
else { return nil }
let insets = cloud.paddings?.resolve(context)
return CloudBackgroundAttribute(
color: color,
cornerRadius: CGFloat(cornerRadius),
range: range.location..<(range.location + range.length),
insets: insets
)
}
}
}
extension DivLineStyle {
fileprivate var underlineStyle: UnderlineStyle {
switch self {
case .none: []
case .single: .single
}
}
}
extension BlockShadow {
fileprivate var typoShadow: Shadow {
Shadow(
offset: CGSize(width: offset.x, height: -offset.y),
blurRadius: blurRadius,
color: color.withAlphaComponent(CGFloat(opacity))
)
}
}