OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/util/manual_text_framer.h" |
| 6 |
| 7 #import <UIKit/UIKit.h> |
| 8 |
| 9 #include "base/i18n/rtl.h" |
| 10 #include "base/logging.h" |
| 11 #include "base/mac/foundation_util.h" |
| 12 #include "base/mac/scoped_nsobject.h" |
| 13 #import "ios/chrome/browser/ui/util/core_text_util.h" |
| 14 #import "ios/chrome/browser/ui/util/text_frame.h" |
| 15 #import "ios/chrome/browser/ui/util/unicode_util.h" |
| 16 |
| 17 // NOTE: When RTL text is laid out into glyph runs, the glyphs appear in the |
| 18 // visual order in which they appear on screen. In other words, the glyphs are |
| 19 // arranged in the reverse order of their corresponding characters in the |
| 20 // original string. |
| 21 |
| 22 namespace { |
| 23 // Aligns |value| to the nearest pixel value, rounding by the function indicated |
| 24 // by |function|. AlignmentFunction::CEIL should be used to align size values, |
| 25 // while AlignmentFunction::FLOOR should be used to align location values. |
| 26 enum class AlignmentFunction : short { CEIL = 0, FLOOR }; |
| 27 CGFloat AlignValueToPixel(CGFloat value, AlignmentFunction function) { |
| 28 static CGFloat scale = [[UIScreen mainScreen] scale]; |
| 29 return function == AlignmentFunction::CEIL ? ceil(value * scale) / scale |
| 30 : floor(value * scale) / scale; |
| 31 } |
| 32 |
| 33 // Returns an NSArray of NSAttributedStrings corresponding to newline-separated |
| 34 // paragraphs within |string|. |
| 35 NSArray* GetParagraphStringsForString(NSAttributedString* string) { |
| 36 NSMutableArray* paragraph_strings = [NSMutableArray array]; |
| 37 NSCharacterSet* newline_char_set = [NSCharacterSet newlineCharacterSet]; |
| 38 NSUInteger string_length = string.string.length; |
| 39 NSRange remaining_range = NSMakeRange(0, string_length); |
| 40 while (remaining_range.location < string_length) { |
| 41 NSRange newline_range = |
| 42 [string.string rangeOfCharacterFromSet:newline_char_set |
| 43 options:0 |
| 44 range:remaining_range]; |
| 45 NSRange paragraph_range = NSMakeRange(0, 0); |
| 46 if (newline_range.location == NSNotFound) { |
| 47 // There's no newline in the remaining portion of the string. |
| 48 paragraph_range = remaining_range; |
| 49 remaining_range = NSMakeRange(string_length, 0); |
| 50 } else { |
| 51 // A newline character was encountered. Compute approximate text lines |
| 52 // for the substring within |remaining_range| up to the newline. |
| 53 NSUInteger newline_end = newline_range.location + newline_range.length; |
| 54 paragraph_range = NSMakeRange(remaining_range.location, |
| 55 newline_end - remaining_range.location); |
| 56 remaining_range.location = newline_end; |
| 57 remaining_range.length = string_length - remaining_range.location; |
| 58 } |
| 59 // Create an attributed substring for the current paragraph and add it to |
| 60 // |paragraphs|. |
| 61 [paragraph_strings |
| 62 addObject:[string attributedSubstringFromRange:paragraph_range]]; |
| 63 } |
| 64 return paragraph_strings; |
| 65 } |
| 66 } // namespace |
| 67 |
| 68 #pragma mark - ManualTextFrame |
| 69 |
| 70 // A TextFrame implementation that is manually created by ManualTextFramer. |
| 71 @interface ManualTextFrame : NSObject<TextFrame> { |
| 72 // Backing objects for properties of the same name. |
| 73 base::scoped_nsobject<NSAttributedString> _string; |
| 74 base::scoped_nsobject<NSMutableArray> _lines; |
| 75 } |
| 76 |
| 77 // Designated initializer. |
| 78 - (instancetype)initWithString:(NSAttributedString*)string |
| 79 inBounds:(CGRect)bounds NS_DESIGNATED_INITIALIZER; |
| 80 - (instancetype)init NS_UNAVAILABLE; |
| 81 |
| 82 // Creates a FramedLine out of |line|, |stringRange|, and |origin|, then adds it |
| 83 // to |lines|. |
| 84 - (void)addFramedLineWithLine:(CTLineRef)line |
| 85 stringRange:(NSRange)stringRange |
| 86 origin:(CGPoint)origin; |
| 87 |
| 88 // Redefine property as readwrite. |
| 89 @property(nonatomic, readwrite) NSRange framedRange; |
| 90 |
| 91 @end |
| 92 |
| 93 @implementation ManualTextFrame |
| 94 |
| 95 @synthesize framedRange = _framedRange; |
| 96 @synthesize bounds = _bounds; |
| 97 |
| 98 - (instancetype)initWithString:(NSAttributedString*)string |
| 99 inBounds:(CGRect)bounds { |
| 100 if ((self = [super init])) { |
| 101 DCHECK(string.string.length); |
| 102 _string.reset([string retain]); |
| 103 _bounds = bounds; |
| 104 _lines.reset([[NSMutableArray alloc] init]); |
| 105 } |
| 106 return self; |
| 107 } |
| 108 |
| 109 #pragma mark Accessors |
| 110 |
| 111 - (NSAttributedString*)string { |
| 112 return _string.get(); |
| 113 } |
| 114 |
| 115 - (NSArray*)lines { |
| 116 return _lines.get(); |
| 117 } |
| 118 |
| 119 #pragma mark Private |
| 120 |
| 121 - (void)addFramedLineWithLine:(CTLineRef)line |
| 122 stringRange:(NSRange)stringRange |
| 123 origin:(CGPoint)origin { |
| 124 base::scoped_nsobject<FramedLine> framedLine([[FramedLine alloc] |
| 125 initWithLine:line |
| 126 stringRange:stringRange |
| 127 origin:origin]); |
| 128 [_lines addObject:framedLine]; |
| 129 } |
| 130 |
| 131 @end |
| 132 |
| 133 #pragma mark - ManualTextFramer Private Interface |
| 134 |
| 135 @interface ManualTextFramer () { |
| 136 // Backing objects for properties of the same name. |
| 137 base::scoped_nsobject<NSAttributedString> _string; |
| 138 base::scoped_nsobject<ManualTextFrame> _manualTextFrame; |
| 139 } |
| 140 |
| 141 // The string passed upon initialization. |
| 142 @property(nonatomic, readonly) NSAttributedString* string; |
| 143 |
| 144 // The bounds passed upon initialization. |
| 145 @property(nonatomic, readonly) CGRect bounds; |
| 146 |
| 147 // The width of the bounds passed upon initialization. |
| 148 @property(nonatomic, readonly) CGFloat boundingWidth; |
| 149 |
| 150 // The remaining height into which text can be framed. |
| 151 @property(nonatomic, assign) CGFloat remainingHeight; |
| 152 |
| 153 // The text frame constructed by |-frameText|. |
| 154 @property(nonatomic, readonly) ManualTextFrame* manualTextFrame; |
| 155 |
| 156 // Creates a ManualTextFrame and assigns it to |_manualTextFrame|. Returns YES |
| 157 // if a new text frame was successfully created. |
| 158 - (BOOL)setupManualTextFrame; |
| 159 |
| 160 @end |
| 161 |
| 162 #pragma mark - ParagraphFramer |
| 163 |
| 164 // ManualTextFramer subclass that frames a single paragraph. A paragraph is |
| 165 // defined as an NSAttributedString which contains either zero newlines or one |
| 166 // newline as its last character. |
| 167 @interface ParagraphFramer : ManualTextFramer { |
| 168 // Backing objects for properties of the same name. |
| 169 base::ScopedCFTypeRef<CTLineRef> _line; |
| 170 base::scoped_nsobject<NSCharacterSet> _lineEndSet; |
| 171 } |
| 172 |
| 173 // The CTLine created from |string|. |
| 174 @property(nonatomic, readonly) CTLineRef line; |
| 175 |
| 176 // The effective text alignment for |line|. |
| 177 @property(nonatomic, readonly) NSTextAlignment effectiveAlignment; |
| 178 |
| 179 // Character set containing characters that are appropriate for line endings. |
| 180 // These characters include whitespaces and newlines (denoting a word boundary), |
| 181 // in addition to line-ending characters like hyphens, em dashes, and en dashes. |
| 182 @property(nonatomic, readonly) NSCharacterSet* lineEndSet; |
| 183 |
| 184 // The index of the current run that is being framed. Setting |runIdx| also |
| 185 // updates |currentRun| and |currentGlyphCount|. |
| 186 @property(nonatomic, assign) CFIndex runIdx; |
| 187 |
| 188 // The CTRun corresponding with |runIdx| in |line|. |
| 189 @property(nonatomic, readonly) CTRunRef currentRun; |
| 190 |
| 191 // The glyph count in |currentRun|. |
| 192 @property(nonatomic, readonly) CFIndex currentGlyphCount; |
| 193 |
| 194 // The number of glyphs in |currentRun| that have been successfully framed. |
| 195 @property(nonatomic, assign) CFIndex framedGlyphCount; |
| 196 |
| 197 // The range in |string| that has successfully been framed for the current line. |
| 198 @property(nonatomic, assign) NSRange currentLineRange; |
| 199 |
| 200 // The width of the typographic bounds for the glyphs framed on the current |
| 201 // line. This is the width of the substring of |string| corresponding to |
| 202 // |currentLineRange|. |
| 203 @property(nonatomic, assign) CGFloat currentLineWidth; |
| 204 |
| 205 // The width of the trailing whitespace for the current line. This whitespace |
| 206 // is not counted against the line width if it's the end of the line, but needs |
| 207 // to be added in if non-whitespace characters from subsequent runs fit on the |
| 208 // same line. |
| 209 @property(nonatomic, assign) CGFloat currentWhitespaceWidth; |
| 210 |
| 211 // Whether the paragraph's writing direction is in RTL. |
| 212 @property(nonatomic, readonly) BOOL isRTL; |
| 213 |
| 214 // Either 1 or -1 depending on |isRTL|. |
| 215 @property(nonatomic, readonly) CFIndex incrementAmount; |
| 216 |
| 217 // Glyphs are laid out differently for RTL and LTR languages (see note at top of |
| 218 // file). These functions return a range with |range|'s length incremented or |
| 219 // decremented and an updated location that would include the next glyph in the |
| 220 // trailing direction. |
| 221 - (CFRange)incrementRange:(CFRange)range byAmount:(CFIndex)amount; |
| 222 - (CFRange)incrementRange:(CFRange)range; |
| 223 - (CFRange)decrementRange:(CFRange)range; |
| 224 |
| 225 // Updates |range| such that its trailing glyph index is |trailingGlyphIdx|. |
| 226 - (CFRange)updateRange:(CFRange)range |
| 227 forTrailingGlyphIdx:(CFIndex)trailingGlyphIdx; |
| 228 |
| 229 // Returns the index of the trailing glyph in |range| for |currentRun|. |
| 230 - (CFIndex)trailingGlyphIdxForRange:(CFRange)range; |
| 231 |
| 232 // Returns the index of the leading or trailing glyph in |currentRun|. |
| 233 - (CFIndex)trailingGlyphIdxForCurrentRun; |
| 234 |
| 235 // Manually frames the glyphs in |currentRun| following |framedGlyphCount|. |
| 236 // This function updates |framedGlyphCount|, |currentLineWidth|, and |
| 237 // |currentLineRange|. |
| 238 - (void)frameCurrentRun; |
| 239 |
| 240 // Returns the character associated with the glyph at |glyphIdx| in |
| 241 // |currentRun|. |
| 242 - (unichar)charForGlyphAtIdx:(CFIndex)glyphIdx; |
| 243 |
| 244 // Returns the index within the original string corresponding to the glyph at |
| 245 // |glyphIdx| in |currentRun|. |
| 246 - (CFIndex)stringIdxForGlyphAtIdx:(CFIndex)glyphIdx; |
| 247 |
| 248 // Returns YES if |runIdx| is within the range of |line|'s glyph runs array. |
| 249 - (BOOL)runIdxIsValid:(CFIndex)runIdx; |
| 250 |
| 251 // Returns YES if |glyphIdx| is within [0, |currentGlyphCount|). |
| 252 - (BOOL)glyphIdxIsValid:(CFIndex)glyphIdx; |
| 253 |
| 254 // Creates a line from |currentLineRange| and adds it to |lines|. |
| 255 - (void)addCurrentLine; |
| 256 |
| 257 // Returns the baselines origin for the current line. This function depends on |
| 258 // |currentLineRange|, |currentLineWidth|, and |remainingHeight|, and must be |
| 259 // called before updating those bookkeeping variables when adding the line. |
| 260 - (CGPoint)originForCurrentLine; |
| 261 @end |
| 262 |
| 263 @implementation ParagraphFramer |
| 264 |
| 265 @synthesize effectiveAlignment = _effectiveTextAlignment; |
| 266 @synthesize runIdx = _runIdx; |
| 267 @synthesize currentRun = _currentRun; |
| 268 @synthesize currentGlyphCount = _currentGlyphCount; |
| 269 @synthesize framedGlyphCount = _framedGlyphCount; |
| 270 @synthesize currentLineRange = _currentLineRange; |
| 271 @synthesize currentLineWidth = _currentLineWidth; |
| 272 @synthesize currentWhitespaceWidth = _currentWhitespaceWidth; |
| 273 @synthesize isRTL = _isRTL; |
| 274 |
| 275 - (instancetype)initWithString:(NSAttributedString*)string |
| 276 inBounds:(CGRect)bounds { |
| 277 if ((self = [super initWithString:string inBounds:bounds])) { |
| 278 NSRange newlineRange = [string.string |
| 279 rangeOfCharacterFromSet:[NSCharacterSet newlineCharacterSet]]; |
| 280 DCHECK(newlineRange.location == NSNotFound || |
| 281 newlineRange.location == string.string.length - 1); |
| 282 CTLineRef line = |
| 283 CTLineCreateWithAttributedString(base::mac::NSToCFCast(string)); |
| 284 _line.reset(line); |
| 285 _effectiveTextAlignment = core_text_util::GetEffectiveTextAlignment(string); |
| 286 NSWritingDirection direction = |
| 287 core_text_util::GetEffectiveWritingDirection(string); |
| 288 _isRTL = direction == NSWritingDirectionRightToLeft; |
| 289 } |
| 290 return self; |
| 291 } |
| 292 |
| 293 - (void)frameText { |
| 294 if (![self setupManualTextFrame]) |
| 295 return; |
| 296 self.runIdx = |
| 297 self.isRTL ? CFArrayGetCount(CTLineGetGlyphRuns(self.line)) - 1 : 0; |
| 298 while (self.currentRun) { |
| 299 NSRange runStringRange = |
| 300 core_text_util::GetStringRangeForRun(self.currentRun); |
| 301 DCHECK_NE(runStringRange.location, static_cast<NSUInteger>(NSNotFound)); |
| 302 DCHECK_NE(runStringRange.length, 0U); |
| 303 CGFloat runLineHeight = |
| 304 core_text_util::GetLineHeight(self.string, runStringRange); |
| 305 // Count of the number of times the framing process (-frameCurrentRun) has |
| 306 // "stalled" -- run without changing the total number of glyphs framed. In |
| 307 // some cases the process may stall once at the end of a run, but if it |
| 308 // stalls twice, it won't make any further progress and should halt. |
| 309 NSUInteger stallCount = 0; |
| 310 // Loop as long as framed glyph count is less that the total glyph count, |
| 311 // and the framer is making progress. |
| 312 while (self.framedGlyphCount < self.currentGlyphCount && stallCount < 2U) { |
| 313 // Stop framing glyphs if there is not enough vertical space for the run. |
| 314 if (self.remainingHeight < runLineHeight) |
| 315 break; |
| 316 CFIndex initialFramedGlyphCount = self.framedGlyphCount; |
| 317 [self frameCurrentRun]; |
| 318 if (self.framedGlyphCount == initialFramedGlyphCount) |
| 319 stallCount++; |
| 320 if (self.framedGlyphCount < self.currentGlyphCount) { |
| 321 // The entire run didn't fit onto the current line, so create a CTLine |
| 322 // from |currentLineRange| and add it to |lines|. |
| 323 [self addCurrentLine]; |
| 324 } |
| 325 } |
| 326 self.runIdx += self.incrementAmount; |
| 327 } |
| 328 // Add the final line. |
| 329 [self addCurrentLine]; |
| 330 // Update |manualTextFrame|'s |framedRange|. |
| 331 self.manualTextFrame.framedRange = |
| 332 NSMakeRange(0, self.currentLineRange.location); |
| 333 } |
| 334 |
| 335 #pragma mark Accessors |
| 336 |
| 337 - (CTLineRef)line { |
| 338 return _line.get(); |
| 339 } |
| 340 |
| 341 - (NSCharacterSet*)lineEndSet { |
| 342 if (!_lineEndSet) { |
| 343 NSMutableCharacterSet* lineEndSet = |
| 344 [NSMutableCharacterSet whitespaceAndNewlineCharacterSet]; |
| 345 [lineEndSet addCharactersInString:@"-\u2013\u2014"]; |
| 346 _lineEndSet.reset([lineEndSet retain]); |
| 347 } |
| 348 return _lineEndSet; |
| 349 } |
| 350 |
| 351 - (void)setRunIdx:(CFIndex)runIdx { |
| 352 _runIdx = runIdx; |
| 353 self.framedGlyphCount = 0; |
| 354 if ([self runIdxIsValid:runIdx]) { |
| 355 NSArray* runs = base::mac::CFToNSCast(CTLineGetGlyphRuns(self.line)); |
| 356 _currentRun = static_cast<CTRunRef>(runs[_runIdx]); |
| 357 _currentGlyphCount = CTRunGetGlyphCount(self.currentRun); |
| 358 } else { |
| 359 _currentRun = nullptr; |
| 360 _currentGlyphCount = 0; |
| 361 } |
| 362 } |
| 363 |
| 364 - (CFIndex)incrementAmount { |
| 365 return self.isRTL ? -1 : 1; |
| 366 } |
| 367 |
| 368 #pragma mark Private |
| 369 |
| 370 - (CFRange)incrementRange:(CFRange)range byAmount:(CFIndex)amount { |
| 371 CFRange incrementedRange = range; |
| 372 incrementedRange.length += amount; |
| 373 if (self.isRTL) |
| 374 incrementedRange.location += self.incrementAmount * amount; |
| 375 return incrementedRange; |
| 376 } |
| 377 |
| 378 - (CFRange)incrementRange:(CFRange)range { |
| 379 return [self incrementRange:range byAmount:1]; |
| 380 } |
| 381 |
| 382 - (CFRange)decrementRange:(CFRange)range { |
| 383 return [self incrementRange:range byAmount:-1]; |
| 384 } |
| 385 |
| 386 - (CFRange)updateRange:(CFRange)range |
| 387 forTrailingGlyphIdx:(CFIndex)trailingGlyphIdx { |
| 388 DCHECK(self.isRTL ? trailingGlyphIdx <= range.location + range.length |
| 389 : trailingGlyphIdx >= range.location); |
| 390 DCHECK([self glyphIdxIsValid:trailingGlyphIdx]); |
| 391 CFIndex currentTrailingGlyphIdx = [self trailingGlyphIdxForRange:range]; |
| 392 CFIndex updateAmount = self.isRTL |
| 393 ? currentTrailingGlyphIdx - trailingGlyphIdx |
| 394 : trailingGlyphIdx - currentTrailingGlyphIdx; |
| 395 return [self incrementRange:range byAmount:updateAmount]; |
| 396 } |
| 397 |
| 398 - (CFIndex)trailingGlyphIdxForRange:(CFRange)range { |
| 399 if (self.isRTL) |
| 400 return range.location; |
| 401 return range.location + range.length - 1; |
| 402 } |
| 403 |
| 404 - (CFIndex)trailingGlyphIdxForCurrentRun { |
| 405 return self.isRTL ? 0 : self.currentGlyphCount - 1; |
| 406 } |
| 407 |
| 408 - (void)frameCurrentRun { |
| 409 DCHECK(self.currentRun); |
| 410 DCHECK_LT(self.framedGlyphCount, self.currentGlyphCount); |
| 411 DCHECK_LT(self.currentLineWidth, self.boundingWidth); |
| 412 |
| 413 // Calculate the range that will fit in the remaining portion of the line. |
| 414 NSCharacterSet* whitespaceSet = |
| 415 [NSCharacterSet whitespaceAndNewlineCharacterSet]; |
| 416 CFIndex startGlyphIdx = self.isRTL |
| 417 ? self.currentGlyphCount - self.framedGlyphCount |
| 418 : self.framedGlyphCount; |
| 419 CFRange remainingRunRange = |
| 420 CFRangeMake(self.isRTL ? 0 : startGlyphIdx, |
| 421 self.currentGlyphCount - self.framedGlyphCount); |
| 422 CFRange range = CFRangeMake(startGlyphIdx, 0); |
| 423 while (remainingRunRange.length > 0) { |
| 424 // Find the range for the next word that can be added to the line. If no |
| 425 // delimiters were found, frame the rest of the run. |
| 426 CFIndex delimIdx = core_text_util::GetGlyphIdxForCharInSet( |
| 427 self.currentRun, remainingRunRange, self.string, self.lineEndSet); |
| 428 if (delimIdx == kCFNotFound) |
| 429 delimIdx = [self trailingGlyphIdxForCurrentRun]; |
| 430 CFRange wordGlyphRange = |
| 431 [self updateRange:remainingRunRange forTrailingGlyphIdx:delimIdx]; |
| 432 CFIndex wordFramedGlyphCount = wordGlyphRange.length; |
| 433 // Trim any whitespace and record its width. |
| 434 CGFloat wordTrailingWhitespaceWidth = 0.0; |
| 435 if ([whitespaceSet characterIsMember:[self charForGlyphAtIdx:delimIdx]]) { |
| 436 wordTrailingWhitespaceWidth = |
| 437 core_text_util::GetGlyphWidth(self.currentRun, delimIdx); |
| 438 wordGlyphRange = [self decrementRange:wordGlyphRange]; |
| 439 } |
| 440 // Check if the word will fit on the line. |
| 441 CGFloat wordWidth = |
| 442 core_text_util::GetRunWidthWithRange(self.currentRun, wordGlyphRange); |
| 443 CGFloat cumulativeLineWidth = |
| 444 self.currentLineWidth + self.currentWhitespaceWidth + wordWidth; |
| 445 if (cumulativeLineWidth <= self.boundingWidth) { |
| 446 // The word at |wordGlyphRange| fits on the line. |
| 447 self.currentLineWidth = cumulativeLineWidth; |
| 448 self.framedGlyphCount += wordFramedGlyphCount; |
| 449 self.currentWhitespaceWidth = wordTrailingWhitespaceWidth; |
| 450 remainingRunRange.length -= wordFramedGlyphCount; |
| 451 if (!self.isRTL) |
| 452 remainingRunRange.location += wordFramedGlyphCount; |
| 453 range = [self incrementRange:range byAmount:wordFramedGlyphCount]; |
| 454 } else { |
| 455 break; |
| 456 } |
| 457 } |
| 458 // Early return if no glyphs were framed. |
| 459 if (!range.length) |
| 460 return; |
| 461 // Use the string index of the next glyph to determine the string range for |
| 462 // the current line, since a glyph may correspond with multiple characters |
| 463 // when ligatures are used. |
| 464 CFIndex nextGlyphIdx = |
| 465 [self trailingGlyphIdxForRange:range] + self.incrementAmount; |
| 466 CFIndex nextGlyphStringIdx; |
| 467 if ([self glyphIdxIsValid:nextGlyphIdx]) { |
| 468 nextGlyphStringIdx = [self stringIdxForGlyphAtIdx:nextGlyphIdx]; |
| 469 } else { |
| 470 CFRange runStringRange = CTRunGetStringRange(self.currentRun); |
| 471 nextGlyphStringIdx = runStringRange.location + runStringRange.length; |
| 472 } |
| 473 self.currentLineRange = |
| 474 NSMakeRange(self.currentLineRange.location, |
| 475 nextGlyphStringIdx - self.currentLineRange.location); |
| 476 } |
| 477 |
| 478 - (unichar)charForGlyphAtIdx:(CFIndex)glyphIdx { |
| 479 DCHECK([self glyphIdxIsValid:glyphIdx]); |
| 480 return [self.string.string |
| 481 characterAtIndex:[self stringIdxForGlyphAtIdx:glyphIdx]]; |
| 482 } |
| 483 |
| 484 - (CFIndex)stringIdxForGlyphAtIdx:(CFIndex)glyphIdx { |
| 485 DCHECK([self glyphIdxIsValid:glyphIdx]); |
| 486 CFIndex stringIdx = 0; |
| 487 CTRunGetStringIndices(self.currentRun, CFRangeMake(glyphIdx, 1), &stringIdx); |
| 488 return stringIdx; |
| 489 } |
| 490 |
| 491 - (BOOL)runIdxIsValid:(CFIndex)runIdx { |
| 492 NSArray* runs = base::mac::CFToNSCast(CTLineGetGlyphRuns(self.line)); |
| 493 return runIdx >= 0 && runIdx < static_cast<CFIndex>(runs.count); |
| 494 } |
| 495 |
| 496 - (BOOL)glyphIdxIsValid:(CFIndex)glyphIdx { |
| 497 return glyphIdx >= 0 && glyphIdx < self.currentGlyphCount; |
| 498 } |
| 499 |
| 500 - (void)addCurrentLine { |
| 501 // Don't attempt to add a line if |currentLineRange| is empty. |
| 502 if (!self.currentLineRange.length) |
| 503 return; |
| 504 // Add the new line and its corresponding string range and baseline origin. |
| 505 NSAttributedString* currentLineString = |
| 506 [self.string attributedSubstringFromRange:self.currentLineRange]; |
| 507 CTLineRef currentLine = CTLineCreateWithAttributedString( |
| 508 base::mac::NSToCFCast(currentLineString)); |
| 509 [self.manualTextFrame addFramedLineWithLine:currentLine |
| 510 stringRange:self.currentLineRange |
| 511 origin:[self originForCurrentLine]]; |
| 512 CFRelease(currentLine); |
| 513 // Update bookkeeping variables for next line. |
| 514 CGFloat usedHeight = |
| 515 core_text_util::GetLineHeight(self.string, self.currentLineRange) + |
| 516 core_text_util::GetLineSpacing(self.string, self.currentLineRange); |
| 517 self.currentLineRange = NSMakeRange( |
| 518 self.currentLineRange.location + self.currentLineRange.length, 0); |
| 519 self.currentLineWidth = 0; |
| 520 self.currentWhitespaceWidth = 0; |
| 521 self.remainingHeight -= usedHeight; |
| 522 } |
| 523 |
| 524 - (CGPoint)originForCurrentLine { |
| 525 CGPoint origin = CGPointZero; |
| 526 CGFloat alignedWidth = |
| 527 AlignValueToPixel(self.currentLineWidth, AlignmentFunction::CEIL); |
| 528 switch (self.effectiveAlignment) { |
| 529 case NSTextAlignmentLeft: |
| 530 // Left-aligned lines begin at 0.0. |
| 531 break; |
| 532 case NSTextAlignmentRight: |
| 533 origin.x = AlignValueToPixel(self.boundingWidth - alignedWidth, |
| 534 AlignmentFunction::FLOOR); |
| 535 break; |
| 536 case NSTextAlignmentCenter: |
| 537 origin.x = AlignValueToPixel((self.boundingWidth - alignedWidth) / 2.0, |
| 538 AlignmentFunction::FLOOR); |
| 539 break; |
| 540 default: |
| 541 // Only left, right, and center effective alignment is supported. |
| 542 NOTREACHED(); |
| 543 break; |
| 544 } |
| 545 UIFont* font = [self.string attribute:NSFontAttributeName |
| 546 atIndex:self.currentLineRange.location |
| 547 effectiveRange:nullptr]; |
| 548 CGFloat lineHeight = |
| 549 core_text_util::GetLineHeight(self.string, self.currentLineRange); |
| 550 origin.y = |
| 551 AlignValueToPixel(self.remainingHeight - lineHeight - font.descender, |
| 552 AlignmentFunction::FLOOR); |
| 553 return origin; |
| 554 } |
| 555 |
| 556 @end |
| 557 |
| 558 #pragma mark - ManualTextFramer |
| 559 |
| 560 @implementation ManualTextFramer |
| 561 |
| 562 @synthesize bounds = _bounds; |
| 563 @synthesize boundingWidth = _boundingWidth; |
| 564 @synthesize remainingHeight = _remainingHeight; |
| 565 |
| 566 - (instancetype)initWithString:(NSAttributedString*)string |
| 567 inBounds:(CGRect)bounds { |
| 568 if ((self = [super init])) { |
| 569 DCHECK(string.string.length); |
| 570 _string.reset([string retain]); |
| 571 _bounds = bounds; |
| 572 _boundingWidth = CGRectGetWidth(bounds); |
| 573 _remainingHeight = CGRectGetHeight(bounds); |
| 574 } |
| 575 return self; |
| 576 } |
| 577 |
| 578 - (void)frameText { |
| 579 if (![self setupManualTextFrame]) |
| 580 return; |
| 581 NSRange framedRange = NSMakeRange(0, 0); |
| 582 NSArray* paragraphs = GetParagraphStringsForString(self.string); |
| 583 NSUInteger stringRangeOffset = 0; |
| 584 for (NSAttributedString* paragraph in paragraphs) { |
| 585 // Frame each paragraph using a ParagraphFramer, then update bookkeeping |
| 586 // variables for the top-level ManualTextFramer. |
| 587 CGRect remainingBounds = |
| 588 CGRectMake(0, 0, self.boundingWidth, self.remainingHeight); |
| 589 base::scoped_nsobject<ParagraphFramer> framer([[ParagraphFramer alloc] |
| 590 initWithString:paragraph |
| 591 inBounds:remainingBounds]); |
| 592 [framer frameText]; |
| 593 id<TextFrame> frame = [framer textFrame]; |
| 594 DCHECK(frame); |
| 595 framedRange.length += frame.framedRange.length; |
| 596 CGFloat paragraphHeight = 0.0; |
| 597 for (FramedLine* line in frame.lines) { |
| 598 NSRange lineRange = line.stringRange; |
| 599 lineRange.location += stringRangeOffset; |
| 600 [self.manualTextFrame addFramedLineWithLine:line.line |
| 601 stringRange:lineRange |
| 602 origin:line.origin]; |
| 603 paragraphHeight += core_text_util::GetLineHeight(self.string, lineRange) + |
| 604 core_text_util::GetLineSpacing(self.string, lineRange); |
| 605 } |
| 606 self.remainingHeight -= paragraphHeight; |
| 607 stringRangeOffset += paragraph.string.length; |
| 608 } |
| 609 self.manualTextFrame.framedRange = framedRange; |
| 610 } |
| 611 |
| 612 #pragma mark Accessors |
| 613 |
| 614 - (NSAttributedString*)string { |
| 615 return _string.get(); |
| 616 } |
| 617 |
| 618 - (ManualTextFrame*)manualTextFrame { |
| 619 return _manualTextFrame.get(); |
| 620 } |
| 621 |
| 622 - (id<TextFrame>)textFrame { |
| 623 return _manualTextFrame.get(); |
| 624 } |
| 625 |
| 626 #pragma mark Private |
| 627 |
| 628 - (BOOL)setupManualTextFrame { |
| 629 if (_manualTextFrame) |
| 630 return NO; |
| 631 _manualTextFrame.reset([[ManualTextFrame alloc] initWithString:self.string |
| 632 inBounds:self.bounds]); |
| 633 return YES; |
| 634 } |
| 635 |
| 636 @end |
OLD | NEW |