Fixed bug where bold/italic font didn't render at the right width
[MacVim/jjgod.git] / MMTextStorage.m
blob8694d54195ff67bc727b7399ec356061bdffd304
1 /* vi:set ts=8 sts=4 sw=4 ft=objc:
2  *
3  * VIM - Vi IMproved            by Bram Moolenaar
4  *                              MacVim GUI port by Bjorn Winckler
5  *
6  * Do ":help uganda"  in Vim to read copying and usage conditions.
7  * Do ":help credits" in Vim to see a list of people who contributed.
8  * See README.txt for an overview of the Vim source code.
9  */
11 #import "MMTextStorage.h"
12 #import "MacVim.h"
17 // TODO: support DRAW_TRANSP flag
18 #define DRAW_TRANSP               0x01    /* draw with transparant bg */
19 #define DRAW_BOLD                 0x02    /* draw bold text */
20 #define DRAW_UNDERL               0x04    /* draw underline text */
21 #define DRAW_UNDERC               0x08    /* draw undercurl text */
22 #define DRAW_ITALIC               0x10    /* draw italic text */
27 @interface MMTextStorage (Private)
28 - (void)lazyResize;
29 @end
33 @implementation MMTextStorage
35 - (id)init
37     if ((self = [super init])) {
38         attribString = [[NSMutableAttributedString alloc] initWithString:@""];
39         // NOTE!  It does not matter which font is set here, Vim will set its
40         // own font on startup anyway.  Just set some bogus values.
41         font = [[NSFont userFixedPitchFontOfSize:0] retain];
42         cellSize.height = [font pointSize];
43         cellSize.width = [font defaultLineHeightForFont];
44     }
46     return self;
49 - (void)dealloc
51     //NSLog(@"%@ %s", [self className], _cmd);
53     [emptyRowString release];
54     [boldItalicFont release];
55     [italicFont release];
56     [boldFont release];
57     [font release];
58     [defaultBackgroundColor release];
59     [defaultForegroundColor release];
60     [attribString release];
61     [super dealloc];
64 - (NSString *)string
66     //NSLog(@"%s : attribString=%@", _cmd, attribString);
67     return [attribString string];
70 - (NSDictionary *)attributesAtIndex:(unsigned)index
71                      effectiveRange:(NSRangePointer)range
73     //NSLog(@"%s", _cmd);
74     if (index>=[attribString length]) {
75         //NSLog(@"%sWARNING: index (%d) out of bounds", _cmd, index);
76         if (range) {
77             *range = NSMakeRange(NSNotFound, 0);
78         }
79         return [NSDictionary dictionary];
80     }
82     return [attribString attributesAtIndex:index effectiveRange:range];
85 - (void)replaceCharactersInRange:(NSRange)range
86                       withString:(NSString *)string
88     //NSLog(@"replaceCharactersInRange:(%d,%d) withString:%@", range.location,
89     //        range.length, string);
90     NSLog(@"WARNING: calling %s on MMTextStorage is unsupported", _cmd);
91     //[attribString replaceCharactersInRange:range withString:string];
94 - (void)setAttributes:(NSDictionary *)attributes range:(NSRange)range
96     // NOTE!  This method must be implemented since the text system calls it
97     // constantly to 'fix attributes', apply font substitution, etc.
98 #if 0
99     [attribString setAttributes:attributes range:range];
100 #else
101     // HACK! If the font attribute is being modified, then ensure that the new
102     // font has a fixed advancement which is either the same as the current
103     // font or twice that, depending on whether it is a 'wide' character that
104     // is being fixed or not.  This code really only works if 'range' has
105     // length 1 or 2.
106     NSFont *newFont = [attributes objectForKey:NSFontAttributeName];
107     if (newFont) {
108         float adv = cellSize.width;
109         if ([attribString length] > range.location+1) {
110             // If the first char is followed by zero-width space, then it is a
111             // 'wide' character, so double the advancement.
112             NSString *string = [attribString string];
113             if ([string characterAtIndex:range.location+1] == 0x200b)
114                 adv += adv;
115         }
117         // Create a new font which has the 'fixed advance attribute' set.
118         NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
119             [NSNumber numberWithFloat:adv], NSFontFixedAdvanceAttribute, nil];
120         NSFontDescriptor *desc = [newFont fontDescriptor];
121         desc = [desc fontDescriptorByAddingAttributes:dict];
122         newFont = [NSFont fontWithDescriptor:desc size:[newFont pointSize]];
124         // Now modify the 'attributes' dictionary to hold the new font.
125         NSMutableDictionary *newAttr = [NSMutableDictionary
126             dictionaryWithDictionary:attributes];
127         [newAttr setObject:newFont forKey:NSFontAttributeName];
129         [attribString setAttributes:newAttr range:range];
130     } else {
131         [attribString setAttributes:attributes range:range];
132     }
133 #endif
136 - (int)maxRows
138     return maxRows;
141 - (int)maxColumns
143     return maxColumns;
146 - (void)getMaxRows:(int*)rows columns:(int*)cols
148     if (rows) *rows = maxRows;
149     if (cols) *cols = maxColumns;
152 - (void)setMaxRows:(int)rows columns:(int)cols
154     // NOTE: Just remember the new values, the actual resizing is done lazily.
155     maxRows = rows;
156     maxColumns = cols;
159 - (void)replaceString:(NSString *)string atRow:(int)row column:(int)col
160             withFlags:(int)flags foregroundColor:(NSColor *)fg
161       backgroundColor:(NSColor *)bg specialColor:(NSColor *)sp
163     //NSLog(@"replaceString:atRow:%d column:%d withFlags:%d", row, col, flags);
164     [self lazyResize];
166     // TODO: support DRAW_TRANSP
167     if (flags & DRAW_TRANSP)
168         return;
170     if (row < 0 || row >= maxRows || col < 0 || col >= maxColumns
171             || col+[string length] > maxColumns) {
172         //NSLog(@"[%s] WARNING : out of range, row=%d (%d) col=%d (%d) "
173         //        "length=%d (%d)", _cmd, row, maxRows, col, maxColumns,
174         //        [string length], [attribString length]);
175         return;
176     }
178     // NOTE: If 'string' was initialized with bad data it might be nil; this
179     // may be due to 'enc' being set to an unsupported value, so don't print an
180     // error message or stdout will most likely get flooded.
181     if (!string) return;
183     if (!(fg && bg && sp)) {
184         NSLog(@"[%s] WARNING: background, foreground or special color not "
185                 "specified", _cmd);
186         return;
187     }
189     NSRange range = NSMakeRange(col+row*(maxColumns+1), [string length]);
190     [attribString replaceCharactersInRange:range withString:string];
192     NSFont *theFont = font;
193     if (flags & DRAW_BOLD)
194         theFont = flags & DRAW_ITALIC ? boldItalicFont : boldFont;
195     else if (flags & DRAW_ITALIC)
196         theFont = italicFont;
198     NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
199             theFont, NSFontAttributeName,
200             bg, NSBackgroundColorAttributeName,
201             fg, NSForegroundColorAttributeName,
202             sp, NSUnderlineColorAttributeName,
203             nil];
204     [attribString setAttributes:attributes range:range];
206     if (flags & DRAW_UNDERL) {
207         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleSingle
208                 | NSUnderlinePatternSolid)]; // | NSUnderlineByWordMask
209         [attribString addAttribute:NSUnderlineStyleAttributeName
210                 value:value range:range];
211     }
213     // TODO: figure out how do draw proper undercurls
214     if (flags & DRAW_UNDERC) {
215         NSNumber *value = [NSNumber numberWithInt:(NSUnderlineStyleThick
216                 | NSUnderlinePatternDot)]; // | NSUnderlineByWordMask
217         [attribString addAttribute:NSUnderlineStyleAttributeName
218                 value:value range:range];
219     }
221     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
222            range:range changeInLength:0];
226  * Delete 'count' lines from 'row' and insert 'count' empty lines at the bottom
227  * of the scroll region.
228  */
229 - (void)deleteLinesFromRow:(int)row lineCount:(int)count
230               scrollBottom:(int)bottom left:(int)left right:(int)right
231                      color:(NSColor *)color
233     //NSLog(@"deleteLinesFromRow:%d lineCount:%d", row, count);
234     [self lazyResize];
236     if (row < 0 || row+count > maxRows) {
237         //NSLog(@"[%s] WARNING : out of range, row=%d (%d) count=%d", _cmd, row,
238         //        maxRows, count);
239         return;
240     }
242     int total = 1 + bottom - row;
243     int move = total - count;
244     int width = right - left + 1;
245     NSRange destRange = { row*(maxColumns+1) + left, width };
246     NSRange srcRange = { (row+count)*(maxColumns+1) + left, width };
247     int i;
249     for (i = 0; i < move; ++i) {
250         NSAttributedString *srcString = [attribString
251                 attributedSubstringFromRange:srcRange];
252         [attribString replaceCharactersInRange:destRange
253                           withAttributedString:srcString];
254         [self edited:(NSTextStorageEditedCharacters
255                 | NSTextStorageEditedAttributes)
256                 range:destRange changeInLength:0];
257         destRange.location += maxColumns+1;
258         srcRange.location += maxColumns+1;
259     }
261     for (i = 0; i < count; ++i) {
262         NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
263                 font, NSFontAttributeName,
264                 color, NSForegroundColorAttributeName,
265                 color, NSBackgroundColorAttributeName, nil];
266         [attribString setAttributes:attribs range:destRange];
267         [self edited:NSTextStorageEditedAttributes range:destRange
268                 changeInLength:0];
269         destRange.location += maxColumns+1;
270     }
274  * Insert 'count' empty lines at 'row' and delete 'count' lines from the bottom
275  * of the scroll region.
276  */
277 - (void)insertLinesAtRow:(int)row lineCount:(int)count
278             scrollBottom:(int)bottom left:(int)left right:(int)right
279                    color:(NSColor *)color
281     //NSLog(@"insertLinesAtRow:%d lineCount:%d", row, count);
282     [self lazyResize];
284     if (row < 0 || row+count > maxRows) {
285         //NSLog(@"[%s] WARNING : out of range, row=%d (%d) count=%d", _cmd, row,
286         //        maxRows, count);
287         return;
288     }
290     int total = 1 + bottom - row;
291     int move = total - count;
292     int width = right - left + 1;
293     NSRange destRange = { bottom*(maxColumns+1) + left, width };
294     NSRange srcRange = { (row+move-1)*(maxColumns+1) + left, width };
295     int i;
297     for (i = 0; i < move; ++i) {
298         NSAttributedString *srcString = [attribString
299                 attributedSubstringFromRange:srcRange];
300         [attribString replaceCharactersInRange:destRange
301                           withAttributedString:srcString];
302         [self edited:(NSTextStorageEditedCharacters
303                 | NSTextStorageEditedAttributes)
304                 range:destRange changeInLength:0];
305         destRange.location -= maxColumns+1;
306         srcRange.location -= maxColumns+1;
307     }
309     for (i = 0; i < count; ++i) {
310         NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
311                 font, NSFontAttributeName,
312                 color, NSForegroundColorAttributeName,
313                 color, NSBackgroundColorAttributeName, nil];
314         [attribString setAttributes:attribs range:destRange];
315         [self edited:NSTextStorageEditedAttributes range:destRange
316                 changeInLength:0];
317         destRange.location -= maxColumns+1;
318     }
321 - (void)clearBlockFromRow:(int)row1 column:(int)col1 toRow:(int)row2
322                    column:(int)col2 color:(NSColor *)color
324     //NSLog(@"clearBlockFromRow:%d column:%d toRow:%d column:%d", row1, col1,
325     //        row2, col2);
326     [self lazyResize];
328     if (row1 < 0 || row2 >= maxRows || col1 < 0 || col2 > maxColumns) {
329         //NSLog(@"[%s] WARNING : out of range, row1=%d row2=%d (%d) col1=%d "
330         //        "col2=%d (%d)", _cmd, row1, row2, maxRows, col1, col2,
331         //        maxColumns);
332         return;
333     }
335     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
336             font, NSFontAttributeName,
337             color, NSForegroundColorAttributeName,
338             color, NSBackgroundColorAttributeName, nil];
340     NSRange range = { row1*(maxColumns+1) + col1, col2-col1+1 };
342     int r;
343     for (r=row1; r<=row2; ++r) {
344         [attribString setAttributes:attribs range:range];
345         [self edited:NSTextStorageEditedAttributes range:range
346                 changeInLength:0];
347         range.location += maxColumns+1;
348     }
351 - (void)clearAllWithColor:(NSColor *)color
353     //NSLog(@"%s%@", _cmd, color);
355     NSRange range = { 0, [attribString length] };
356     NSDictionary *attribs = [NSDictionary dictionaryWithObjectsAndKeys:
357             font, NSFontAttributeName,
358             color, NSForegroundColorAttributeName,
359             color, NSBackgroundColorAttributeName, nil];
360     [attribString setAttributes:attribs range:range];
361     [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
364 - (void)setDefaultColorsBackground:(NSColor *)bgColor
365                         foreground:(NSColor *)fgColor
367     if (defaultBackgroundColor != bgColor) {
368         [defaultBackgroundColor release];
369         defaultBackgroundColor = bgColor ? [bgColor retain] : nil;
370     }
372     // NOTE: The default foreground color isn't actually used for anything, but
373     // other class instances might want to be able to access it so it is stored
374     // here.
375     if (defaultForegroundColor != fgColor) {
376         [defaultForegroundColor release];
377         defaultForegroundColor = fgColor ? [fgColor retain] : nil;
378     }
381 - (void)setFont:(NSFont*)newFont
383     if (newFont && font != newFont) {
384         [font release];
386         // NOTE! When setting a new font we make sure that the advancement of
387         // each glyph is fixed. 
389         float em = [newFont widthOfString:@"m"];
390         float cellWidthMultiplier = [[NSUserDefaults standardUserDefaults]
391                 floatForKey:MMCellWidthMultiplierKey];
393         // NOTE! Even though NSFontFixedAdvanceAttribute is a float, it will
394         // only render at integer sizes.  Hence, we restrict the cell width to
395         // an integer here, otherwise the window width and the actual text
396         // width will not match.
397         cellSize.width = ceilf(em * cellWidthMultiplier);
399         float pointSize = [newFont pointSize];
400         NSDictionary *dict = [NSDictionary
401             dictionaryWithObject:[NSNumber numberWithFloat:cellSize.width]
402                           forKey:NSFontFixedAdvanceAttribute];
404         NSFontDescriptor *desc = [newFont fontDescriptor];
405         desc = [desc fontDescriptorByAddingAttributes:dict];
406         font = [NSFont fontWithDescriptor:desc size:pointSize];
407         [font retain];
409         NSLayoutManager *lm = [[self layoutManagers] objectAtIndex:0];
410         cellSize.height = lm ? [lm defaultLineHeightForFont:font]
411                              : [font defaultLineHeightForFont];
413         // NOTE: The font manager does not care about the 'font fixed advance'
414         // attribute, so after converting the font we have to add this
415         // attribute again.
416         boldFont = [[NSFontManager sharedFontManager]
417             convertFont:font toHaveTrait:NSBoldFontMask];
418         desc = [boldFont fontDescriptor];
419         desc = [desc fontDescriptorByAddingAttributes:dict];
420         boldFont = [NSFont fontWithDescriptor:desc size:pointSize];
421         [boldFont retain];
423         italicFont = [[NSFontManager sharedFontManager]
424             convertFont:font toHaveTrait:NSItalicFontMask];
425         desc = [italicFont fontDescriptor];
426         desc = [desc fontDescriptorByAddingAttributes:dict];
427         italicFont = [NSFont fontWithDescriptor:desc size:pointSize];
428         [italicFont retain];
430         boldItalicFont = [[NSFontManager sharedFontManager]
431             convertFont:italicFont toHaveTrait:NSBoldFontMask];
432         desc = [boldItalicFont fontDescriptor];
433         desc = [desc fontDescriptorByAddingAttributes:dict];
434         boldItalicFont = [NSFont fontWithDescriptor:desc size:pointSize];
435         [boldItalicFont retain];
436     }
439 - (NSFont*)font
441     return font;
444 - (NSColor *)defaultBackgroundColor
446     return defaultBackgroundColor;
449 - (NSColor *)defaultForegroundColor
451     return defaultForegroundColor;
454 - (NSSize)size
456     return NSMakeSize(maxColumns*cellSize.width, maxRows*cellSize.height);
459 - (NSSize)cellSize
461     return cellSize;
464 - (NSRect)rectForRowsInRange:(NSRange)range
466     NSRect rect = { 0, 0, 0, 0 };
467     unsigned start = range.location > maxRows ? maxRows : range.location;
468     unsigned length = range.length;
470     if (start+length > maxRows)
471         length = maxRows - start;
473     rect.origin.y = cellSize.height * start;
474     rect.size.height = cellSize.height * length;
476     return rect;
479 - (NSRect)rectForColumnsInRange:(NSRange)range
481     NSRect rect = { 0, 0, 0, 0 };
482     unsigned start = range.location > maxColumns ? maxColumns : range.location;
483     unsigned length = range.length;
485     if (start+length > maxColumns)
486         length = maxColumns - start;
488     rect.origin.x = cellSize.width * start;
489     rect.size.width = cellSize.width * length;
491     return rect;
494 - (unsigned)characterIndexForRow:(int)row column:(int)col
496     // Ensure the offset returned is valid.
497     // This code also works if maxRows and/or maxColumns is 0.
498     if (row >= maxRows) row = maxRows-1;
499     if (row < 0) row = 0;
500     if (col >= maxColumns) col = maxColumns-1;
501     if (col < 0) col = 0;
503     return (unsigned)(col + row*(maxColumns+1));
506 - (BOOL)resizeToFitSize:(NSSize)size
508     int rows = maxRows, cols = maxColumns;
510     [self fitToSize:size rows:&rows columns:&cols];
511     if (rows != maxRows || cols != maxColumns) {
512         [self setMaxRows:rows columns:cols];
513         return YES;
514     }
516     // Return NO only if dimensions did not change.
517     return NO;
520 - (NSSize)fitToSize:(NSSize)size
522     return [self fitToSize:size rows:NULL columns:NULL];
525 - (NSSize)fitToSize:(NSSize)size rows:(int *)rows columns:(int *)columns
527     NSSize curSize = [self size];
528     NSSize fitSize = curSize;
529     int fitRows = maxRows;
530     int fitCols = maxColumns;
532     if (size.height < curSize.height) {
533         // Remove lines until the height of the text storage fits inside
534         // 'size'.  However, always make sure there are at least 3 lines in the
535         // text storage.  (Why 3? It seem Vim never allows less than 3 lines.)
536         //
537         // TODO: No need to search since line height is fixed, just calculate
538         // the new height.
539         int rowCount = maxRows;
540         int rowsToRemove;
541         for (rowsToRemove = 0; rowsToRemove < maxRows-3; ++rowsToRemove) {
542             float height = cellSize.height*rowCount;
544             if (height <= size.height) {
545                 fitSize.height = height;
546                 break;
547             }
549             --rowCount;
550         }
552         fitRows -= rowsToRemove;
553     } else if (size.height > curSize.height) {
554         float fh = cellSize.height;
555         if (fh < 1.0f) fh = 1.0f;
557         fitRows = floor(size.height/fh);
558         fitSize.height = fh*fitRows;
559     }
561     if (size.width != curSize.width) {
562         float fw = cellSize.width;
563         if (fw < 1.0f) fw = 1.0f;
565         fitCols = floor(size.width/fw);
566         fitSize.width = fw*fitCols;
567     }
569     if (rows) *rows = fitRows;
570     if (columns) *columns = fitCols;
572     return fitSize;
575 @end // MMTextStorage
580 @implementation MMTextStorage (Private)
581 - (void)lazyResize
583     int i;
585     // Do nothing if the dimensions are already right.
586     if (actualRows == maxRows && actualColumns == maxColumns)
587         return;
589     NSRange oldRange = NSMakeRange(0, actualRows*(actualColumns+1));
591     actualRows = maxRows;
592     actualColumns = maxColumns;
594     NSDictionary *dict;
595     if (defaultBackgroundColor) {
596         dict = [NSDictionary dictionaryWithObjectsAndKeys:
597                 font, NSFontAttributeName,
598                 defaultBackgroundColor, NSBackgroundColorAttributeName, nil];
599     } else {
600         dict = [NSDictionary dictionaryWithObjectsAndKeys:
601                 font, NSFontAttributeName, nil];
602     }
603             
604     NSMutableString *rowString = [NSMutableString string];
605     for (i = 0; i < maxColumns; ++i) {
606         [rowString appendString:@" "];
607     }
608     [rowString appendString:@"\n"];
610     [emptyRowString release];
611     emptyRowString = [[NSAttributedString alloc] initWithString:rowString
612                                                      attributes:dict];
614     [attribString release];
615     attribString = [[NSMutableAttributedString alloc] init];
616     for (i=0; i<maxRows; ++i) {
617         [attribString appendAttributedString:emptyRowString];
618     }
620     NSRange fullRange = NSMakeRange(0, [attribString length]);
621     [self edited:(NSTextStorageEditedCharacters|NSTextStorageEditedAttributes)
622            range:oldRange changeInLength:fullRange.length-oldRange.length];
625 @end // MMTextStorage (Private)