git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

HexFiend/HFRepresenterTextViewCallout.m

      1 //
      2 //  HFRepresenterTextViewCallout.m
      3 //  HexFiend_2
      4 //
      5 //  Copyright 2011 ridiculous_fish. All rights reserved.
      6 //
      7 
      8 #import "HFRepresenterTextViewCallout.h"
      9 #import "HFRepresenterTextView.h"
     10 
     11 static const CGFloat HFTeardropRadius = 12;
     12 static const CGFloat HFTeadropTipScale = 2.5;
     13 
     14 static const CGFloat HFShadowXOffset = -6;
     15 static const CGFloat HFShadowYOffset = 0;
     16 static const CGFloat HFShadowOffscreenHack = 3100;
     17 
     18 static NSPoint rotatePoint(NSPoint center, NSPoint point, CGFloat percent) {
     19     CGFloat radians = percent * M_PI * 2;
     20     CGFloat x = point.x - center.x;
     21     CGFloat y = point.y - center.y;
     22     CGFloat newX = x * cos(radians) + y * sin(radians);
     23     CGFloat newY = x * -sin(radians) + y * cos(radians);
     24     return NSMakePoint(center.x + newX, center.y + newY);
     25 }
     26 
     27 static NSPoint scalePoint(NSPoint center, NSPoint point, CGFloat percent) {
     28     CGFloat x = point.x - center.x;
     29     CGFloat y = point.y - center.y;
     30     CGFloat newX = x * percent;
     31     CGFloat newY = y * percent;
     32     return NSMakePoint(center.x + newX, center.y + newY);
     33 }
     34 
     35 static NSBezierPath *copyTeardropPath(void) {
     36     static NSBezierPath *sPath = nil;
     37     if (! sPath) {
     38         
     39         CGFloat radius = HFTeardropRadius;
     40         CGFloat rotation = 0;
     41         CGFloat droppiness = .15;
     42         CGFloat tipScale = HFTeadropTipScale;
     43         CGFloat tipLengthFromCenter = radius * tipScale;
     44         NSPoint bulbCenter = NSMakePoint(-tipLengthFromCenter, 0);
     45         
     46         NSPoint triangleCenter = rotatePoint(bulbCenter, NSMakePoint(bulbCenter.x + radius, bulbCenter.y), rotation);
     47         NSPoint dropCorner1 = rotatePoint(bulbCenter, triangleCenter, droppiness / 2);
     48         NSPoint dropCorner2 = rotatePoint(bulbCenter, triangleCenter, -droppiness / 2);
     49         NSPoint dropTip = scalePoint(bulbCenter, triangleCenter, tipScale);
     50         
     51         NSBezierPath *path = [[NSBezierPath alloc] init];
     52         [path appendBezierPathWithArcWithCenter:bulbCenter radius:radius startAngle:-rotation * 360 + droppiness * 180. endAngle:-rotation * 360 - droppiness * 180. clockwise:NO];
     53         
     54         [path moveToPoint:dropCorner1];
     55         [path lineToPoint:dropTip];
     56         [path lineToPoint:dropCorner2];
     57         [path closePath];
     58         
     59         sPath = path;
     60     }
     61     return [sPath retain];
     62 }
     63 
     64 
     65 @implementation HFRepresenterTextViewCallout
     66 
     67 /* A helpful struct for representing a wedge (portion of a circle). Wedges are counterclockwise. */
     68 typedef struct {
     69     double offset; // 0 <= offset < 1
     70     double length; // 0 <= length <= 1
     71 } Wedge_t;
     72 
     73 
     74 static inline double normalizeAngle(double x) {
     75     /* Convert an angle to the range [0, 1). We typically only generate angles that are off by a full rotation, so a loop isn't too bad. */
     76     while (x >= 1.) x -= 1.;
     77     while (x < 0.) x += 1.;
     78     return x;
     79 }
     80 
     81 static inline double distanceCCW(double a, double b) { return normalizeAngle(b-a); }
     82 
     83 static inline double wedgeMax(Wedge_t wedge) {
     84     return normalizeAngle(wedge.offset + wedge.length);
     85 }
     86 
     87 /* Computes the smallest wedge containing the two given wedges. Compute the wedge from the min of one to the furthest part of the other, and pick the smaller. */
     88 static Wedge_t wedgeUnion(Wedge_t wedge1, Wedge_t wedge2) {
     89     // empty wedges don't participate
     90     if (wedge1.length <= 0) return wedge2;
     91     if (wedge2.length <= 0) return wedge1;
     92     
     93     Wedge_t union1 = wedge1;
     94     union1.length = fmin(1., fmax(union1.length, distanceCCW(union1.offset, wedge2.offset) + wedge2.length));
     95     
     96     Wedge_t union2 = wedge2;
     97     union2.length = fmin(1., fmax(union2.length, distanceCCW(union2.offset, wedge1.offset) + wedge1.length));
     98     
     99     Wedge_t result = (union1.length <= union2.length ? union1 : union2);
    100     HFASSERT(result.length <= 1);
    101     return result;
    102 }
    103 
    104 - (instancetype)init {
    105     self = [super init];
    106     if (self) {
    107         // Initialization code here.
    108     }
    109     
    110     return self;
    111 }
    112 
    113 - (void)dealloc {
    114     [_representedObject release];
    115     [_color release];
    116     [_label release];
    117     [super dealloc];
    118 }
    119 
    120 - (NSComparisonResult)compare:(HFRepresenterTextViewCallout *)callout {
    121     return [_representedObject compare:callout.representedObject];
    122 }
    123 
    124 static Wedge_t computeForbiddenAngle(double distanceFromEdge, double angleToEdge) {
    125     Wedge_t newForbiddenAngle;
    126     
    127     /* This is how far it is to the center of our teardrop */
    128     const double teardropLength = HFTeardropRadius * HFTeadropTipScale;
    129     
    130     if (distanceFromEdge <= 0) {
    131         /* We're above or below. */
    132         if (-distanceFromEdge >= (teardropLength + HFTeardropRadius)) {
    133             /* We're so far above or below we won't be visible at all. No hope. */
    134             newForbiddenAngle = (Wedge_t){.offset = 0, .length = 1};
    135         } else { 
    136             /* We're either above or below the bounds, but there's a hope we can be visible */
    137             
    138             double invertedAngleToEdge = normalizeAngle(angleToEdge + .5);
    139             double requiredAngle;
    140             if (-distanceFromEdge >= teardropLength) {
    141                 // We're too far north or south that all we can do is point in the right direction
    142                 requiredAngle = 0;
    143             } else {
    144                 // By confining ourselves to required angles, we can make ourselves visible
    145                 requiredAngle = acos(-distanceFromEdge / teardropLength) / (2 * M_PI);
    146             }
    147             // Require at least a small spread
    148             requiredAngle = fmax(requiredAngle, .04);
    149             
    150             double requiredMin = invertedAngleToEdge - requiredAngle;
    151             double requiredMax = invertedAngleToEdge + requiredAngle;
    152             
    153             newForbiddenAngle = (Wedge_t){.offset = requiredMax, .length = distanceCCW(requiredMax, requiredMin) };
    154         }
    155     } else if (distanceFromEdge < teardropLength) {
    156         // We're onscreen, but some angle will be forbidden
    157         double forbiddenAngle = acos(distanceFromEdge / teardropLength) / (2 * M_PI);
    158         
    159         // This is a wedge out of the top (or bottom)
    160         newForbiddenAngle = (Wedge_t){.offset = angleToEdge - forbiddenAngle, .length = 2 * forbiddenAngle};
    161     } else {
    162         /* Nothing prohibited at all */
    163         newForbiddenAngle = (Wedge_t){0, 0};
    164     }
    165     return newForbiddenAngle;
    166 }
    167 
    168 
    169 static double distanceMod1(double a, double b) {
    170     /* Assuming 0 <= a, b < 1, returns the distance between a and b, mod 1 */
    171     if (a > b) {
    172         return fmin(a-b, b-a+1);
    173     } else {
    174         return fmin(b-a, a-b+1);
    175     }
    176 }
    177 
    178 + (void)layoutCallouts:(NSArray *)callouts inView:(HFRepresenterTextView *)textView {
    179     
    180     // Keep track of how many drops are at a given location
    181     NSCountedSet *dropsPerByteLoc = [[NSCountedSet alloc] init];
    182     
    183     const CGFloat lineHeight = [textView lineHeight];
    184     const NSRect bounds = [textView bounds];
    185     
    186     NSMutableArray *remainingCallouts = [[callouts mutableCopy] autorelease];
    187     [remainingCallouts sortUsingSelector:@selector(compare:)];
    188     
    189     while ([remainingCallouts count] > 0) {
    190         /* Get the next callout to lay out */
    191         const NSInteger byteLoc = [remainingCallouts[0] byteOffset];
    192         
    193         /* Get all the callouts that share that byteLoc */
    194         NSMutableArray *sharedCallouts = [NSMutableArray array];
    195         FOREACH(HFRepresenterTextViewCallout *, testCallout, remainingCallouts) {
    196             if ([testCallout byteOffset] == byteLoc) {
    197                 [sharedCallouts addObject:testCallout];
    198             }
    199         }
    200         
    201         /* We expect to get at least one */
    202         const NSUInteger calloutCount = [sharedCallouts count];
    203         HFASSERT(calloutCount > 0);
    204         
    205         /* Get the character origin */
    206         const NSPoint characterOrigin = [textView originForCharacterAtByteIndex:byteLoc];
    207 
    208         Wedge_t forbiddenAngle = {0, 0};
    209         
    210         // Compute how far we are from the top (or bottom)
    211         BOOL isNearerTop = (characterOrigin.y < NSMidY(bounds));
    212         double verticalDistance = (isNearerTop ? characterOrigin.y - NSMinY(bounds) : NSMaxY(bounds) - characterOrigin.y);
    213         forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(verticalDistance, (isNearerTop ? .25 : .75)));
    214         
    215         // Compute how far we are from the left (or right)
    216         BOOL isNearerLeft = (characterOrigin.x < NSMidX(bounds));
    217         double horizontalDistance = (isNearerLeft ? characterOrigin.x - NSMinX(bounds) : NSMaxX(bounds) - characterOrigin.x);
    218         forbiddenAngle = wedgeUnion(forbiddenAngle, computeForbiddenAngle(horizontalDistance, (isNearerLeft ? .5 : 0.)));
    219         
    220         
    221         /* How much will each callout rotate? No more than 1/8th. */
    222         HFASSERT(forbiddenAngle.length <= 1);
    223         double changeInRotationPerCallout = fmin(.125, (1. - forbiddenAngle.length) / calloutCount);
    224         double totalConsumedAmount = changeInRotationPerCallout * calloutCount;
    225         
    226         /* We would like to center around .375. */
    227         const double goalCenter = .375;
    228         
    229         /* We're going to pretend to work on a line segment that extends from the max prohibited angle all the way back to min */
    230         double segmentLength = 1. - forbiddenAngle.length;
    231         double goalSegmentCenter = normalizeAngle(goalCenter - wedgeMax(forbiddenAngle)); //may exceed segmentLength!
    232                 
    233         /* Now center us on the goal, or as close as we can get. */
    234         double consumedSegmentCenter;
    235         
    236         /* We only need to worry about wrapping around if we have some prohibited angle */
    237         if (forbiddenAngle.length <= 0) { //never expect < 0, but be paranoid
    238             consumedSegmentCenter = goalSegmentCenter;
    239         } else {
    240             
    241             /* The consumed segment center is confined to the segment range [amount/2, length - amount/2] */
    242             double consumedSegmentCenterMin = totalConsumedAmount/2;
    243             double consumedSegmentCenterMax = segmentLength - totalConsumedAmount/2;
    244             if (goalSegmentCenter >= consumedSegmentCenterMin && goalSegmentCenter < consumedSegmentCenterMax) {
    245                 /* We can hit our goal */
    246                 consumedSegmentCenter = goalSegmentCenter;
    247             } else {
    248                 /* Pick either the min or max location, depending on which one gets us closer to the goal segment center mod 1. */
    249                 if (distanceMod1(goalSegmentCenter, consumedSegmentCenterMin) <= distanceMod1(goalSegmentCenter, consumedSegmentCenterMax)) {
    250                     consumedSegmentCenter = consumedSegmentCenterMin;
    251                 } else {
    252                     consumedSegmentCenter = consumedSegmentCenterMax;
    253                 }
    254                 
    255             }
    256         }
    257         
    258         /* Now convert this back to an angle */
    259         double consumedAngleCenter = normalizeAngle(wedgeMax(forbiddenAngle) + consumedSegmentCenter);
    260         
    261         // move us slightly towards the character
    262         NSPoint teardropTipOrigin = NSMakePoint(characterOrigin.x + 1, characterOrigin.y + floor(lineHeight / 8.));
    263         
    264         // make the pin
    265         NSPoint pinStart, pinEnd;
    266         pinStart = NSMakePoint(characterOrigin.x + .25, characterOrigin.y);
    267         pinEnd = NSMakePoint(pinStart.x, pinStart.y + lineHeight);
    268         
    269         // store it all, invalidating as necessary
    270         NSInteger i = 0;
    271         FOREACH(HFRepresenterTextViewCallout *, callout, sharedCallouts) {
    272             
    273             /* Compute the rotation */
    274             double seq = (i+1)/2; //0, 1, -1, 2, -2...
    275             if ((i & 1) == 0) seq = -seq;
    276             //if we've got an even number of callouts, we want -.5, .5, -1.5, 1.5...
    277             if (! (calloutCount & 1)) seq -= .5;
    278             // compute the angle of rotation
    279             double angle = consumedAngleCenter + seq * changeInRotationPerCallout;
    280             // our notion of rotation has 0 meaning pointing right and going counterclockwise, but callouts with 0 pointing left and going clockwise, so convert
    281             angle = normalizeAngle(.5 - angle);
    282 
    283             
    284             NSRect beforeRect = [callout rect];
    285             
    286             callout->rotation = angle;
    287             callout->tipOrigin = teardropTipOrigin;
    288             callout->pinStart = pinStart;
    289             callout->pinEnd = pinEnd;
    290             
    291             // Only the first gets a pin
    292             pinStart = pinEnd = NSZeroPoint;
    293             
    294             NSRect afterRect = [callout rect];
    295             
    296             if (! NSEqualRects(beforeRect, afterRect)) {
    297                 [textView setNeedsDisplayInRect:beforeRect];
    298                 [textView setNeedsDisplayInRect:afterRect];
    299             }
    300             
    301             i++;
    302         }
    303 
    304         
    305         /* We're done laying out these callouts */
    306         [remainingCallouts removeObjectsInArray:sharedCallouts];
    307     }
    308     
    309     [dropsPerByteLoc release];
    310 }
    311 
    312 - (CGAffineTransform)teardropTransform {
    313     CGAffineTransform trans = CGAffineTransformMakeTranslation(tipOrigin.x, tipOrigin.y);
    314     trans = CGAffineTransformRotate(trans, rotation * M_PI * 2);
    315     return trans;
    316 }
    317 
    318 - (NSRect)teardropBaseRect {
    319     NSSize teardropSize = NSMakeSize(HFTeardropRadius * (1 + HFTeadropTipScale), HFTeardropRadius*2);
    320     NSRect result = NSMakeRect(-teardropSize.width, -teardropSize.height/2, teardropSize.width, teardropSize.height);
    321     return result;
    322 }
    323 
    324 - (CGAffineTransform)shadowTransform {
    325     CGFloat shadowXOffset = HFShadowXOffset;
    326     CGFloat shadowYOffset = HFShadowYOffset;
    327     CGFloat offscreenOffset = HFShadowOffscreenHack;
    328     
    329     // Figure out how much movement the shadow offset produces
    330     CGFloat shadowTranslationDistance = hypot(shadowXOffset, shadowYOffset);
    331     
    332     CGAffineTransform transform = CGAffineTransformIdentity;
    333     transform = CGAffineTransformTranslate(transform, tipOrigin.x + offscreenOffset - shadowXOffset, tipOrigin.y - shadowYOffset);
    334     transform = CGAffineTransformRotate(transform, rotation * M_PI * 2 - atan2(shadowTranslationDistance, 2*HFTeardropRadius /* bulbHeight */));
    335     return transform;
    336 }
    337 
    338 - (void)drawShadowWithClip:(NSRect)clip {
    339     USE(clip);
    340     CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
    341     
    342     // Set the shadow. Note that these shadows are pretty unphysical for high rotations.
    343     NSShadow *shadow = [[NSShadow alloc] init];
    344     [shadow setShadowBlurRadius:5.];
    345     [shadow setShadowOffset:NSMakeSize(HFShadowXOffset - HFShadowOffscreenHack, HFShadowYOffset)];
    346     [shadow setShadowColor:[NSColor colorWithDeviceWhite:0. alpha:.5]];
    347     [shadow set];
    348     [shadow release];
    349     
    350     // Draw the shadow first and separately
    351     CGAffineTransform transform = [self shadowTransform];
    352     CGContextConcatCTM(ctx, transform);
    353     
    354     NSBezierPath *teardrop = copyTeardropPath();
    355     [teardrop fill];
    356     [teardrop release];
    357     
    358     // Clear the shadow
    359     CGContextSetShadowWithColor(ctx, CGSizeZero, 0, NULL);
    360     
    361     // Undo the transform
    362     CGContextConcatCTM(ctx, CGAffineTransformInvert(transform));
    363 }
    364 
    365 - (void)drawWithClip:(NSRect)clip {
    366     USE(clip);
    367     CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort];
    368     // Here's the font we'll use
    369     CTFontRef ctfont = CTFontCreateWithName(CFSTR("Helvetica-Bold"), 1., NULL);
    370     if (ctfont) {
    371         // Set the font
    372         [(NSFont *)ctfont set];
    373             
    374         // Get characters
    375         NSUInteger labelLength = MIN([_label length], kHFRepresenterTextViewCalloutMaxGlyphCount);
    376         UniChar calloutUniLabel[kHFRepresenterTextViewCalloutMaxGlyphCount];
    377         [_label getCharacters:calloutUniLabel range:NSMakeRange(0, labelLength)];
    378         
    379         // Get our glyphs and advances
    380         CGGlyph glyphs[kHFRepresenterTextViewCalloutMaxGlyphCount];
    381         CGSize advances[kHFRepresenterTextViewCalloutMaxGlyphCount];
    382         CTFontGetGlyphsForCharacters(ctfont, calloutUniLabel, glyphs, labelLength);
    383         CTFontGetAdvancesForGlyphs(ctfont, kCTFontHorizontalOrientation, glyphs, advances, labelLength);
    384 
    385         // Count our glyphs. Note: this won't work with any label containing spaces, etc.
    386         NSUInteger glyphCount;
    387         for (glyphCount = 0; glyphCount < labelLength; glyphCount++) {
    388             if (glyphs[glyphCount] == 0) break;
    389         }
    390                 
    391         // Set our color.
    392         [_color set];
    393         
    394         // Draw the pin first
    395         if (! NSEqualPoints(pinStart, pinEnd)) {
    396             [NSBezierPath setDefaultLineWidth:1.25];
    397             [NSBezierPath strokeLineFromPoint:pinStart toPoint:pinEnd];
    398         }
    399         
    400         CGContextSaveGState(ctx);
    401         CGContextBeginTransparencyLayerWithRect(ctx, NSRectToCGRect([self rect]), NULL);
    402 
    403         // Rotate and translate in preparation for drawing the teardrop
    404         CGContextConcatCTM(ctx, [self teardropTransform]);
    405         
    406         // Draw the teardrop
    407         NSBezierPath *teardrop = copyTeardropPath();
    408         [teardrop fill];
    409         [teardrop release];
    410         
    411         // Draw the text with white and alpha.  Use blend mode copy so that we clip out the shadow, and when the transparency layer is ended we'll composite over the text.
    412         CGFloat textScale = (glyphCount == 1 ? 24 : 20);
    413         
    414         // we are flipped by default, so invert the rotation's sign to get the text direction. Use a little slop so we don't get jitter.
    415         const CGFloat textDirection = (rotation <= .27 || rotation >= .73) ? -1 : 1;
    416         
    417         CGPoint positions[kHFRepresenterTextViewCalloutMaxGlyphCount];
    418         CGFloat totalAdvance = 0;
    419         for (NSUInteger i=0; i < glyphCount; i++) {
    420             // make sure to provide negative advances if necessary
    421             positions[i].x = copysign(totalAdvance, -textDirection);
    422             positions[i].y = 0;
    423             CGFloat advance = advances[i].width;
    424             // Workaround 5834794
    425             advance *= textScale;
    426             // Tighten up the advances a little
    427             advance *= .85;
    428             totalAdvance += advance;
    429         }
    430         
    431         
    432         // Compute the vertical offset
    433         CGFloat textYOffset = (glyphCount == 1 ? 4 : 5);                
    434         // LOL
    435         if ([_label isEqualToString:@"6"]) textYOffset -= 1;
    436         
    437         
    438         // Apply this text matrix
    439         NSRect bulbRect = [self teardropBaseRect];
    440         CGAffineTransform textMatrix = CGAffineTransformMakeScale(-copysign(textScale, textDirection), copysign(textScale, textDirection)); //roughly the font size we want
    441         textMatrix.tx = NSMinX(bulbRect) + HFTeardropRadius + copysign(totalAdvance/2, textDirection);
    442         
    443 
    444         if (textDirection < 0) {
    445             textMatrix.ty = NSMaxY(bulbRect) - textYOffset;
    446         } else {
    447             textMatrix.ty = NSMinY(bulbRect) + textYOffset;
    448         }
    449         
    450         // Draw
    451         CGContextSetTextMatrix(ctx, textMatrix);
    452         CGContextSetTextDrawingMode(ctx, kCGTextClip);
    453         CGContextShowGlyphsAtPositions(ctx, glyphs, positions, glyphCount);
    454         
    455         CGContextSetBlendMode(ctx, kCGBlendModeCopy);
    456         CGContextSetGrayFillColor(ctx, 1., .66); //faint white fill
    457         CGContextFillRect(ctx, NSRectToCGRect(NSInsetRect(bulbRect, -20, -20)));
    458         
    459         // Done drawing, so composite
    460         CGContextEndTransparencyLayer(ctx);
    461         CGContextRestoreGState(ctx); // this also restores the clip, which is important
    462         
    463         // Done with the font
    464         CFRelease(ctfont);
    465     }
    466 }
    467 
    468 - (NSRect)rect {
    469     // get the transformed teardrop rect
    470     NSRect result = NSRectFromCGRect(CGRectApplyAffineTransform(NSRectToCGRect([self teardropBaseRect]), [self teardropTransform]));
    471     
    472     // outset a bit for the shadow
    473     result = NSInsetRect(result, -8, -8);
    474     return result;
    475 }
    476 
    477 @end

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.