Swiftgram/TelegramUI/InstantPageTextItem.swift
2017-08-15 14:44:14 +03:00

337 lines
13 KiB
Swift

import Foundation
import TelegramCore
struct InstantPageTextUrlItem {
let frame: CGRect
let item: AnyObject
}
struct InstantPageTextStrikethroughItem {
let frame: CGRect
}
final class InstantPageTextLine {
let line: CTLine
let frame: CGRect
let urlItems: [InstantPageTextUrlItem]
let strikethroughItems: [InstantPageTextStrikethroughItem]
init(line: CTLine, frame: CGRect, urlItems: [InstantPageTextUrlItem], strikethroughItems: [InstantPageTextStrikethroughItem]) {
self.line = line
self.frame = frame
self.urlItems = urlItems
self.strikethroughItems = strikethroughItems
}
}
final class InstantPageTextItem: InstantPageItem {
let lines: [InstantPageTextLine]
let rtlLineIndices: Set<Int>
let hasLinks: Bool
var frame: CGRect
var alignment: NSTextAlignment = .left
let medias: [InstantPageMedia] = []
let wantsNode: Bool = false
var containsRTL: Bool {
return !self.rtlLineIndices.isEmpty
}
init(frame: CGRect, lines: [InstantPageTextLine]) {
self.frame = frame
self.lines = lines
var hasLinks = false
var index = 0
var rtlLineIndices = Set<Int>()
for line in lines {
if !line.urlItems.isEmpty {
hasLinks = true
}
let glyphRuns = CTLineGetGlyphRuns(line.line) as NSArray
if glyphRuns.count != 0 {
inner: for i in 0 ..< glyphRuns.count {
let runStatus = CTRunGetStatus(glyphRuns[i] as! CTRun)
if runStatus.contains(.rightToLeft) {
rtlLineIndices.insert(index)
break inner
}
}
}
index += 1
}
self.hasLinks = hasLinks
self.rtlLineIndices = rtlLineIndices
}
func drawInTile(context: CGContext) {
context.saveGState()
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
context.translateBy(x: self.frame.minX, y: self.frame.minY)
let clipRect = context.boundingBoxOfClipPath
let upperOriginBound = clipRect.minY - 10.0
let lowerOriginBound = clipRect.maxY + 10.0
let boundsWidth = self.frame.size.width
for i in 0 ..< self.lines.count {
let line = self.lines[i]
let lineFrame = line.frame
if lineFrame.maxY < upperOriginBound || lineFrame.minY > lowerOriginBound {
continue
}
var lineOrigin = lineFrame.origin
if self.alignment == .center {
lineOrigin.x = floor((boundsWidth - lineFrame.size.width) / 2.0)
} else if self.alignment == .right {
lineOrigin.x = boundsWidth - lineFrame.size.width
} else if self.alignment == .natural && self.rtlLineIndices.contains(i) {
lineOrigin.x = boundsWidth - lineFrame.size.width
}
context.textPosition = CGPoint(x: lineOrigin.x, y: lineOrigin.y + lineFrame.size.height)
CTLineDraw(line.line, context)
if !line.strikethroughItems.isEmpty {
for item in line.strikethroughItems {
context.fill(CGRect(x: item.frame.minX, y: item.frame.minY + floor((lineFrame.size.height / 2.0) + 1.0), width: item.frame.size.width, height: 1.0))
}
}
}
context.restoreGState()
}
func linkSelectionViews() -> [InstantPageLinkSelectionView] {
return []
}
func matchesAnchor(_ anchor: String) -> Bool {
return false
}
func node(account: Account) -> InstantPageNode? {
return nil
}
func matchesNode(_ node: InstantPageNode) -> Bool {
return false
}
func distanceThresholdGroup() -> Int? {
return nil
}
func distanceThresholdWithGroupCount(_ count: Int) -> CGFloat {
return 0.0
}
}
/*
static TGInstantPageLinkSelectionView *selectionViewFromFrames(NSArray<NSValue *> *frames, CGPoint origin, id urlItem) {
CGRect frame = CGRectMake(0.0f, 0.0f, 0.0f, 0.0f);
bool first = true;
for (NSValue *rectValue in frames) {
CGRect rect = [rectValue CGRectValue];
if (first) {
first = false;
frame = rect;
} else {
frame = CGRectUnion(rect, frame);
}
}
NSMutableArray *adjustedFrames = [[NSMutableArray alloc] init];
for (NSValue *rectValue in frames) {
CGRect rect = [rectValue CGRectValue];
rect.origin.x -= frame.origin.x;
rect.origin.y -= frame.origin.y;
[adjustedFrames addObject:[NSValue valueWithCGRect:rect]];
}
return [[TGInstantPageLinkSelectionView alloc] initWithFrame:CGRectOffset(frame, origin.x, origin.y) rects:adjustedFrames urlItem:urlItem];
}
- (NSArray<TGInstantPageLinkSelectionView *> *)linkSelectionViews {
if (_hasLinks) {
NSMutableArray<TGInstantPageLinkSelectionView *> *views = [[NSMutableArray alloc] init];
NSMutableArray<NSValue *> *currentLinkFrames = [[NSMutableArray alloc] init];
id currentUrlItem = nil;
for (TGInstantPageTextLine *line in _lines) {
if (line.urlItems != nil) {
for (TGInstantPageTextUrlItem *urlItem in line.urlItems) {
if (currentUrlItem == urlItem.item) {
} else {
if (currentLinkFrames.count != 0) {
[views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)];
}
[currentLinkFrames removeAllObjects];
currentUrlItem = urlItem.item;
}
CGPoint lineOrigin = line.frame.origin;
if (_alignment == NSTextAlignmentCenter) {
lineOrigin.x = CGFloor((self.frame.size.width - line.frame.size.width) / 2.0f);
}
[currentLinkFrames addObject:[NSValue valueWithCGRect:CGRectOffset(urlItem.frame, lineOrigin.x, 0.0)]];
}
} else if (currentUrlItem != nil) {
if (currentLinkFrames.count != 0) {
[views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)];
}
[currentLinkFrames removeAllObjects];
currentUrlItem = nil;
}
}
if (currentLinkFrames.count != 0 && currentUrlItem != nil) {
[views addObject:selectionViewFromFrames(currentLinkFrames, self.frame.origin, currentUrlItem)];
}
return views;
}
return nil;
}
@end*/
func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextStyleStack) -> NSAttributedString {
switch text {
case .empty:
return NSAttributedString(string: "", attributes: styleStack.textAttributes())
case let .plain(string):
return NSAttributedString(string: string, attributes: styleStack.textAttributes())
case let .bold(text):
styleStack.push(.bold)
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
return result
case let .italic(text):
styleStack.push(.italic)
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
return result
case let .underline(text):
styleStack.push(.underline)
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
return result
case let .strikethrough(text):
styleStack.push(.strikethrough)
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
return result
case let .fixed(text):
styleStack.push(.fontFixed(true))
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
return result
case let .url(text, url, _):
styleStack.push(.textColor(UIColor(rgb: 0x007BE8)))
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
styleStack.pop()
return result
case let .email(text, _):
styleStack.push(.bold)
styleStack.push(.textColor(UIColor(rgb: 0x007BE8)))
let result = attributedStringForRichText(text, styleStack: styleStack)
styleStack.pop()
styleStack.pop()
return result
case let .concat(texts):
let string = NSMutableAttributedString()
for text in texts {
let substring = attributedStringForRichText(text, styleStack: styleStack)
string.append(substring)
}
return string
}
}
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat) -> InstantPageTextItem {
if string.length == 0 {
return InstantPageTextItem(frame: CGRect(), lines: [])
}
var lines: [InstantPageTextLine] = []
guard let font = string.attribute(NSFontAttributeName, at: 0, effectiveRange: nil) as? UIFont else {
return InstantPageTextItem(frame: CGRect(), lines: [])
}
var lineSpacingFactor: CGFloat = 1.12
if let lineSpacingFactorAttribute = string.attribute(InstantPageLineSpacingFactorAttribute, at: 0, effectiveRange: nil) {
lineSpacingFactor = CGFloat((lineSpacingFactorAttribute as! NSNumber).floatValue)
}
let typesetter = CTTypesetterCreateWithAttributedString(string)
let fontAscent = font.ascender
let fontDescent = font.descender
let fontLineHeight = floor(fontAscent + fontDescent)
let fontLineSpacing = floor(fontLineHeight * lineSpacingFactor)
var lastIndex: CFIndex = 0
var currentLineOrigin = CGPoint()
while true {
let currentMaxWidth = boundingWidth - currentLineOrigin.x
let currentLineInset: CGFloat = 0.0
let lineCharacterCount = CTTypesetterSuggestLineBreak(typesetter, lastIndex, Double(currentMaxWidth))
if lineCharacterCount > 0 {
let line = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastIndex, lineCharacterCount), 100.0)
if line != nil {
let trailingWhitespace = CGFloat(CTLineGetTrailingWhitespaceWidth(line))
let lineWidth = CGFloat(CTLineGetTypographicBounds(line, nil, nil, nil) + Double(currentLineInset))
var urlItems: [InstantPageTextUrlItem] = []
var strikethroughItems: [InstantPageTextStrikethroughItem] = []
string.enumerateAttribute(NSStrikethroughStyleAttributeName, in: NSMakeRange(lastIndex, lineCharacterCount), options: [], using: { item, range, _ in
if let item = item {
let lowerX = floor(CTLineGetOffsetForStringIndex(line, range.location, nil))
let upperX = ceil(CTLineGetOffsetForStringIndex(line, range.location + range.length, nil))
strikethroughItems.append(InstantPageTextStrikethroughItem(frame: CGRect(x: currentLineOrigin.x + lowerX, y: currentLineOrigin.y, width: upperX - lowerX, height: fontLineHeight)))
}
})
/*__block NSMutableArray<TGInstantPageTextUrlItem *> *urlItems = nil;
[string enumerateAttribute:(NSString *)TGUrlAttribute inRange:NSMakeRange(lastIndex, lineCharacterCount) options:0 usingBlock:^(id item, NSRange range, __unused BOOL *stop) {
if (item != nil) {
if (urlItems == nil) {
urlItems = [[NSMutableArray alloc] init];
}
CGFloat lowerX = CGFloor(CTLineGetOffsetForStringIndex(line, range.location, NULL));
CGFloat upperX = CGCeil(CTLineGetOffsetForStringIndex(line, range.location + range.length, NULL));
[urlItems addObject:[[TGInstantPageTextUrlItem alloc] initWithFrame:CGRectMake(currentLineOrigin.x + lowerX, currentLineOrigin.y, upperX - lowerX, fontLineHeight) item:item]];
}
}];*/
let textLine = InstantPageTextLine(line: line, frame: CGRect(x: currentLineOrigin.x, y: currentLineOrigin.y, width: lineWidth, height: fontLineHeight), urlItems: urlItems, strikethroughItems: strikethroughItems)
lines.append(textLine)
currentLineOrigin.x = 0.0;
currentLineOrigin.y += fontLineHeight + fontLineSpacing
lastIndex += lineCharacterCount
} else {
break;
}
} else {
break;
}
}
var height: CGFloat = 0.0
if !lines.isEmpty {
height = lines.last!.frame.maxY
}
return InstantPageTextItem(frame: CGRect(x: 0.0, y: 0.0, width: boundingWidth, height: height), lines: lines)
}