CYRLayoutManager.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. //
  2. // CYRLayoutManager.h
  3. //
  4. // Version 0.2.0
  5. //
  6. // Created by Illya Busigin on 01/05/2014.
  7. // Copyright (c) 2014 Cyrillian, Inc.
  8. //
  9. // Distributed under MIT license.
  10. // Get the latest version from here:
  11. //
  12. // https://github.com/illyabusigin/CYRTextView
  13. // Original implementation taken from: https://github.com/alldritt/TextKit_LineNumbers
  14. //
  15. // The MIT License (MIT)
  16. //
  17. // Copyright (c) 2014 Cyrillian, Inc.
  18. //
  19. // Permission is hereby granted, free of charge, to any person obtaining a copy of
  20. // this software and associated documentation files (the "Software"), to deal in
  21. // the Software without restriction, including without limitation the rights to
  22. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  23. // the Software, and to permit persons to whom the Software is furnished to do so,
  24. // subject to the following conditions:
  25. //
  26. // The above copyright notice and this permission notice shall be included in all
  27. // copies or substantial portions of the Software.
  28. //
  29. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  30. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  31. // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  32. // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  33. // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  34. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  35. #import "CYRLayoutManager.h"
  36. static CGFloat kMinimumGutterWidth = 30.f;
  37. @interface CYRLayoutManager ()
  38. @property (nonatomic, assign) CGFloat gutterWidth;
  39. @property (nonatomic, assign) UIEdgeInsets lineAreaInset;
  40. @property (nonatomic) NSUInteger lastParaLocation;
  41. @property (nonatomic) NSUInteger lastParaNumber;
  42. @end
  43. @implementation CYRLayoutManager
  44. #pragma mark - Initialization & Setup
  45. - (instancetype)init
  46. {
  47. self = [super init];
  48. if (self)
  49. {
  50. [self _commonSetup];
  51. }
  52. return self;
  53. }
  54. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  55. {
  56. self = [super initWithCoder:aDecoder];
  57. if (self)
  58. {
  59. [self _commonSetup];
  60. }
  61. return self;
  62. }
  63. - (void)_commonSetup
  64. {
  65. self.gutterWidth = kMinimumGutterWidth;
  66. self.selectedRange = NSMakeRange(0, 0);
  67. self.lineAreaInset = UIEdgeInsetsMake(0, 10, 0, 4);
  68. self.lineNumberColor = [UIColor grayColor];
  69. self.lineNumberFont = [UIFont systemFontOfSize:10.0f];
  70. }
  71. #pragma mark - Convenience
  72. - (CGRect)paragraphRectForRange:(NSRange)range
  73. {
  74. range = [self.textStorage.string paragraphRangeForRange:range];
  75. range = [self glyphRangeForCharacterRange:range actualCharacterRange:NULL];
  76. CGRect startRect = [self lineFragmentRectForGlyphAtIndex:range.location effectiveRange:NULL];
  77. CGRect endRect = [self lineFragmentRectForGlyphAtIndex:range.location + range.length - 1 effectiveRange:NULL];
  78. CGRect paragraphRectForRange = CGRectUnion(startRect, endRect);
  79. paragraphRectForRange = CGRectOffset(paragraphRectForRange, _gutterWidth, 8);
  80. return paragraphRectForRange;
  81. }
  82. - (NSUInteger) _paraNumberForRange:(NSRange) charRange
  83. {
  84. // NSString does not provide a means of efficiently determining the paragraph number of a range of text. This code
  85. // attempts to optimize what would normally be a series linear searches by keeping track of the last paragraph number
  86. // found and uses that as the starting point for next paragraph number search. This works (mostly) because we
  87. // are generally asked for continguous increasing sequences of paragraph numbers. Also, this code is called in the
  88. // course of drawing a pagefull of text, and so even when moving back, the number of paragraphs to search for is
  89. // relativly low, even in really long bodies of text.
  90. //
  91. // This all falls down when the user edits the text, and can potentially invalidate the cached paragraph number which
  92. // causes a (potentially lengthy) search from the beginning of the string.
  93. if (charRange.location == self.lastParaLocation)
  94. return self.lastParaNumber;
  95. else if (charRange.location < self.lastParaLocation)
  96. {
  97. // We need to look backwards from the last known paragraph for the new paragraph range. This generally happens
  98. // when the text in the UITextView scrolls downward, revaling paragraphs before/above the ones previously drawn.
  99. NSString* s = self.textStorage.string;
  100. __block NSUInteger paraNumber = self.lastParaNumber;
  101. [s enumerateSubstringsInRange:NSMakeRange(charRange.location, self.lastParaLocation - charRange.location)
  102. options:NSStringEnumerationByParagraphs |
  103. NSStringEnumerationSubstringNotRequired |
  104. NSStringEnumerationReverse
  105. usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop){
  106. if (enclosingRange.location <= charRange.location) {
  107. *stop = YES;
  108. }
  109. --paraNumber;
  110. }];
  111. self.lastParaLocation = charRange.location;
  112. self.lastParaNumber = paraNumber;
  113. return paraNumber;
  114. }
  115. else
  116. {
  117. // We need to look forward from the last known paragraph for the new paragraph range. This generally happens
  118. // when the text in the UITextView scrolls upwards, revealing paragraphs that follow the ones previously drawn.
  119. NSString* s = self.textStorage.string;
  120. __block NSUInteger paraNumber = self.lastParaNumber;
  121. [s enumerateSubstringsInRange:NSMakeRange(self.lastParaLocation, charRange.location - self.lastParaLocation)
  122. options:NSStringEnumerationByParagraphs | NSStringEnumerationSubstringNotRequired
  123. usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop){
  124. if (enclosingRange.location >= charRange.location) {
  125. *stop = YES;
  126. }
  127. ++paraNumber;
  128. }];
  129. self.lastParaLocation = charRange.location;
  130. self.lastParaNumber = paraNumber;
  131. return paraNumber;
  132. }
  133. }
  134. #pragma mark - Layouting
  135. - (void)processEditingForTextStorage:(NSTextStorage *)textStorage edited:(NSTextStorageEditActions)editMask range:(NSRange)newCharRange changeInLength:(NSInteger)delta invalidatedRange:(NSRange)invalidatedCharRange
  136. {
  137. [super processEditingForTextStorage:textStorage edited:editMask range:newCharRange changeInLength:delta invalidatedRange:invalidatedCharRange];
  138. if (invalidatedCharRange.location < self.lastParaLocation)
  139. {
  140. // When the backing store is edited ahead the cached paragraph location, invalidate the cache and force a complete
  141. // recalculation. We cannot be much smarter than this because we don't know how many paragraphs have been deleted
  142. // since the text has already been removed from the backing store.
  143. self.lastParaLocation = 0;
  144. self.lastParaNumber = 0;
  145. }
  146. }
  147. #pragma mark - Drawing
  148. - (void) drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin
  149. {
  150. [super drawBackgroundForGlyphRange:glyphsToShow atPoint:origin];
  151. // Draw line numbers. Note that the background for line number gutter is drawn by the LineNumberTextView class.
  152. NSDictionary* atts = @{NSFontAttributeName : _lineNumberFont ,
  153. NSForegroundColorAttributeName : _lineNumberColor};
  154. [self enumerateLineFragmentsForGlyphRange:glyphsToShow
  155. usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop) {
  156. NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
  157. NSRange paraRange = [self.textStorage.string paragraphRangeForRange:charRange];
  158. BOOL showCursorRect = NSLocationInRange(self->_selectedRange.location, paraRange);
  159. if (showCursorRect)
  160. {
  161. CGContextRef context = UIGraphicsGetCurrentContext();
  162. CGRect cursorRect = CGRectMake(0, usedRect.origin.y + 8, self->_gutterWidth, usedRect.size.height);
  163. CGContextSetFillColorWithColor(context, [UIColor colorWithWhite:0.9 alpha:1].CGColor);
  164. CGContextFillRect(context, cursorRect);
  165. }
  166. // Only draw line numbers for the paragraph's first line fragment. Subsequent fragments are wrapped portions of the paragraph and don't get the line number.
  167. if (charRange.location == paraRange.location) {
  168. CGRect gutterRect = CGRectOffset(CGRectMake(0, rect.origin.y, self->_gutterWidth, rect.size.height), origin.x, origin.y);
  169. NSUInteger paraNumber = [self _paraNumberForRange:charRange];
  170. NSString* ln = [NSString stringWithFormat:@"%ld", (unsigned long) paraNumber + 1];
  171. CGSize size = [ln sizeWithAttributes:atts];
  172. [ln drawInRect:CGRectOffset(gutterRect, CGRectGetWidth(gutterRect) - self->_lineAreaInset.right - size.width - self->_gutterWidth, (CGRectGetHeight(gutterRect) - size.height) / 2.0)
  173. withAttributes:atts];
  174. }
  175. }];
  176. }
  177. @end