API  1.0.0
CPTypesetter.j
Go to the documentation of this file.
1 /*
2  * CPTypesetter.j
3  * AppKit
4  *
5  * Created by Daniel Boehringer on 27/12/2013.
6  * All modifications copyright Daniel Boehringer 2013.
7  * Extensive code formatting and review by Andrew Hankinson
8  * Based on original work by
9  * Emmanuel Maillard on 27/02/2010.
10  * Copyright Emmanuel Maillard 2010.
11  *
12  * This library is free software; you can redistribute it and/or
13  * modify it under the terms of the GNU Lesser General Public
14  * License as published by the Free Software Foundation; either
15  * version 2.1 of the License, or (at your option) any later version.
16  *
17  * This library is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20  * Lesser General Public License for more details.
21  *
22  * You should have received a copy of the GNU Lesser General Public
23  * License along with this library; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
25  */
26 
27 
28 
29 // forward declare these classes for type matching
30 
31 /*
32  CPTypesetterControlCharacterAction
33 */
40 
42  _sharedSimpleTypesetter;
43 @implementation CPTypesetter : CPObject
44 {
45  id __doxygen__;
46 }
47 
48 
49 #pragma mark -
50 #pragma mark Class methods
51 
52 + (void)initialize
53 {
54  [CPTypesetter _setSystemTypesetterFactory:[CPSimpleTypesetter class]];
55 }
56 
57 + (id)sharedSystemTypesetter
58 {
59  return [CPSystemTypesetterFactory sharedInstance];
60 }
61 
62 + (void)_setSystemTypesetterFactory:(Class)aClass
63 {
65 }
66 
67 - (CPTypesetterControlCharacterAction)actionForControlCharacterAtIndex:(unsigned)charIndex
68 {
70 }
71 
72 - (CPLayoutManager)layoutManager
73 {
74  return nil;
75 }
76 
77 - (CPTextContainer)currentTextContainer
78 {
79  return nil;
80 }
81 
82 - (CPArray)textContainers
83 {
84  return nil;
85 }
86 
87 - (void)layoutGlyphsInLayoutManager:(CPLayoutManager)layoutManager
88  startingAtGlyphIndex:(unsigned)startGlyphIndex
89  maxNumberOfLineFragments:(unsigned)maxNumLines
90  nextGlyphIndex:(UIntegerReference)nextGlyph
91 {
92  _CPRaiseInvalidAbstractInvocation(self, _cmd);
93 }
94 
95 @end
96 
97 @implementation CPSimpleTypesetter : CPTypesetter
98 {
99  CPLayoutManager _layoutManager;
100  CPTextContainer _currentTextContainer;
101  CPTextStorage _textStorage;
102 
103  CPRange _attributesRange;
104  CPDictionary _currentAttributes;
105  CPParagraphStyle _currentParagraph;
106 
107  float _lineHeight;
108  float _lineBase;
109  float _lineWidth;
110 
111  unsigned _indexOfCurrentContainer;
112 
113  CPArray _lineFragments;
114 }
115 
116 
117 #pragma mark -
118 #pragma mark Class methods
119 
120 + (id)sharedInstance
121 {
122  if (!_sharedSimpleTypesetter)
123  _sharedSimpleTypesetter = [[CPSimpleTypesetter alloc] init];
124 
125  return _sharedSimpleTypesetter;
126 }
127 
128 - (CPArray)textContainers
129 {
130  return [_layoutManager textContainers];
131 }
132 
133 - (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
134 {
135  var tabStops = [_currentParagraph tabStops];
136 
137  if (!tabStops)
138  tabStops = [CPParagraphStyle _defaultTabStops];
139 
140  var l = tabStops.length;
141 
142  if (aWidth > tabStops[l - 1]._location)
143  return nil;
144 
145  for (var i = l - 1; i >= 0; i--)
146  {
147  if (aWidth > tabStops[i]._location)
148  {
149  if (i + 1 < l)
150  return tabStops[i + 1];
151  }
152  }
153 
154  if (i === -1)
155  return tabStops[0];
156 
157  return nil;
158 }
159 
160 - (BOOL)_flushRange:(CPRange)lineRange
161  lineOrigin:(CGPoint)lineOrigin
162  currentContainer:(CPTextContainer)aContainer
163  advancements:(CPArray)advancements
164  lineCount:(unsigned)lineCount
165  sameLine:(BOOL)sameLine
166 {
167  var myX = 0,
168  rect = CGRectMake(lineOrigin.x, lineOrigin.y, _lineWidth, _lineHeight),
169  containerSize = aContainer._size;
170 
171  [_layoutManager _appendNewLineFragmentInTextContainer:_currentTextContainer forGlyphRange:lineRange];
172 
173  var fragment = [_layoutManager._lineFragments lastObject];
174  fragment._isLast = !sameLine;
175  _lineFragments.push(fragment);
176 
177  [_layoutManager setLineFragmentRect:rect forGlyphRange:lineRange usedRect:rect];
178 
179  switch ([_currentParagraph alignment])
180  {
181  case CPLeftTextAlignment:
182  myX = 0;
183  break;
184 
186  myX = (containerSize.width - _lineWidth) / 2;
187  break;
188 
190  myX = containerSize.width - _lineWidth;
191  break;
192  }
193 
194  [_layoutManager setLocation:CGPointMake(myX, _lineBase) forStartOfGlyphRange:lineRange];
195  [_layoutManager _setAdvancements:advancements forGlyphRange:lineRange];
196 
197  if (!sameLine) //fix the _lineFragments when fontsizes differ
198  {
199  var l = _lineFragments.length;
200 
201  for (var i = 0 ; i < l ; i++)
202  [_lineFragments[i] _adjustForHeight:_lineHeight];
203  }
204 
205  if (!lineCount) // do not rescue on first line
206  return NO;
207 
208  if (aContainer._inResizing)
209  return NO;
210 
211  return ([_layoutManager _rescuingInvalidFragmentsWasPossibleForGlyphRange:lineRange]);
212 }
213 
214 - (void)layoutGlyphsInLayoutManager:(CPLayoutManager)layoutManager
215  startingAtGlyphIndex:(unsigned)glyphIndex
216  maxNumberOfLineFragments:(unsigned)maxNumLines
217  nextGlyphIndex:(UIntegerReference)nextGlyph
218 {
219  var textContainers = [layoutManager textContainers],
220  textContainersCount = [textContainers count];
221 
222  _layoutManager = layoutManager;
223  _textStorage = [_layoutManager textStorage];
224  _indexOfCurrentContainer = MAX(0, [textContainers
225  indexOfObject:[_layoutManager textContainerForGlyphAtIndex:glyphIndex effectiveRange:nil withoutAdditionalLayout:YES]
226  inRange:CPMakeRange(0, textContainersCount)]);
227 
228  _currentTextContainer = textContainers[_indexOfCurrentContainer];
229 
230  _attributesRange = CPMakeRange(0, 0);
231  _lineHeight = 0;
232  _lineBase = 0;
233  _lineWidth = 0;
234 
235  var containerSize = [_currentTextContainer containerSize],
236  containerSizeWidth = containerSize.width,
237  containerSizeHeight = containerSize.height,
238  lineRange = CPMakeRange(glyphIndex, 0),
239  wrapRange = CPMakeRange(0, 0),
240  wrapWidth = 0,
241  isNewline = NO,
242  isTabStop = NO,
243  isWordWrapped = NO,
244  numberOfGlyphs= [_textStorage length],
245  leading,
246  numLines = 0,
247  theString = [_textStorage string],
248  lineOrigin,
249  ascent,
250  descent,
251  advancements = [],
252  prevRangeWidth = 0,
253  measuringRange = CPMakeRange(glyphIndex, 0),
254  currentAnchor = 0,
255  currentFont,
256  currentFontLineHeight,
257  previousFont,
258  currentParagraphMinimumLineHeight,
259  currentParagraphMaximumLineHeight,
260  currentParagraphLineSpacing;
261 
262  if (glyphIndex > 0)
263  lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
264  else if ([_layoutManager extraLineFragmentTextContainer])
265  lineOrigin = CGPointMake(0, [_layoutManager extraLineFragmentUsedRect].origin.y);
266  else
267  lineOrigin = CGPointMake(0, 0);
268 
269  [_layoutManager _removeInvalidLineFragments];
270 
271  if (![_textStorage length])
272  return;
273 
274  _lineFragments = [];
275 
276  for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++)
277  {
278  // check whether there any change in the attributes from here on
279  if (!CPLocationInRange(glyphIndex, _attributesRange))
280  {
281  _currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange];
282  currentFont = [_currentAttributes objectForKey:CPFontAttributeName];
283  _currentParagraph = [_currentAttributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle];
284  currentParagraphMinimumLineHeight = [_currentParagraph minimumLineHeight];
285  currentParagraphMaximumLineHeight = [_currentParagraph maximumLineHeight];
286  currentParagraphLineSpacing = [_currentParagraph lineSpacing];
287 
288  if (!currentFont)
289  currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0];
290 
291  ascent = [currentFont ascender];
292  descent = [currentFont descender];
293  leading = (ascent - descent) * 0.2; // FAKE leading
294 
295  currentFontLineHeight = ascent - descent + leading;
296 
297  if (previousFont !== currentFont)
298  {
299  measuringRange = CPMakeRange(glyphIndex, 0);
300  currentAnchor = prevRangeWidth;
301  previousFont = currentFont;
302  }
303 
304  }
305 
306  if (currentFontLineHeight > _lineHeight)
307  _lineHeight = currentFontLineHeight;
308 
309  if (ascent > _lineBase)
310  _lineBase = ascent;
311 
312  lineRange.length++;
313  measuringRange.length++;
314 
315  var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons
316  rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) _sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
317 
318  switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char.
319  {
320  case 9: // '\t'
321  {
322  var nextTab = [self textTabForWidth:rangeWidth + lineOrigin.x writingDirection:0];
323 
324  isTabStop = YES;
325 
326  if (nextTab)
327  rangeWidth = nextTab._location - lineOrigin.x;
328  else
329  rangeWidth += 28; //FIXME
330  } // fallthrough intentional
331  case 32: // ' '
332  wrapRange = CPMakeRangeCopy(lineRange);
333  wrapWidth = rangeWidth;
334  wrapRange._height = _lineHeight;
335  wrapRange._base = _lineBase;
336  break;
337 
338  case 10:
339  case 13:
340  isNewline = YES;
341  }
342 
343  advancements.push({width: rangeWidth - prevRangeWidth, height: ascent, descent: descent});
344 
345  prevRangeWidth = _lineWidth = rangeWidth;
346 
347  if (lineOrigin.x + rangeWidth > containerSizeWidth)
348  {
349  if (wrapWidth)
350  {
351  lineRange = wrapRange;
352  _lineWidth = wrapWidth;
353  _lineHeight = wrapRange._height;
354  _lineBase = wrapRange._base;
355  }
356 
357  isNewline = YES;
358  isWordWrapped = YES;
359  glyphIndex = CPMaxRange(lineRange) - 1; // start the line starts directly at current character
360  }
361 
362  if (isNewline || isTabStop)
363  {
364  if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline])
365  return;
366 
367  if (isTabStop)
368  {
369  lineOrigin.x += rangeWidth;
370  isTabStop = NO;
371  }
372 
373  if (isNewline)
374  {
375  if (currentParagraphMinimumLineHeight && currentParagraphMinimumLineHeight > _lineHeight)
376  _lineHeight = currentParagraphMinimumLineHeight;
377 
378  if (currentParagraphMaximumLineHeight && currentParagraphMaximumLineHeight < _lineHeight)
379  _lineHeight = currentParagraphMaximumLineHeight;
380 
381  lineOrigin.y += _lineHeight;
382 
383  if (currentParagraphLineSpacing)
384  lineOrigin.y += currentParagraphLineSpacing;
385 
386  if (lineOrigin.y > containerSizeHeight && _indexOfCurrentContainer < textContainersCount - 1)
387  {
388  _currentTextContainer = textContainers[++_indexOfCurrentContainer];
389  containerSize = [_currentTextContainer containerSize];
390  containerSizeWidth = containerSize.width;
391  containerSizeHeight = containerSize.height;
392  }
393 
394  lineOrigin.x = 0;
395  numLines++;
396  isNewline = NO;
397  _lineFragments = [];
398  _lineHeight = 0;
399  _lineBase = ascent;
400  }
401 
402  _lineWidth = 0;
403  advancements = [];
404  currentAnchor = 0;
405  prevRangeWidth = 0;
406  lineRange = CPMakeRange(glyphIndex + 1, 0);
407  measuringRange = CPMakeRange(glyphIndex + 1, 0);
408  wrapRange = CPMakeRange(0, 0);
409  wrapWidth = 0;
410  isWordWrapped = NO;
411  }
412  }
413 
414  // this is to "flush" the remaining characters
415  if (lineRange.length)
416  {
417  [self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];
418  }
419 
420  var rect = CGRectMake(0, lineOrigin.y, containerSizeWidth, [_layoutManager._lineFragments lastObject]._usedRect.size.height - descent);
421  [_layoutManager setExtraLineFragmentRect:rect usedRect:rect textContainer:_currentTextContainer];
422 }
423 
424 @end
425 
427 
431 - (CPLayoutManager)layoutManager
432 {
433  return _layoutManager;
434 }
435 
439 - (void)setLayoutManager:(CPLayoutManager)aValue
440 {
441  _layoutManager = aValue;
442 }
443 
447 - (CPTextContainer)currentTextContainer
448 {
449  return _currentTextContainer;
450 }
451 
455 - (void)setCurrentTextContainer:(CPTextContainer)aValue
456 {
457  _currentTextContainer = aValue;
458 }
459 
460 @end
Definition: CPFont.h:2
CPFont systemFontOfSize:(CGSize aSize)
Definition: CPFont.j:282
CPRightTextAlignment
Definition: CPText.j:77
CPTypesetterLineBreakAction
Definition: CPTypesetter.j:37
int width
CPTypesetterZeroAdvancementAction
Definition: CPTypesetter.j:34
A mutable key-value pair collection.
Definition: CPDictionary.h:2
var CPSystemTypesetterFactory
Definition: CPTypesetter.j:41
CPSTypesetterHorizontalTabAction
Definition: CPTypesetter.j:36
function CPMaxRange(aRange)
Definition: CPRange.j:70
CPParagraphStyle defaultParagraphStyle()
CPTypesetterWhitespaceAction
Definition: CPTypesetter.j:35
function CPMakeRangeCopy(aRange)
Definition: CPRange.j:48
function CPLocationInRange(aLocation, aRange)
Definition: CPRange.j:93
CPTextAlignment CPLeftTextAlignment
Definition: CPText.j:76
Class class()
Definition: CPObject.j:179
CPCenterTextAlignment
Definition: CPText.j:78
CPTypesetterParagraphBreakAction
Definition: CPTypesetter.j:38
CPRange function CPMakeRange(location, length)
Definition: CPRange.j:37
CPTypesetterContainerBreakAction
Definition: CPTypesetter.j:39
CPTextTab textTabForWidth:writingDirection:(double aWidth, [writingDirection] CPWritingDirection direction)
Definition: CPTypesetter.j:133
id alloc()
Definition: CPObject.j:130