Files
divkit/client/ios/DivKit/Extensions/DivTextExtensions.swift
T
morevsavva 3ed0abbc30 add background for text ranges
Добавил возможность задавать фоны в ренджах.
2022-11-21 13:54:44 +03:00

313 lines
10 KiB
Swift

import CoreFoundation
import CoreGraphics
import Foundation
import Base
import BaseUI
import LayoutKit
import Networking
extension DivText: DivBlockModeling {
public func makeBlock(context: DivBlockModelingContext) throws -> Block {
try applyBaseProperties(
to: { try makeBaseBlock(context: context) },
context: context,
actions: makeActions(context: context.actionContext),
actionAnimation: actionAnimation.makeActionAnimation(with: context.expressionResolver),
doubleTapActions: makeDoubleTapActions(context: context.actionContext),
longTapActions: makeLongTapActions(context: context.actionContext),
customA11yElement: makeCustomA11yElement(with: context.expressionResolver)
)
}
private func makeBaseBlock(context: DivBlockModelingContext) throws -> Block {
let font = context.fontSpecifiers.font(
family: resolveFontFamily(context.expressionResolver).fontFamily,
weight: resolveFontWeight(context.expressionResolver).fontWeight,
size: resolveFontSizeUnit(context.expressionResolver)
.makeScaledValue(resolveFontSize(context.expressionResolver))
)
var typo = Typo(font: font).allowHeightOverrun
let alignment = resolveTextAlignmentHorizontal(context.expressionResolver).system
if alignment != .left {
typo = typo.with(alignment: alignment)
}
let kern = CGFloat(resolveLetterSpacing(context.expressionResolver))
if !kern.isApproximatelyEqualTo(0) {
typo = typo.kerned(kern)
}
let resolvedColor: Color = resolveTextColor(context.expressionResolver)
if resolvedColor != .black {
typo = typo.with(color: resolvedColor)
}
if let lineHeight = resolveLineHeight(context.expressionResolver) {
typo = typo.with(height: CGFloat(lineHeight))
}
switch resolveStrike(context.expressionResolver) {
case .none: break
case .single: typo = typo.struckThrough(.single)
}
switch resolveUnderline(context.expressionResolver) {
case .none: break
case .single: typo = typo.underlined(.single)
}
let attributedString = makeAttributedString(
text: resolveText(context.expressionResolver) ?? ("" as CFString),
typo: typo,
ranges: ranges,
actions: nil,
context: context
)
let images = (self.images ?? [])
.filter {
let start = $0.resolveStart(context.expressionResolver) ?? 0
return start <= CFAttributedStringGetLength(attributedString)
}
.map { $0.makeImage(
withFactory: context.imageHolderFactory,
expressionResolver: context.expressionResolver
) }
let truncationToken = ellipsis.map {
makeAttributedString(
text: ($0.resolveText(context.expressionResolver) ?? "") as CFString,
typo: typo,
ranges: $0.ranges,
actions: $0.makeActions(context: context.actionContext),
context: context
)
}
return TextBlock(
widthTrait: makeContentWidthTrait(with: context),
heightTrait: makeContentHeightTrait(with: context),
text: attributedString,
textGradient: makeGradient(context.expressionResolver),
verticalAlignment:
resolveTextAlignmentVertical(context.expressionResolver).alignment,
maxIntrinsicNumberOfLines: resolveMaxLines(context.expressionResolver) ?? .max,
minNumberOfHiddenLines: resolveMinHiddenLines(context.expressionResolver) ?? 0,
images: images,
truncationToken: truncationToken,
canSelect: resolveSelectable(context.expressionResolver) && context.flagsInfo
.isTextSelectingEnabled
)
}
private func makeAttributedString(
text: CFString,
typo: Typo,
ranges: [Range]?,
actions: NonEmptyArray<UserInterfaceAction>?,
context: DivBlockModelingContext
) -> 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) }
attributedString.apply(actions, at: fullRange)
CFAttributedStringEndEditing(attributedString)
return attributedString
}
private func apply(
_ range: DivText.Range,
to string: CFMutableAttributedString,
context: DivBlockModelingContext
) {
let start = range.resolveStart(context.expressionResolver) ?? 0
let end = range.resolveEnd(context.expressionResolver) ?? 0
guard end > start && start < CFAttributedStringGetLength(string) else {
return
}
let fontTypo: Typo?
if range.fontSize != nil || range.fontWeight != nil {
let font = context.fontSpecifiers.font(
family: range.resolveFontFamily(context.expressionResolver)?.fontFamily
?? resolveFontFamily(context.expressionResolver).fontFamily,
weight: (
range.resolveFontWeight(context.expressionResolver)
?? resolveFontWeight(context.expressionResolver)
).fontWeight,
size: CGFloat(
range.resolveFontSize(context.expressionResolver)
?? resolveFontSize(context.expressionResolver)
)
)
fontTypo = Typo(font: font)
} else {
fontTypo = nil
}
let colorTypo = range.resolveTextColor(context.expressionResolver)
.map { Typo(color: $0) }
let heightTypo = range.resolveLineHeight(context.expressionResolver)
.map { Typo(height: CGFloat($0)) }
let spacingTypo = range.resolveLetterSpacing(context.expressionResolver)
.map { Typo(kern: CGFloat($0)) }
let strikethroughTypo = range.resolveStrike(context.expressionResolver)
.map { Typo(strikethrough: $0.underlineStyle) }
let underlineTypo = range.resolveUnderline(context.expressionResolver)
.map { Typo(underline: $0.underlineStyle) }
let typos = [fontTypo, colorTypo, heightTypo, spacingTypo, strikethroughTypo, underlineTypo]
.compactMap { $0 }
let actions = range.makeActions(context: context.actionContext)
if typos.isEmpty, actions == nil, range.background == nil, range.border == nil {
return
}
let actualEnd = min(end, CFAttributedStringGetLength(string))
let cfRange = CFRange(location: start, length: actualEnd - start)
typos.forEach { $0.apply(to: string, at: cfRange) }
string.apply(actions, at: cfRange)
range.makeBackground(range: cfRange, resolver: context.expressionResolver)?
.apply(to: string, at: cfRange)
range.makeBorder(range: cfRange, resolver: context.expressionResolver)?
.apply(to: string, at: cfRange)
}
private func makeGradient(_ expressionResolver: ExpressionResolver) -> Gradient? {
guard let textGradient = textGradient else {
return nil
}
switch textGradient {
case let .divLinearGradient(gradient):
return
.linear(
Gradient.Linear(
colors: gradient.resolveColors(expressionResolver) ?? [],
angle: gradient.resolveAngle(expressionResolver)
)
)
case let .divRadialGradient(gradient):
return
.radial(
Gradient.Radial(
colors: gradient.resolveColors(expressionResolver) ?? [],
end: gradient.resolveRadius(expressionResolver),
centerX: gradient.resolveCenterX(expressionResolver),
centerY: gradient.resolveCenterY(expressionResolver)
)
)
}
}
private func makeCustomA11yElement(with expressionResolver: ExpressionResolver)
-> AccessibilityElement? {
guard accessibility.resolveDescription(expressionResolver) == nil else {
return nil
}
return AccessibilityElement(
traits: accessibility.type?.cast() ?? .none,
strings: AccessibilityElement.Strings(
label: resolveText(expressionResolver) as String?,
hint: accessibility.resolveHint(expressionResolver),
value: accessibility.resolveStateDescription(expressionResolver),
identifier: id
),
hideElementWithChildren: accessibility.resolveMode(expressionResolver).isExclude
)
}
}
extension CFMutableAttributedString {
fileprivate func apply(
_ actions: NonEmptyArray<UserInterfaceAction>?,
at range: CFRange
) {
if let actions = actions {
ActionsAttribute(actions: actions.asArray()).apply(to: self, at: range)
}
}
}
extension DivAlignmentHorizontal {
fileprivate var system: TextAlignment {
switch self {
case .left:
return .left
case .center:
return .center
case .right:
return .right
}
}
}
extension URLQueryItem {
init(name: String, value: DivCardID?) {
self.init(name: name, value: value?.rawValue)
}
}
extension DivText.Image {
fileprivate func makeImage(
withFactory factory: ImageHolderFactory,
expressionResolver: ExpressionResolver
) -> TextBlock.InlineImage {
TextBlock.InlineImage(
size: CGSize(
width: CGFloat(width.resolveValue(expressionResolver) ?? 0),
height: CGFloat(height.resolveValue(expressionResolver) ?? 0)
),
holder: factory.make(resolveUrl(expressionResolver)),
location: resolveStart(expressionResolver) ?? 0,
tintColor: resolveTintColor(expressionResolver)
)
}
}
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(
range: CFRange,
resolver: ExpressionResolver
) -> BackgroundAttribute? {
guard let background else { return nil }
switch background {
case let .divSolidBackground(solid):
let color = solid.resolveColor(resolver) ?? .clear
return BackgroundAttribute(color: color.cgColor, range: range)
}
}
}
extension DivLineStyle {
fileprivate var underlineStyle: UnderlineStyle {
switch self {
case .none: return []
case .single: return .single
}
}
}