Index: ios/chrome/browser/ui/util/manual_text_framer.mm |
diff --git a/ios/chrome/browser/ui/util/manual_text_framer.mm b/ios/chrome/browser/ui/util/manual_text_framer.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..74cdb7b12d2e57a2935d6c4dd1915e1328c65443 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/util/manual_text_framer.mm |
@@ -0,0 +1,636 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/ui/util/manual_text_framer.h" |
+ |
+#import <UIKit/UIKit.h> |
+ |
+#include "base/i18n/rtl.h" |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/scoped_nsobject.h" |
+#import "ios/chrome/browser/ui/util/core_text_util.h" |
+#import "ios/chrome/browser/ui/util/text_frame.h" |
+#import "ios/chrome/browser/ui/util/unicode_util.h" |
+ |
+// NOTE: When RTL text is laid out into glyph runs, the glyphs appear in the |
+// visual order in which they appear on screen. In other words, the glyphs are |
+// arranged in the reverse order of their corresponding characters in the |
+// original string. |
+ |
+namespace { |
+// Aligns |value| to the nearest pixel value, rounding by the function indicated |
+// by |function|. AlignmentFunction::CEIL should be used to align size values, |
+// while AlignmentFunction::FLOOR should be used to align location values. |
+enum class AlignmentFunction : short { CEIL = 0, FLOOR }; |
+CGFloat AlignValueToPixel(CGFloat value, AlignmentFunction function) { |
+ static CGFloat scale = [[UIScreen mainScreen] scale]; |
+ return function == AlignmentFunction::CEIL ? ceil(value * scale) / scale |
+ : floor(value * scale) / scale; |
+} |
+ |
+// Returns an NSArray of NSAttributedStrings corresponding to newline-separated |
+// paragraphs within |string|. |
+NSArray* GetParagraphStringsForString(NSAttributedString* string) { |
+ NSMutableArray* paragraph_strings = [NSMutableArray array]; |
+ NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet]; |
+ NSUInteger string_length = string.string.length; |
+ NSRange remaining_range = NSMakeRange(0, string_length); |
+ while (remaining_range.location < string_length) { |
+ NSRange newline_range = |
+ [string.string rangeOfCharacterFromSet:newline_char_set |
+ options:0 |
+ range:remaining_range]; |
+ NSRange paragraph_range = NSMakeRange(0, 0); |
+ if (newline_range.location == NSNotFound) { |
+ // There's no newline in the remaining portion of the string. |
+ paragraph_range = remaining_range; |
+ remaining_range = NSMakeRange(string_length, 0); |
+ } else { |
+ // A newline character was encountered. Compute approximate text lines |
+ // for the substring within |remaining_range| up to the newline. |
+ NSUInteger newline_end = newline_range.location + newline_range.length; |
+ paragraph_range = NSMakeRange(remaining_range.location, |
+ newline_end - remaining_range.location); |
+ remaining_range.location = newline_end; |
+ remaining_range.length = string_length - remaining_range.location; |
+ } |
+ // Create an attributed substring for the current paragraph and add it to |
+ // |paragraphs|. |
+ [paragraph_strings |
+ addObject:[string attributedSubstringFromRange:paragraph_range]]; |
+ } |
+ return paragraph_strings; |
+} |
+} // namespace |
+ |
+#pragma mark - ManualTextFrame |
+ |
+// A TextFrame implementation that is manually created by ManualTextFramer. |
+@interface ManualTextFrame : NSObject<TextFrame> { |
+ // Backing objects for properties of the same name. |
+ base::scoped_nsobject<NSAttributedString> _string; |
+ base::scoped_nsobject<NSMutableArray> _lines; |
+} |
+ |
+// Designated initializer. |
+- (instancetype)initWithString:(NSAttributedString*)string |
+ inBounds:(CGRect)bounds NS_DESIGNATED_INITIALIZER; |
+- (instancetype)init NS_UNAVAILABLE; |
+ |
+// Creates a FramedLine out of |line|, |stringRange|, and |origin|, then adds it |
+// to |lines|. |
+- (void)addFramedLineWithLine:(CTLineRef)line |
+ stringRange:(NSRange)stringRange |
+ origin:(CGPoint)origin; |
+ |
+// Redefine property as readwrite. |
+@property(nonatomic, readwrite) NSRange framedRange; |
+ |
+@end |
+ |
+@implementation ManualTextFrame |
+ |
+@synthesize framedRange = _framedRange; |
+@synthesize bounds = _bounds; |
+ |
+- (instancetype)initWithString:(NSAttributedString*)string |
+ inBounds:(CGRect)bounds { |
+ if ((self = [super init])) { |
+ DCHECK(string.string.length); |
+ _string.reset([string retain]); |
+ _bounds = bounds; |
+ _lines.reset([[NSMutableArray alloc] init]); |
+ } |
+ return self; |
+} |
+ |
+#pragma mark Accessors |
+ |
+- (NSAttributedString*)string { |
+ return _string.get(); |
+} |
+ |
+- (NSArray*)lines { |
+ return _lines.get(); |
+} |
+ |
+#pragma mark Private |
+ |
+- (void)addFramedLineWithLine:(CTLineRef)line |
+ stringRange:(NSRange)stringRange |
+ origin:(CGPoint)origin { |
+ base::scoped_nsobject<FramedLine> framedLine([[FramedLine alloc] |
+ initWithLine:line |
+ stringRange:stringRange |
+ origin:origin]); |
+ [_lines addObject:framedLine]; |
+} |
+ |
+@end |
+ |
+#pragma mark - ManualTextFramer Private Interface |
+ |
+@interface ManualTextFramer () { |
+ // Backing objects for properties of the same name. |
+ base::scoped_nsobject<NSAttributedString> _string; |
+ base::scoped_nsobject<ManualTextFrame> _manualTextFrame; |
+} |
+ |
+// The string passed upon initialization. |
+@property(nonatomic, readonly) NSAttributedString* string; |
+ |
+// The bounds passed upon initialization. |
+@property(nonatomic, readonly) CGRect bounds; |
+ |
+// The width of the bounds passed upon initialization. |
+@property(nonatomic, readonly) CGFloat boundingWidth; |
+ |
+// The remaining height into which text can be framed. |
+@property(nonatomic, assign) CGFloat remainingHeight; |
+ |
+// The text frame constructed by |-frameText|. |
+@property(nonatomic, readonly) ManualTextFrame* manualTextFrame; |
+ |
+// Creates a ManualTextFrame and assigns it to |_manualTextFrame|. Returns YES |
+// if a new text frame was successfully created. |
+- (BOOL)setupManualTextFrame; |
+ |
+@end |
+ |
+#pragma mark - ParagraphFramer |
+ |
+// ManualTextFramer subclass that frames a single paragraph. A paragraph is |
+// defined as an NSAttributedString which contains either zero newlines or one |
+// newline as its last character. |
+@interface ParagraphFramer : ManualTextFramer { |
+ // Backing objects for properties of the same name. |
+ base::ScopedCFTypeRef<CTLineRef> _line; |
+ base::scoped_nsobject<NSCharacterSet> _lineEndSet; |
+} |
+ |
+// The CTLine created from |string|. |
+@property(nonatomic, readonly) CTLineRef line; |
+ |
+// The effective text alignment for |line|. |
+@property(nonatomic, readonly) NSTextAlignment effectiveAlignment; |
+ |
+// Character set containing characters that are appropriate for line endings. |
+// These characters include whitespaces and newlines (denoting a word boundary), |
+// in addition to line-ending characters like hyphens, em dashes, and en dashes. |
+@property(nonatomic, readonly) NSCharacterSet* lineEndSet; |
+ |
+// The index of the current run that is being framed. Setting |runIdx| also |
+// updates |currentRun| and |currentGlyphCount|. |
+@property(nonatomic, assign) CFIndex runIdx; |
+ |
+// The CTRun corresponding with |runIdx| in |line|. |
+@property(nonatomic, readonly) CTRunRef currentRun; |
+ |
+// The glyph count in |currentRun|. |
+@property(nonatomic, readonly) CFIndex currentGlyphCount; |
+ |
+// The number of glyphs in |currentRun| that have been successfully framed. |
+@property(nonatomic, assign) CFIndex framedGlyphCount; |
+ |
+// The range in |string| that has successfully been framed for the current line. |
+@property(nonatomic, assign) NSRange currentLineRange; |
+ |
+// The width of the typographic bounds for the glyphs framed on the current |
+// line. This is the width of the substring of |string| corresponding to |
+// |currentLineRange|. |
+@property(nonatomic, assign) CGFloat currentLineWidth; |
+ |
+// The width of the trailing whitespace for the current line. This whitespace |
+// is not counted against the line width if it's the end of the line, but needs |
+// to be added in if non-whitespace characters from subsequent runs fit on the |
+// same line. |
+@property(nonatomic, assign) CGFloat currentWhitespaceWidth; |
+ |
+// Whether the paragraph's writing direction is in RTL. |
+@property(nonatomic, readonly) BOOL isRTL; |
+ |
+// Either 1 or -1 depending on |isRTL|. |
+@property(nonatomic, readonly) CFIndex incrementAmount; |
+ |
+// Glyphs are laid out differently for RTL and LTR languages (see note at top of |
+// file). These functions return a range with |range|'s length incremented or |
+// decremented and an updated location that would include the next glyph in the |
+// trailing direction. |
+- (CFRange)incrementRange:(CFRange)range byAmount:(CFIndex)amount; |
+- (CFRange)incrementRange:(CFRange)range; |
+- (CFRange)decrementRange:(CFRange)range; |
+ |
+// Updates |range| such that its trailing glyph index is |trailingGlyphIdx|. |
+- (CFRange)updateRange:(CFRange)range |
+ forTrailingGlyphIdx:(CFIndex)trailingGlyphIdx; |
+ |
+// Returns the index of the trailing glyph in |range| for |currentRun|. |
+- (CFIndex)trailingGlyphIdxForRange:(CFRange)range; |
+ |
+// Returns the index of the leading or trailing glyph in |currentRun|. |
+- (CFIndex)trailingGlyphIdxForCurrentRun; |
+ |
+// Manually frames the glyphs in |currentRun| following |framedGlyphCount|. |
+// This function updates |framedGlyphCount|, |currentLineWidth|, and |
+// |currentLineRange|. |
+- (void)frameCurrentRun; |
+ |
+// Returns the character associated with the glyph at |glyphIdx| in |
+// |currentRun|. |
+- (unichar)charForGlyphAtIdx:(CFIndex)glyphIdx; |
+ |
+// Returns the index within the original string corresponding to the glyph at |
+// |glyphIdx| in |currentRun|. |
+- (CFIndex)stringIdxForGlyphAtIdx:(CFIndex)glyphIdx; |
+ |
+// Returns YES if |runIdx| is within the range of |line|'s glyph runs array. |
+- (BOOL)runIdxIsValid:(CFIndex)runIdx; |
+ |
+// Returns YES if |glyphIdx| is within [0, |currentGlyphCount|). |
+- (BOOL)glyphIdxIsValid:(CFIndex)glyphIdx; |
+ |
+// Creates a line from |currentLineRange| and adds it to |lines|. |
+- (void)addCurrentLine; |
+ |
+// Returns the baselines origin for the current line. This function depends on |
+// |currentLineRange|, |currentLineWidth|, and |remainingHeight|, and must be |
+// called before updating those bookkeeping variables when adding the line. |
+- (CGPoint)originForCurrentLine; |
+@end |
+ |
+@implementation ParagraphFramer |
+ |
+@synthesize effectiveAlignment = _effectiveTextAlignment; |
+@synthesize runIdx = _runIdx; |
+@synthesize currentRun = _currentRun; |
+@synthesize currentGlyphCount = _currentGlyphCount; |
+@synthesize framedGlyphCount = _framedGlyphCount; |
+@synthesize currentLineRange = _currentLineRange; |
+@synthesize currentLineWidth = _currentLineWidth; |
+@synthesize currentWhitespaceWidth = _currentWhitespaceWidth; |
+@synthesize isRTL = _isRTL; |
+ |
+- (instancetype)initWithString:(NSAttributedString*)string |
+ inBounds:(CGRect)bounds { |
+ if ((self = [super initWithString:string inBounds:bounds])) { |
+ NSRange newlineRange = [string.string |
+ rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]]; |
+ DCHECK(newlineRange.location == NSNotFound || |
+ newlineRange.location == string.string.length - 1); |
+ CTLineRef line = |
+ CTLineCreateWithAttributedString(base::mac::NSToCFCast(string)); |
+ _line.reset(line); |
+ _effectiveTextAlignment = core_text_util::GetEffectiveTextAlignment(string); |
+ NSWritingDirection direction = |
+ core_text_util::GetEffectiveWritingDirection(string); |
+ _isRTL = direction == NSWritingDirectionRightToLeft; |
+ } |
+ return self; |
+} |
+ |
+- (void)frameText { |
+ if (![self setupManualTextFrame]) |
+ return; |
+ self.runIdx = |
+ self.isRTL ? CFArrayGetCount(CTLineGetGlyphRuns(self.line)) - 1 : 0; |
+ while (self.currentRun) { |
+ NSRange runStringRange = |
+ core_text_util::GetStringRangeForRun(self.currentRun); |
+ DCHECK_NE(runStringRange.location, static_cast<NSUInteger>(NSNotFound)); |
+ DCHECK_NE(runStringRange.length, 0U); |
+ CGFloat runLineHeight = |
+ core_text_util::GetLineHeight(self.string, runStringRange); |
+ // Count of the number of times the framing process (-frameCurrentRun) has |
+ // "stalled" -- run without changing the total number of glyphs framed. In |
+ // some cases the process may stall once at the end of a run, but if it |
+ // stalls twice, it won't make any further progress and should halt. |
+ NSUInteger stallCount = 0; |
+ // Loop as long as framed glyph count is less that the total glyph count, |
+ // and the framer is making progress. |
+ while (self.framedGlyphCount < self.currentGlyphCount && stallCount < 2U) { |
+ // Stop framing glyphs if there is not enough vertical space for the run. |
+ if (self.remainingHeight < runLineHeight) |
+ break; |
+ CFIndex initialFramedGlyphCount = self.framedGlyphCount; |
+ [self frameCurrentRun]; |
+ if (self.framedGlyphCount == initialFramedGlyphCount) |
+ stallCount++; |
+ if (self.framedGlyphCount < self.currentGlyphCount) { |
+ // The entire run didn't fit onto the current line, so create a CTLine |
+ // from |currentLineRange| and add it to |lines|. |
+ [self addCurrentLine]; |
+ } |
+ } |
+ self.runIdx += self.incrementAmount; |
+ } |
+ // Add the final line. |
+ [self addCurrentLine]; |
+ // Update |manualTextFrame|'s |framedRange|. |
+ self.manualTextFrame.framedRange = |
+ NSMakeRange(0, self.currentLineRange.location); |
+} |
+ |
+#pragma mark Accessors |
+ |
+- (CTLineRef)line { |
+ return _line.get(); |
+} |
+ |
+- (NSCharacterSet*)lineEndSet { |
+ if (!_lineEndSet) { |
+ NSMutableCharacterSet* lineEndSet = |
+ [NSMutableCharacterSet whitespaceAndNewlineCharacterSet]; |
+ [lineEndSet addCharactersInString:@"-\u2013\u2014"]; |
+ _lineEndSet.reset([lineEndSet retain]); |
+ } |
+ return _lineEndSet; |
+} |
+ |
+- (void)setRunIdx:(CFIndex)runIdx { |
+ _runIdx = runIdx; |
+ self.framedGlyphCount = 0; |
+ if ([self runIdxIsValid:runIdx]) { |
+ NSArray* runs = base::mac::CFToNSCast(CTLineGetGlyphRuns(self.line)); |
+ _currentRun = static_cast<CTRunRef>(runs[_runIdx]); |
+ _currentGlyphCount = CTRunGetGlyphCount(self.currentRun); |
+ } else { |
+ _currentRun = nullptr; |
+ _currentGlyphCount = 0; |
+ } |
+} |
+ |
+- (CFIndex)incrementAmount { |
+ return self.isRTL ? -1 : 1; |
+} |
+ |
+#pragma mark Private |
+ |
+- (CFRange)incrementRange:(CFRange)range byAmount:(CFIndex)amount { |
+ CFRange incrementedRange = range; |
+ incrementedRange.length += amount; |
+ if (self.isRTL) |
+ incrementedRange.location += self.incrementAmount * amount; |
+ return incrementedRange; |
+} |
+ |
+- (CFRange)incrementRange:(CFRange)range { |
+ return [self incrementRange:range byAmount:1]; |
+} |
+ |
+- (CFRange)decrementRange:(CFRange)range { |
+ return [self incrementRange:range byAmount:-1]; |
+} |
+ |
+- (CFRange)updateRange:(CFRange)range |
+ forTrailingGlyphIdx:(CFIndex)trailingGlyphIdx { |
+ DCHECK(self.isRTL ? trailingGlyphIdx <= range.location + range.length |
+ : trailingGlyphIdx >= range.location); |
+ DCHECK([self glyphIdxIsValid:trailingGlyphIdx]); |
+ CFIndex currentTrailingGlyphIdx = [self trailingGlyphIdxForRange:range]; |
+ CFIndex updateAmount = self.isRTL |
+ ? currentTrailingGlyphIdx - trailingGlyphIdx |
+ : trailingGlyphIdx - currentTrailingGlyphIdx; |
+ return [self incrementRange:range byAmount:updateAmount]; |
+} |
+ |
+- (CFIndex)trailingGlyphIdxForRange:(CFRange)range { |
+ if (self.isRTL) |
+ return range.location; |
+ return range.location + range.length - 1; |
+} |
+ |
+- (CFIndex)trailingGlyphIdxForCurrentRun { |
+ return self.isRTL ? 0 : self.currentGlyphCount - 1; |
+} |
+ |
+- (void)frameCurrentRun { |
+ DCHECK(self.currentRun); |
+ DCHECK_LT(self.framedGlyphCount, self.currentGlyphCount); |
+ DCHECK_LT(self.currentLineWidth, self.boundingWidth); |
+ |
+ // Calculate the range that will fit in the remaining portion of the line. |
+ NSCharacterSet* whitespaceSet = |
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]; |
+ CFIndex startGlyphIdx = self.isRTL |
+ ? self.currentGlyphCount - self.framedGlyphCount |
+ : self.framedGlyphCount; |
+ CFRange remainingRunRange = |
+ CFRangeMake(self.isRTL ? 0 : startGlyphIdx, |
+ self.currentGlyphCount - self.framedGlyphCount); |
+ CFRange range = CFRangeMake(startGlyphIdx, 0); |
+ while (remainingRunRange.length > 0) { |
+ // Find the range for the next word that can be added to the line. If no |
+ // delimiters were found, frame the rest of the run. |
+ CFIndex delimIdx = core_text_util::GetGlyphIdxForCharInSet( |
+ self.currentRun, remainingRunRange, self.string, self.lineEndSet); |
+ if (delimIdx == kCFNotFound) |
+ delimIdx = [self trailingGlyphIdxForCurrentRun]; |
+ CFRange wordGlyphRange = |
+ [self updateRange:remainingRunRange forTrailingGlyphIdx:delimIdx]; |
+ CFIndex wordFramedGlyphCount = wordGlyphRange.length; |
+ // Trim any whitespace and record its width. |
+ CGFloat wordTrailingWhitespaceWidth = 0.0; |
+ if ([whitespaceSet characterIsMember:[self charForGlyphAtIdx:delimIdx]]) { |
+ wordTrailingWhitespaceWidth = |
+ core_text_util::GetGlyphWidth(self.currentRun, delimIdx); |
+ wordGlyphRange = [self decrementRange:wordGlyphRange]; |
+ } |
+ // Check if the word will fit on the line. |
+ CGFloat wordWidth = |
+ core_text_util::GetRunWidthWithRange(self.currentRun, wordGlyphRange); |
+ CGFloat cumulativeLineWidth = |
+ self.currentLineWidth + self.currentWhitespaceWidth + wordWidth; |
+ if (cumulativeLineWidth <= self.boundingWidth) { |
+ // The word at |wordGlyphRange| fits on the line. |
+ self.currentLineWidth = cumulativeLineWidth; |
+ self.framedGlyphCount += wordFramedGlyphCount; |
+ self.currentWhitespaceWidth = wordTrailingWhitespaceWidth; |
+ remainingRunRange.length -= wordFramedGlyphCount; |
+ if (!self.isRTL) |
+ remainingRunRange.location += wordFramedGlyphCount; |
+ range = [self incrementRange:range byAmount:wordFramedGlyphCount]; |
+ } else { |
+ break; |
+ } |
+ } |
+ // Early return if no glyphs were framed. |
+ if (!range.length) |
+ return; |
+ // Use the string index of the next glyph to determine the string range for |
+ // the current line, since a glyph may correspond with multiple characters |
+ // when ligatures are used. |
+ CFIndex nextGlyphIdx = |
+ [self trailingGlyphIdxForRange:range] + self.incrementAmount; |
+ CFIndex nextGlyphStringIdx; |
+ if ([self glyphIdxIsValid:nextGlyphIdx]) { |
+ nextGlyphStringIdx = [self stringIdxForGlyphAtIdx:nextGlyphIdx]; |
+ } else { |
+ CFRange runStringRange = CTRunGetStringRange(self.currentRun); |
+ nextGlyphStringIdx = runStringRange.location + runStringRange.length; |
+ } |
+ self.currentLineRange = |
+ NSMakeRange(self.currentLineRange.location, |
+ nextGlyphStringIdx - self.currentLineRange.location); |
+} |
+ |
+- (unichar)charForGlyphAtIdx:(CFIndex)glyphIdx { |
+ DCHECK([self glyphIdxIsValid:glyphIdx]); |
+ return [self.string.string |
+ characterAtIndex:[self stringIdxForGlyphAtIdx:glyphIdx]]; |
+} |
+ |
+- (CFIndex)stringIdxForGlyphAtIdx:(CFIndex)glyphIdx { |
+ DCHECK([self glyphIdxIsValid:glyphIdx]); |
+ CFIndex stringIdx = 0; |
+ CTRunGetStringIndices(self.currentRun, CFRangeMake(glyphIdx, 1), &stringIdx); |
+ return stringIdx; |
+} |
+ |
+- (BOOL)runIdxIsValid:(CFIndex)runIdx { |
+ NSArray* runs = base::mac::CFToNSCast(CTLineGetGlyphRuns(self.line)); |
+ return runIdx >= 0 && runIdx < static_cast<CFIndex>(runs.count); |
+} |
+ |
+- (BOOL)glyphIdxIsValid:(CFIndex)glyphIdx { |
+ return glyphIdx >= 0 && glyphIdx < self.currentGlyphCount; |
+} |
+ |
+- (void)addCurrentLine { |
+ // Don't attempt to add a line if |currentLineRange| is empty. |
+ if (!self.currentLineRange.length) |
+ return; |
+ // Add the new line and its corresponding string range and baseline origin. |
+ NSAttributedString* currentLineString = |
+ [self.string attributedSubstringFromRange:self.currentLineRange]; |
+ CTLineRef currentLine = CTLineCreateWithAttributedString( |
+ base::mac::NSToCFCast(currentLineString)); |
+ [self.manualTextFrame addFramedLineWithLine:currentLine |
+ stringRange:self.currentLineRange |
+ origin:[self originForCurrentLine]]; |
+ CFRelease(currentLine); |
+ // Update bookkeeping variables for next line. |
+ CGFloat usedHeight = |
+ core_text_util::GetLineHeight(self.string, self.currentLineRange) + |
+ core_text_util::GetLineSpacing(self.string, self.currentLineRange); |
+ self.currentLineRange = NSMakeRange( |
+ self.currentLineRange.location + self.currentLineRange.length, 0); |
+ self.currentLineWidth = 0; |
+ self.currentWhitespaceWidth = 0; |
+ self.remainingHeight -= usedHeight; |
+} |
+ |
+- (CGPoint)originForCurrentLine { |
+ CGPoint origin = CGPointZero; |
+ CGFloat alignedWidth = |
+ AlignValueToPixel(self.currentLineWidth, AlignmentFunction::CEIL); |
+ switch (self.effectiveAlignment) { |
+ case NSTextAlignmentLeft: |
+ // Left-aligned lines begin at 0.0. |
+ break; |
+ case NSTextAlignmentRight: |
+ origin.x = AlignValueToPixel(self.boundingWidth - alignedWidth, |
+ AlignmentFunction::FLOOR); |
+ break; |
+ case NSTextAlignmentCenter: |
+ origin.x = AlignValueToPixel((self.boundingWidth - alignedWidth) / 2.0, |
+ AlignmentFunction::FLOOR); |
+ break; |
+ default: |
+ // Only left, right, and center effective alignment is supported. |
+ NOTREACHED(); |
+ break; |
+ } |
+ UIFont* font = [self.string attribute:NSFontAttributeName |
+ atIndex:self.currentLineRange.location |
+ effectiveRange:nullptr]; |
+ CGFloat lineHeight = |
+ core_text_util::GetLineHeight(self.string, self.currentLineRange); |
+ origin.y = |
+ AlignValueToPixel(self.remainingHeight - lineHeight - font.descender, |
+ AlignmentFunction::FLOOR); |
+ return origin; |
+} |
+ |
+@end |
+ |
+#pragma mark - ManualTextFramer |
+ |
+@implementation ManualTextFramer |
+ |
+@synthesize bounds = _bounds; |
+@synthesize boundingWidth = _boundingWidth; |
+@synthesize remainingHeight = _remainingHeight; |
+ |
+- (instancetype)initWithString:(NSAttributedString*)string |
+ inBounds:(CGRect)bounds { |
+ if ((self = [super init])) { |
+ DCHECK(string.string.length); |
+ _string.reset([string retain]); |
+ _bounds = bounds; |
+ _boundingWidth = CGRectGetWidth(bounds); |
+ _remainingHeight = CGRectGetHeight(bounds); |
+ } |
+ return self; |
+} |
+ |
+- (void)frameText { |
+ if (![self setupManualTextFrame]) |
+ return; |
+ NSRange framedRange = NSMakeRange(0, 0); |
+ NSArray* paragraphs = GetParagraphStringsForString(self.string); |
+ NSUInteger stringRangeOffset = 0; |
+ for (NSAttributedString* paragraph in paragraphs) { |
+ // Frame each paragraph using a ParagraphFramer, then update bookkeeping |
+ // variables for the top-level ManualTextFramer. |
+ CGRect remainingBounds = |
+ CGRectMake(0, 0, self.boundingWidth, self.remainingHeight); |
+ base::scoped_nsobject<ParagraphFramer> framer([[ParagraphFramer alloc] |
+ initWithString:paragraph |
+ inBounds:remainingBounds]); |
+ [framer frameText]; |
+ id<TextFrame> frame = [framer textFrame]; |
+ DCHECK(frame); |
+ framedRange.length += frame.framedRange.length; |
+ CGFloat paragraphHeight = 0.0; |
+ for (FramedLine* line in frame.lines) { |
+ NSRange lineRange = line.stringRange; |
+ lineRange.location += stringRangeOffset; |
+ [self.manualTextFrame addFramedLineWithLine:line.line |
+ stringRange:lineRange |
+ origin:line.origin]; |
+ paragraphHeight += core_text_util::GetLineHeight(self.string, lineRange) + |
+ core_text_util::GetLineSpacing(self.string, lineRange); |
+ } |
+ self.remainingHeight -= paragraphHeight; |
+ stringRangeOffset += paragraph.string.length; |
+ } |
+ self.manualTextFrame.framedRange = framedRange; |
+} |
+ |
+#pragma mark Accessors |
+ |
+- (NSAttributedString*)string { |
+ return _string.get(); |
+} |
+ |
+- (ManualTextFrame*)manualTextFrame { |
+ return _manualTextFrame.get(); |
+} |
+ |
+- (id<TextFrame>)textFrame { |
+ return _manualTextFrame.get(); |
+} |
+ |
+#pragma mark Private |
+ |
+- (BOOL)setupManualTextFrame { |
+ if (_manualTextFrame) |
+ return NO; |
+ _manualTextFrame.reset([[ManualTextFrame alloc] initWithString:self.string |
+ inBounds:self.bounds]); |
+ return YES; |
+} |
+ |
+@end |