Reimplement using cache system for layout information.

git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/gui/trunk@18478 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
alexm 2004-01-25 15:19:53 +00:00
parent 58ec69b0ae
commit 3083c7c0f6
2 changed files with 471 additions and 127 deletions

View file

@ -1,3 +1,18 @@
2004-01-25 16:16 Alexander Malmberg <alexander@malmberg.org>
* Source/NSStringDrawing.m: Big redesign.
(cache_match, cache_lookup_string, cache_lookup_attributed_string,
use_screen_fonts): New helper functions for caching layout
information.
(init_string_drawing, [NSAttributedString -drawAtPoint:],
[NSAttributedString -drawInRect:], [NSAttributedString -size],
[NSString -drawAtPoint:withAttributes:],
[NSString -drawInRect:withAttributes:],
[NSString -sizeWithAttributes:]): Reimplement using the cache system
and the new helper functions.
2004-01-25 Fred Kiefer <FredKiefer@gmx.de> 2004-01-25 Fred Kiefer <FredKiefer@gmx.de>
* Source/NSWindow.m (-orderWindow:relativeTo:): In OrderOut case * Source/NSWindow.m (-orderWindow:relativeTo:): In OrderOut case

View file

@ -3,7 +3,7 @@
<abstract>Categories which add drawing capabilities to NSAttributedString <abstract>Categories which add drawing capabilities to NSAttributedString
and NSString.</abstract> and NSString.</abstract>
Copyright (C) 1999, 2003 Free Software Foundation, Inc. Copyright (C) 1999, 2003, 2004 Free Software Foundation, Inc.
Author: Richard Frith-Macdonald <richard@brainstorm.co.uk> Author: Richard Frith-Macdonald <richard@brainstorm.co.uk>
Date: Mar 1999 - rewrite from scratch Date: Mar 1999 - rewrite from scratch
@ -31,6 +31,8 @@
#include <math.h> #include <math.h>
#include <Foundation/NSException.h>
#include "AppKit/NSAffineTransform.h" #include "AppKit/NSAffineTransform.h"
#include "AppKit/NSLayoutManager.h" #include "AppKit/NSLayoutManager.h"
#include "AppKit/NSTextContainer.h" #include "AppKit/NSTextContainer.h"
@ -42,40 +44,328 @@
TODO: TODO:
these methods are _not_ reentrant. should they be? these methods are _not_ reentrant. should they be?
We could save time by trying to avoid changing the text storage and
container size if the new rect and text are the same as the previous.
This might be a common case if lots of calls to -size... and -draw...
are paired.
*/ */
#define LARGE_SIZE 8e6 #define LARGE_SIZE 4e6
/* /*
8e6 is not as arbitrary as it seems. 8e6 is chosen because it's close to 4e6 is chosen because it's close to (half of, for extra margin) 1<<23-1, the
1<<23-1, the largest number that can be stored in a 32-bit float with an largest number that can be stored in a 32-bit float with an ulp of 0.5, which
ulp of 0.5, which means things should round to the correct whole point. means things should round to the correct whole point.
*/ */
static NSTextStorage *textStorage;
static NSLayoutManager *layoutManager; #define NUM_CACHE_ENTRIES 16
static NSTextContainer *textContainer; #define HIT_BOOST 2
#define MISS_COST 1
/*
A size of 16 and these constants give a hit rate of 80%-90% for normal app
use (based on real world statistics gathered with the help of some users
from #GNUstep).
*/
typedef struct
{
int used;
unsigned int string_hash;
int hasSize, useScreenFonts;
NSTextStorage *textStorage;
NSLayoutManager *layoutManager;
NSTextContainer *textContainer;
NSSize givenSize;
NSRect usedRect;
} cache_t;
static BOOL did_init;
static cache_t cache[NUM_CACHE_ENTRIES];
static NSTextStorage *scratchTextStorage;
static NSLayoutManager *scratchLayoutManager;
static NSTextContainer *scratchTextContainer;
static int total, hits, misses, hash_hits;
/* For collecting statistics. */
//#define STATS
#ifdef STATS
static void NSStringDrawing_dump_stats(void)
{
#define P(x) printf("%15i %s\n", x, #x);
P(total)
P(hits)
P(misses)
P(hash_hits)
#undef P
printf("%15.8f hit ratio\n", hits / (double)total);
}
#endif
static void init_string_drawing(void) static void init_string_drawing(void)
{ {
if (textStorage) int i;
return; NSTextStorage *textStorage;
NSLayoutManager *layoutManager;
NSTextContainer *textContainer;
textStorage = [[NSTextStorage alloc] init]; if (did_init)
layoutManager = [[NSLayoutManager alloc] init]; return;
[textStorage addLayoutManager: layoutManager]; did_init = YES;
[layoutManager release];
textContainer = [[NSTextContainer alloc] #ifdef STATS
initWithContainerSize: NSMakeSize(10,10)]; atexit(NSStringDrawing_dump_stats);
[layoutManager addTextContainer: textContainer]; #endif
[textContainer release];
for (i = 0; i < NUM_CACHE_ENTRIES + 1; i++)
{
textStorage = [[NSTextStorage alloc] init];
layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager: layoutManager];
[layoutManager release];
textContainer = [[NSTextContainer alloc]
initWithContainerSize: NSMakeSize(10, 10)];
[layoutManager addTextContainer: textContainer];
[textContainer release];
if (i < NUM_CACHE_ENTRIES)
{
cache[i].textStorage = textStorage;
cache[i].layoutManager = layoutManager;
cache[i].textContainer = textContainer;
}
else
{
scratchTextStorage = textStorage;
scratchLayoutManager = layoutManager;
scratchTextContainer = textContainer;
}
}
} }
static int cache_match(int hasSize, NSSize size, int useScreenFonts, int *matched)
{
int i, j;
cache_t *c;
int least_used;
int replace;
int orig_used;
unsigned int string_hash = [[scratchTextStorage string] hash];
total++;
*matched = 1;
replace = least_used = -1;
/*
A deterministic pattern for replacing cache entries can hit ugly worst
cases on certain matching use patterns (where the cache is full of old
unused entries, but the new entries keep replacing each other).
By starting at a random index, we avoid this kind of problem.
*/
j = random() % NUM_CACHE_ENTRIES;
for (i = 0; i < NUM_CACHE_ENTRIES; i++, j++)
{
if (j == NUM_CACHE_ENTRIES)
j = 0;
c = cache + j;
if (least_used == -1 || c->used < least_used)
{
least_used = c->used;
replace = j;
}
if (!c->used)
continue;
orig_used = c->used;
if (c->used > MISS_COST)
c->used -= MISS_COST;
else
c->used = 1;
if (c->string_hash != string_hash
|| c->useScreenFonts != useScreenFonts)
continue;
hash_hits++;
if (![scratchTextStorage isEqualToAttributedString: c->textStorage])
continue;
/* String and attributes match. */
if (!c->hasSize && !hasSize)
{
c->used = orig_used + HIT_BOOST;
hits++;
return j;
}
if (c->hasSize && hasSize && c->givenSize.width == size.width
&& c->givenSize.height == size.height)
{
c->used = orig_used + HIT_BOOST;
hits++;
return j;
}
if (!c->hasSize && hasSize && size.width >= NSMaxX(c->usedRect)
&& size.height >= NSMaxY(c->usedRect))
{
c->used = orig_used + HIT_BOOST;
hits++;
return j;
}
}
NSCAssert(replace != -1, @"Couldn't find a cache entry to replace.");
misses++;
*matched = 0;
c = cache + replace;
c->used = 1;
c->string_hash = string_hash;
c->hasSize = hasSize;
c->useScreenFonts = useScreenFonts;
c->givenSize = size;
{
id temp;
#define SWAP(a, b) temp = a; a = b; b = temp;
SWAP(scratchTextStorage, c->textStorage)
SWAP(scratchLayoutManager, c->layoutManager)
SWAP(scratchTextContainer, c->textContainer)
#undef SWAP
}
return replace;
}
static int cache_lookup_string(NSString *string, NSDictionary *attributes,
int hasSize, NSSize size, int useScreenFonts)
{
cache_t *c;
int ci, hit;
NSTextStorage *textStorage;
NSLayoutManager *layoutManager;
NSTextContainer *textContainer;
if (!did_init)
init_string_drawing();
/*
This is a hack, but it's an efficient way of getting the layout manager
to just ditch all old information, and here, we don't want it to try to
be clever and cache or soft-invalidate anything since the new string has
nothing to do with the old one.
TODO: the layout manager should realize this by itself
*/
[scratchLayoutManager setTextStorage: scratchTextStorage];
[scratchTextStorage beginEditing];
[scratchTextStorage replaceCharactersInRange: NSMakeRange(0, [scratchTextStorage length])
withString: @""];
if ([string length])
{
[scratchTextStorage replaceCharactersInRange: NSMakeRange(0, 0)
withString: string];
[scratchTextStorage setAttributes: attributes
range: NSMakeRange(0, [string length])];
}
[scratchTextStorage endEditing];
ci = cache_match(hasSize, size, useScreenFonts, &hit);
if (hit)
return ci;
c = &cache[ci];
textStorage = c->textStorage;
layoutManager = c->layoutManager;
textContainer = c->textContainer;
if (hasSize)
[textContainer setContainerSize: NSMakeSize(size.width, LARGE_SIZE)];
else
[textContainer setContainerSize: NSMakeSize(LARGE_SIZE, LARGE_SIZE)];
[layoutManager setUsesScreenFonts: useScreenFonts];
c->usedRect = [layoutManager usedRectForTextContainer: textContainer];
return ci;
}
static int cache_lookup_attributed_string(NSAttributedString *string,
int hasSize, NSSize size, int useScreenFonts)
{
cache_t *c;
int ci, hit;
NSTextStorage *textStorage;
NSLayoutManager *layoutManager;
NSTextContainer *textContainer;
if (!did_init)
init_string_drawing();
[scratchTextStorage replaceCharactersInRange: NSMakeRange(0, [scratchTextStorage length])
withString: @""];
[scratchTextStorage replaceCharactersInRange: NSMakeRange(0, 0)
withAttributedString: string];
ci = cache_match(hasSize, size, useScreenFonts, &hit);
if (hit)
return ci;
c = &cache[ci];
textStorage = c->textStorage;
layoutManager = c->layoutManager;
textContainer = c->textContainer;
if (hasSize)
[textContainer setContainerSize: NSMakeSize(size.width, LARGE_SIZE)];
else
[textContainer setContainerSize: NSMakeSize(LARGE_SIZE, LARGE_SIZE)];
[layoutManager setUsesScreenFonts: useScreenFonts];
c->usedRect = [layoutManager usedRectForTextContainer: textContainer];
return ci;
}
static int use_screen_fonts(void)
{
NSGraphicsContext *ctxt = GSCurrentContext();
NSAffineTransform *ctm = GSCurrentCTM(ctxt);
if (ctm->matrix.m11 != 1.0 || ctm->matrix.m12 != 0.0 ||
ctm->matrix.m21 != 0.0 || fabs(ctm->matrix.m22) != 1.0)
{
return 0;
}
else
{
return 1;
}
}
/* /*
This is an ugly hack to get text to display correctly in non-flipped views. This is an ugly hack to get text to display correctly in non-flipped views.
@ -87,40 +377,24 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
+(void) _setFontFlipHack: (BOOL)flip; +(void) _setFontFlipHack: (BOOL)flip;
@end @end
@implementation NSAttributedString (NSStringDrawing) @implementation NSAttributedString (NSStringDrawing)
- (void) drawAtPoint: (NSPoint)point - (void) drawAtPoint: (NSPoint)point
{ {
int ci;
cache_t *c;
NSRange r; NSRange r;
NSGraphicsContext *ctxt = GSCurrentContext(); NSGraphicsContext *ctxt = GSCurrentContext();
NSAffineTransform *ctm = GSCurrentCTM(ctxt);
init_string_drawing(); ci = cache_lookup_attributed_string(self, 0, NSZeroSize, use_screen_fonts());
c = &cache[ci];
[textStorage replaceCharactersInRange: NSMakeRange(0, [textStorage length]) r = NSMakeRange(0, [c->layoutManager numberOfGlyphs]);
withString: @""];
[textContainer setContainerSize: NSMakeSize(LARGE_SIZE, LARGE_SIZE)];
if (ctm->matrix.m11 != 1.0 || ctm->matrix.m12 != 0.0 ||
ctm->matrix.m21 != 0.0 || fabs(ctm->matrix.m22) != 1.0)
{
[layoutManager setUsesScreenFonts: NO];
}
else
{
[layoutManager setUsesScreenFonts: YES];
}
[textStorage replaceCharactersInRange: NSMakeRange(0, 0)
withAttributedString: self];
r = NSMakeRange(0, [layoutManager numberOfGlyphs]);
if (![[NSView focusView] isFlipped]) if (![[NSView focusView] isFlipped])
{ {
NSRect usedRect;
DPSscale(ctxt, 1, -1); DPSscale(ctxt, 1, -1);
point.y = -point.y; point.y = -point.y;
@ -128,17 +402,16 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
Adjust point.y so the lower left corner of the used rect is at the Adjust point.y so the lower left corner of the used rect is at the
point that was passed to us. point that was passed to us.
*/ */
usedRect = [layoutManager usedRectForTextContainer: textContainer]; point.y -= NSMaxY(c->usedRect);
point.y -= NSMaxY(usedRect);
[NSFont _setFontFlipHack: YES]; [NSFont _setFontFlipHack: YES];
} }
[layoutManager drawBackgroundForGlyphRange: r [c->layoutManager drawBackgroundForGlyphRange: r
atPoint: point]; atPoint: point];
[layoutManager drawGlyphsForGlyphRange: r [c->layoutManager drawGlyphsForGlyphRange: r
atPoint: point]; atPoint: point];
if (![[NSView focusView] isFlipped]) if (![[NSView focusView] isFlipped])
{ {
@ -149,38 +422,18 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
- (void) drawInRect: (NSRect)rect - (void) drawInRect: (NSRect)rect
{ {
int ci;
cache_t *c;
NSRange r; NSRange r;
BOOL need_clip; BOOL need_clip;
NSRect used;
NSGraphicsContext *ctxt = GSCurrentContext(); NSGraphicsContext *ctxt = GSCurrentContext();
NSAffineTransform *ctm = GSCurrentCTM(ctxt);
init_string_drawing(); if (rect.size.width <= 0 || rect.size.height <= 0)
return;
[textStorage replaceCharactersInRange: NSMakeRange(0, [textStorage length]) ci = cache_lookup_attributed_string(self, 1, rect.size, use_screen_fonts());
withString: @""]; c = &cache[ci];
/*
TODO: Use rect.size.heigth instead of LARGE_SIZE? Should make things faster,
since we'll only typeset what fits, but lines that used to fit partially
won't fit at all.
*/
[textContainer setContainerSize: NSMakeSize(rect.size.width, LARGE_SIZE)];
if (ctm->matrix.m11 != 1.0 || ctm->matrix.m12 != 0.0 ||
ctm->matrix.m21 != 0.0 || fabs(ctm->matrix.m22) != 1.0)
{
[layoutManager setUsesScreenFonts: NO];
}
else
{
[layoutManager setUsesScreenFonts: YES];
}
[textStorage replaceCharactersInRange: NSMakeRange(0, 0)
withAttributedString: self];
used = [layoutManager usedRectForTextContainer: textContainer];
/* /*
If the used rect fits completely in the rect we draw in, we save time If the used rect fits completely in the rect we draw in, we save time
@ -189,8 +442,9 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
This isn't completely safe; the used rect isn't guaranteed to contain This isn't completely safe; the used rect isn't guaranteed to contain
all parts of all glyphs. all parts of all glyphs.
*/ */
if (used.origin.x >= 0 && used.origin.y <= 0 && if (c->usedRect.origin.x >= 0 && c->usedRect.origin.y <= 0
NSMaxX(used) <= rect.size.width && NSMaxY(used) <= rect.size.height) && NSMaxX(c->usedRect) <= rect.size.width
&& NSMaxY(c->usedRect) <= rect.size.height)
{ {
need_clip = NO; need_clip = NO;
} }
@ -202,9 +456,10 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
rect.size.width, rect.size.height); rect.size.width, rect.size.height);
} }
r = [layoutManager r = [c->layoutManager
glyphRangeForBoundingRect: NSMakeRect(0, 0, rect.size.width, rect.size.height) glyphRangeForBoundingRect: NSMakeRect(0, 0, rect.size.width,
inTextContainer: textContainer]; rect.size.height)
inTextContainer: c->textContainer];
if (![[NSView focusView] isFlipped]) if (![[NSView focusView] isFlipped])
{ {
@ -213,11 +468,11 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
[NSFont _setFontFlipHack: YES]; [NSFont _setFontFlipHack: YES];
} }
[layoutManager drawBackgroundForGlyphRange: r [c->layoutManager drawBackgroundForGlyphRange: r
atPoint: rect.origin]; atPoint: rect.origin];
[layoutManager drawGlyphsForGlyphRange: r [c->layoutManager drawGlyphsForGlyphRange: r
atPoint: rect.origin]; atPoint: rect.origin];
[NSFont _setFontFlipHack: NO]; [NSFont _setFontFlipHack: NO];
if (![[NSView focusView] isFlipped]) if (![[NSView focusView] isFlipped])
@ -234,24 +489,14 @@ glyphs to be drawn upside-down, so we need to tell NSFont to flip the fonts.
- (NSSize) size - (NSSize) size
{ {
NSRange r; int ci;
NSRect rect; ci = cache_lookup_attributed_string(self, 0, NSZeroSize, 1);
/*
init_string_drawing(); An argument could be made for using NSMaxX/NSMaxY here, but that fails
horribly on right-aligned strings. For now, we handle that case in a
[textStorage replaceCharactersInRange: NSMakeRange(0, [textStorage length]) useful way and ignore indents.
withString: @""]; */
return cache[ci].usedRect.size;
[textContainer setContainerSize: NSMakeSize(LARGE_SIZE, LARGE_SIZE)];
[layoutManager setUsesScreenFonts: YES];
[textStorage replaceCharactersInRange: NSMakeRange(0, 0)
withAttributedString: self];
r = NSMakeRange(0, [layoutManager numberOfGlyphs]);
rect = [layoutManager usedRectForTextContainer: textContainer];
return rect.size;
} }
@end @end
@ -264,37 +509,121 @@ NSAttributedString to do the job.
- (void) drawAtPoint: (NSPoint)point withAttributes: (NSDictionary *)attrs - (void) drawAtPoint: (NSPoint)point withAttributes: (NSDictionary *)attrs
{ {
NSAttributedString *a; int ci;
cache_t *c;
a = [[NSAttributedString allocWithZone: NSDefaultMallocZone()] NSRange r;
initWithString: self NSGraphicsContext *ctxt = GSCurrentContext();
attributes: attrs];
[a drawAtPoint: point]; ci = cache_lookup_string(self, attrs, 0, NSZeroSize, use_screen_fonts());
RELEASE(a); c = &cache[ci];
r = NSMakeRange(0, [c->layoutManager numberOfGlyphs]);
if (![[NSView focusView] isFlipped])
{
DPSscale(ctxt, 1, -1);
point.y = -point.y;
/*
Adjust point.y so the lower left corner of the used rect is at the
point that was passed to us.
*/
point.y -= NSMaxY(c->usedRect);
[NSFont _setFontFlipHack: YES];
}
[c->layoutManager drawBackgroundForGlyphRange: r
atPoint: point];
[c->layoutManager drawGlyphsForGlyphRange: r
atPoint: point];
if (![[NSView focusView] isFlipped])
{
DPSscale(ctxt, 1, -1);
[NSFont _setFontFlipHack: NO];
}
} }
- (void) drawInRect: (NSRect)rect withAttributes: (NSDictionary *)attrs - (void) drawInRect: (NSRect)rect withAttributes: (NSDictionary *)attrs
{ {
NSAttributedString *a; int ci;
cache_t *c;
a = [[NSAttributedString allocWithZone: NSDefaultMallocZone()] NSRange r;
initWithString: self BOOL need_clip;
attributes: attrs]; NSGraphicsContext *ctxt = GSCurrentContext();
[a drawInRect: rect];
RELEASE(a); if (rect.size.width <= 0 || rect.size.height <= 0)
return;
ci = cache_lookup_string(self, attrs, 1, rect.size, use_screen_fonts());
c = &cache[ci];
/*
If the used rect fits completely in the rect we draw in, we save time
by avoiding the DPSrectclip (and the state save and restore).
This isn't completely safe; the used rect isn't guaranteed to contain
all parts of all glyphs.
*/
if (c->usedRect.origin.x >= 0 && c->usedRect.origin.y <= 0
&& NSMaxX(c->usedRect) <= rect.size.width
&& NSMaxY(c->usedRect) <= rect.size.height)
{
need_clip = NO;
}
else
{
need_clip = YES;
DPSgsave(ctxt);
DPSrectclip(ctxt, rect.origin.x, rect.origin.y,
rect.size.width, rect.size.height);
}
r = [c->layoutManager
glyphRangeForBoundingRect: NSMakeRect(0, 0, rect.size.width,
rect.size.height)
inTextContainer: c->textContainer];
if (![[NSView focusView] isFlipped])
{
DPSscale(ctxt, 1, -1);
rect.origin.y = -NSMaxY(rect);
[NSFont _setFontFlipHack: YES];
}
[c->layoutManager drawBackgroundForGlyphRange: r
atPoint: rect.origin];
[c->layoutManager drawGlyphsForGlyphRange: r
atPoint: rect.origin];
[NSFont _setFontFlipHack: NO];
if (![[NSView focusView] isFlipped])
{
DPSscale(ctxt, 1, -1);
}
if (need_clip)
{
/* Restore the original clipping path. */
DPSgrestore(ctxt);
}
} }
- (NSSize) sizeWithAttributes: (NSDictionary *)attrs - (NSSize) sizeWithAttributes: (NSDictionary *)attrs
{ {
NSAttributedString *a; int ci;
NSSize s; ci = cache_lookup_string(self, attrs, 0, NSZeroSize, 1);
/*
a = [[NSAttributedString allocWithZone: NSDefaultMallocZone()] An argument could be made for using NSMaxX/NSMaxY here, but that fails
initWithString: self horribly on right-aligned strings (which may be the right thing). For now,
attributes: attrs]; we handle that case in a useful way and ignore indents.
s = [a size]; */
RELEASE(a); return cache[ci].usedRect.size;
return s;
} }
@end @end