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.