X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=blobdiff_plain;f=jwxyz%2Fjwxyz-cocoa.m;h=079c4ff22a8a82181eb5d049dd58b94f6404720c;hb=39809ded547bdbb08207d3e514950425215b4410;hp=3b6bf4119f03ebfe297c7353d1e6e2bb360839a8;hpb=aa75c7476aeaa84cf3abc192b376a8b03c325213;p=xscreensaver diff --git a/jwxyz/jwxyz-cocoa.m b/jwxyz/jwxyz-cocoa.m index 3b6bf411..079c4ff2 100644 --- a/jwxyz/jwxyz-cocoa.m +++ b/jwxyz/jwxyz-cocoa.m @@ -1,4 +1,4 @@ -/* xscreensaver, Copyright (c) 1991-2016 Jamie Zawinski +/* xscreensaver, Copyright (c) 1991-2017 Jamie Zawinski * * Permission to use, copy, modify, distribute, and sell this software and its * documentation for any purpose is hereby granted without fee, provided that @@ -20,6 +20,7 @@ #import "jwxyzI.h" #import "jwxyz-cocoa.h" +#import "utf8wc.h" #include @@ -27,10 +28,30 @@ # import # import # define NSOpenGLContext EAGLContext +# define NSFont UIFont + +# define NSFontTraitMask UIFontDescriptorSymbolicTraits +// The values for the flags for NSFontTraitMask and +// UIFontDescriptorSymbolicTraits match up, not that it really matters here. +# define NSBoldFontMask UIFontDescriptorTraitBold +# define NSFixedPitchFontMask UIFontDescriptorTraitMonoSpace +# define NSItalicFontMask UIFontDescriptorTraitItalic #endif +#import +#import + +#define VTBL JWXYZ_VTBL(dpy) + /* OS X/iOS-specific JWXYZ implementation. */ +void +jwxyz_logv (Bool error, const char *fmt, va_list args) +{ + vfprintf (stderr, fmt, args); + fputc ('\n', stderr); +} + /* Instead of calling abort(), throw a real exception, so that XScreenSaverView can catch it and display a dialog. */ @@ -52,6 +73,7 @@ jwxyz_abort (const char *fmt, ...) encoding:NSUTF8StringEncoding] userInfo: nil] raise]; +# undef abort abort(); // not reached } @@ -72,6 +94,458 @@ jwxyz_drawable_depth (Drawable d) } +float +jwxyz_scale (Window main_window) +{ + float scale = 1; + +# ifdef USE_IPHONE + /* Since iOS screens are physically smaller than desktop screens, scale up + the fonts to make them more readable. + + Note that X11 apps on iOS also have the backbuffer sized in points + instead of pixels, resulting in an effective X11 screen size of 768x1024 + or so, even if the display has significantly higher resolution. That is + unrelated to this hack, which is really about DPI. + */ + scale = main_window->window.view.hackedContentScaleFactor; + if (scale < 1) // iPad Pro magnifies the backbuffer by 3x, which makes text + scale = 1; // excessively blurry in BSOD. + +# else // !USE_IPHONE + + /* Desktop retina displays also need fonts doubled. */ + scale = main_window->window.view.hackedContentScaleFactor; + +# endif // !USE_IPHONE + + return scale; +} + + +/* Font metric terminology, as used by X11: + + "lbearing" is the distance from the logical origin to the leftmost pixel. + If a character's ink extends to the left of the origin, it is negative. + + "rbearing" is the distance from the logical origin to the rightmost pixel. + + "descent" is the distance from the logical origin to the bottommost pixel. + For characters with descenders, it is positive. For superscripts, it + is negative. + + "ascent" is the distance from the logical origin to the topmost pixel. + It is the number of pixels above the baseline. + + "width" is the distance from the logical origin to the position where + the logical origin of the next character should be placed. + + If "rbearing" is greater than "width", then this character overlaps the + following character. If smaller, then there is trailing blank space. + */ +static void +utf8_metrics (Display *dpy, NSFont *nsfont, NSString *nsstr, XCharStruct *cs) +{ + // Returns the metrics of the multi-character, single-line UTF8 string. + + Drawable d = XRootWindow (dpy, 0); + + CGContextRef cgc = d->cgc; + NSDictionary *attr = + [NSDictionary dictionaryWithObjectsAndKeys: + nsfont, NSFontAttributeName, + nil]; + NSAttributedString *astr = [[NSAttributedString alloc] + initWithString:nsstr + attributes:attr]; + CTLineRef ctline = CTLineCreateWithAttributedString ( + (__bridge CFAttributedStringRef) astr); + CGContextSetTextPosition (cgc, 0, 0); + CGContextSetShouldAntialias (cgc, True); // #### Guess? + + memset (cs, 0, sizeof(*cs)); + + // "CTRun represents set of consecutive glyphs sharing the same + // attributes and direction". + // + // We also get multiple runs any time font subsitution happens: + // E.g., if the current font is Verdana-Bold, a ← character + // in the NSString will actually be rendered in LucidaGrande-Bold. + // + int count = 0; + for (id runid in (NSArray *)CTLineGetGlyphRuns(ctline)) { + CTRunRef run = (CTRunRef) runid; + CFRange r = { 0, }; + CGRect bbox = CTRunGetImageBounds (run, cgc, r); + CGFloat ascent, descent, leading; + CGFloat advancement = + CTRunGetTypographicBounds (run, r, &ascent, &descent, &leading); + +# ifndef USE_IPHONE + // Only necessary for when LCD smoothing is enabled, which iOS doesn't do. + bbox.origin.x -= 2.0/3.0; + bbox.size.width += 4.0/3.0; + bbox.size.height += 1.0/2.0; +# endif + + // Create the metrics for this run: + XCharStruct cc; + cc.ascent = ceil (bbox.origin.y + bbox.size.height); + cc.descent = ceil (-bbox.origin.y); + cc.lbearing = floor (bbox.origin.x); + cc.rbearing = ceil (bbox.origin.x + bbox.size.width); + cc.width = floor (advancement + 0.5); + + // Add those metrics into the cumulative metrics: + if (count == 0) + *cs = cc; + else + { + cs->ascent = MAX (cs->ascent, cc.ascent); + cs->descent = MAX (cs->descent, cc.descent); + cs->lbearing = MIN (cs->lbearing, cs->width + cc.lbearing); + cs->rbearing = MAX (cs->rbearing, cs->width + cc.rbearing); + cs->width = MAX (cs->width, cs->width + cc.width); + } + + // Why no y? What about vertical text? + // XCharStruct doesn't encapsulate that but XGlyphInfo does. + + count++; + } + + [astr release]; + CFRelease (ctline); +} + + +static NSArray * +font_family_members (NSString *family_name) +{ +# ifndef USE_IPHONE + return [[NSFontManager sharedFontManager] + availableMembersOfFontFamily:family_name]; +# else + return [UIFont fontNamesForFamilyName:family_name]; +# endif +} + + +const char * +jwxyz_default_font_family (int require) +{ + return require & JWXYZ_STYLE_MONOSPACE ? "Courier" : "Verdana"; +} + + +static NSFontTraitMask +nsfonttraitmask_for (int font_style) +{ + NSFontTraitMask result = 0; + if (font_style & JWXYZ_STYLE_BOLD) + result |= NSBoldFontMask; + if (font_style & JWXYZ_STYLE_ITALIC) + result |= NSItalicFontMask; + if (font_style & JWXYZ_STYLE_MONOSPACE) + result |= NSFixedPitchFontMask; + return result; +} + + +static NSFont * +try_font (NSFontTraitMask traits, NSFontTraitMask mask, + NSString *family_name, float size) +{ + NSArray *family_members = font_family_members (family_name); + if (!family_members.count) { + family_members = font_family_members ( + [NSString stringWithUTF8String:jwxyz_default_font_family ( + traits & NSFixedPitchFontMask ? JWXYZ_STYLE_MONOSPACE : 0)]); + } + +# ifndef USE_IPHONE + for (unsigned k = 0; k != family_members.count; ++k) { + + NSArray *member = [family_members objectAtIndex:k]; + NSFontTraitMask font_mask = + [(NSNumber *)[member objectAtIndex:3] unsignedIntValue]; + + if ((font_mask & mask) == traits) { + + NSString *name = [member objectAtIndex:0]; + NSFont *f = [NSFont fontWithName:name size:size]; + if (!f) + break; + + /* Don't use this font if it (probably) doesn't include ASCII characters. + */ + NSStringEncoding enc = [f mostCompatibleStringEncoding]; + if (! (enc == NSUTF8StringEncoding || + enc == NSISOLatin1StringEncoding || + enc == NSNonLossyASCIIStringEncoding || + enc == NSISOLatin2StringEncoding || + enc == NSUnicodeStringEncoding || + enc == NSWindowsCP1250StringEncoding || + enc == NSWindowsCP1252StringEncoding || + enc == NSMacOSRomanStringEncoding)) { + // NSLog(@"skipping \"%@\": encoding = %d", name, enc); + break; + } + // NSLog(@"using \"%@\": %d", name, enc); + + return f; + } + } +# else // USE_IPHONE + + // This trick needs iOS 3.1, see "Using SDK-Based Development". + Class has_font_descriptor = [UIFontDescriptor class]; + + for (NSString *fn in family_members) { +# define MATCH(X) \ + ([fn rangeOfString:X options:NSCaseInsensitiveSearch].location \ + != NSNotFound) + + NSFontTraitMask font_mask; + if (has_font_descriptor) { + // This only works on iOS 7 and later. + font_mask = [[UIFontDescriptor + fontDescriptorWithFontAttributes: + @{UIFontDescriptorNameAttribute:fn}] + symbolicTraits]; + } else { + font_mask = 0; + if (MATCH(@"Bold")) + font_mask |= NSBoldFontMask; + if (MATCH(@"Italic") || MATCH(@"Oblique")) + font_mask |= NSItalicFontMask; + if (MATCH(@"Courier")) + font_mask |= NSFixedPitchFontMask; + } + + if ((font_mask & mask) == traits) { + + /* Check if it can do ASCII. No good way to accomplish this! + These are fonts present in iPhone Simulator as of June 2012 + that don't include ASCII. + */ + if (MATCH(@"AppleGothic") || // Korean + MATCH(@"Dingbats") || // Dingbats + MATCH(@"Emoji") || // Emoticons + MATCH(@"Geeza") || // Arabic + MATCH(@"Hebrew") || // Hebrew + MATCH(@"HiraKaku") || // Japanese + MATCH(@"HiraMin") || // Japanese + MATCH(@"Kailasa") || // Tibetan + MATCH(@"Ornaments") || // Dingbats + MATCH(@"STHeiti") // Chinese + ) + break; + + return [UIFont fontWithName:fn size:size]; + } +# undef MATCH + } +# endif + + return NULL; +} + + +/* Returns a random font in the given size and face. + */ +static NSFont * +random_font (NSFontTraitMask traits, NSFontTraitMask mask, float size) +{ + +# ifndef USE_IPHONE + // Providing Unbold or Unitalic in the mask for availableFontNamesWithTraits + // returns an empty list, at least on a system with default fonts only. + NSArray *families = [[NSFontManager sharedFontManager] + availableFontFamilies]; + if (!families) return 0; +# else + NSArray *families = [UIFont familyNames]; + + // There are many dups in the families array -- uniquify it. + { + NSArray *sorted_families = + [families sortedArrayUsingSelector:@selector(compare:)]; + NSMutableArray *new_families = + [NSMutableArray arrayWithCapacity:sorted_families.count]; + + NSString *prev_family = @""; + for (NSString *family in sorted_families) { + if ([family compare:prev_family]) + [new_families addObject:family]; + prev_family = family; + } + + families = new_families; + } +# endif // USE_IPHONE + + long n = [families count]; + if (n <= 0) return 0; + + int j; + for (j = 0; j < n; j++) { + int i = random() % n; + NSString *family_name = [families objectAtIndex:i]; + + NSFont *result = try_font (traits, mask, family_name, size); + if (result) + return result; + } + + // None of the fonts support ASCII? + return 0; +} + +void * +jwxyz_load_native_font (Window main_window, int traits_jwxyz, int mask_jwxyz, + const char *font_name_ptr, size_t font_name_length, + int font_name_type, float size, + char **family_name_ret, + int *ascent_ret, int *descent_ret) +{ + NSFont *nsfont = NULL; + + NSFontTraitMask + traits = nsfonttraitmask_for (traits_jwxyz), + mask = nsfonttraitmask_for (mask_jwxyz); + + NSString *font_name = font_name_type != JWXYZ_FONT_RANDOM ? + [[NSString alloc] initWithBytes:font_name_ptr + length:font_name_length + encoding:NSUTF8StringEncoding] : + nil; + + size *= jwxyz_scale (main_window); + + if (font_name_type == JWXYZ_FONT_RANDOM) { + + nsfont = random_font (traits, mask, size); + [font_name release]; + + } else if (font_name_type == JWXYZ_FONT_FACE) { + + nsfont = [NSFont fontWithName:font_name size:size]; + + } else if (font_name_type == JWXYZ_FONT_FAMILY) { + + Assert (size > 0, "zero font size"); + + if (!nsfont) + nsfont = try_font (traits, mask, font_name, size); + + // if that didn't work, turn off attibutes until it does + // (e.g., there is no "Monaco-Bold".) + // + if (!nsfont && (mask & NSItalicFontMask)) { + traits &= ~NSItalicFontMask; + mask &= ~NSItalicFontMask; + nsfont = try_font (traits, mask, font_name, size); + } + if (!nsfont && (mask & NSBoldFontMask)) { + traits &= ~NSBoldFontMask; + mask &= ~NSBoldFontMask; + nsfont = try_font (traits, mask, font_name, size); + } + if (!nsfont && (mask & NSFixedPitchFontMask)) { + traits &= ~NSFixedPitchFontMask; + mask &= ~NSFixedPitchFontMask; + nsfont = try_font (traits, mask, font_name, size); + } + } + + if (nsfont) + { + if (family_name_ret) + *family_name_ret = strdup (nsfont.familyName.UTF8String); + + CFRetain (nsfont); // needed for garbage collection? + + *ascent_ret = ceil ([nsfont ascender]); + *descent_ret = -floor ([nsfont descender]); + + Assert([nsfont fontName], "broken NSFont in fid"); + } + + return nsfont; +} + + +void +jwxyz_release_native_font (Display *dpy, void *native_font) +{ + // #### DAMMIT! I can't tell what's going wrong here, but I keep getting + // crashes in [NSFont ascender] <- query_font, and it seems to go away + // if I never release the nsfont. So, fuck it, we'll just leak fonts. + // They're probably not very big... + // + // [fid->nsfont release]; + // CFRelease (fid->nsfont); +} + + +// Given a UTF8 string, return an NSString. Bogus UTF8 characters are ignored. +// We have to do this because stringWithCString returns NULL if there are +// any invalid characters at all. +// +static NSString * +sanitize_utf8 (const char *in, size_t in_len, Bool *latin1_pP) +{ + size_t out_len = in_len * 4; // length of string might increase + char *s2 = (char *) malloc (out_len); + char *out = s2; + const char *in_end = in + in_len; + const char *out_end = out + out_len; + Bool latin1_p = True; + + while (in < in_end) + { + unsigned long uc; + long L1 = utf8_decode ((const unsigned char *) in, in_end - in, &uc); + long L2 = utf8_encode (uc, out, out_end - out); + in += L1; + out += L2; + if (uc > 255) latin1_p = False; + } + *out = 0; + NSString *nsstr = + [NSString stringWithCString:s2 encoding:NSUTF8StringEncoding]; + free (s2); + if (latin1_pP) *latin1_pP = latin1_p; + return (nsstr ? nsstr : @""); +} + + +NSString * +nsstring_from(const char *str, size_t len, int utf8_p) +{ + Bool latin1_p; + NSString *nsstr = utf8_p ? + sanitize_utf8 (str, len, &latin1_p) : + [[[NSString alloc] initWithBytes:str + length:len + encoding:NSISOLatin1StringEncoding] + autorelease]; + return nsstr; +} + +void +jwxyz_render_text (Display *dpy, void *native_font, + const char *str, size_t len, Bool utf8_p, Bool antialias_p, + XCharStruct *cs_ret, char **pixmap_ret) +{ + utf8_metrics (dpy, (NSFont *)native_font, nsstring_from (str, len, utf8_p), + cs_ret); + + Assert (!pixmap_ret, "TODO"); +} + + void jwxyz_get_pos (Window w, XPoint *xvpos, XPoint *xp) { @@ -213,19 +687,19 @@ create_framebuffer (GLuint *gl_framebuffer, GLuint *gl_renderbuffer) static void push_bg_gc (Display *dpy, Drawable d, GC gc, Bool fill_p) { - XGCValues *gcv = jwxyz_gc_gcv (gc); + XGCValues *gcv = VTBL->gc_gcv (gc); push_color_gc (dpy, d, gc, gcv->background, gcv->antialias_p, fill_p); } void -jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc, - int src_x, int src_y, - unsigned int width, unsigned int height, - int dst_x, int dst_y) +jwxyz_quartz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc, + int src_x, int src_y, + unsigned int width, unsigned int height, + int dst_x, int dst_y) { XRectangle src_frame = src->frame, dst_frame = dst->frame; - XGCValues *gcv = jwxyz_gc_gcv (gc); + XGCValues *gcv = VTBL->gc_gcv (gc); BOOL mask_p = src->type == PIXMAP && src->pixmap.depth == 1; @@ -244,30 +718,12 @@ jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc, !dst_frame.y, "unexpected non-zero origin"); - ptrdiff_t src_pitch = CGBitmapContextGetBytesPerRow(src->cgc); - ptrdiff_t dst_pitch = CGBitmapContextGetBytesPerRow(dst->cgc); - char *src_data = seek_xy (CGBitmapContextGetData (src->cgc), src_pitch, - src_x, src_y); - char *dst_data = seek_xy (CGBitmapContextGetData (dst->cgc), dst_pitch, - dst_x, dst_y); - - size_t bytes = width * 4; - - if (src == dst && dst_y > src_y) { - // Copy upwards if the areas might overlap. - src_data += src_pitch * (height - 1); - dst_data += dst_pitch * (height - 1); - src_pitch = -src_pitch; - dst_pitch = -dst_pitch; - } + jwxyz_blit (CGBitmapContextGetData (src->cgc), + CGBitmapContextGetBytesPerRow (src->cgc), src_x, src_y, + CGBitmapContextGetData (dst->cgc), + CGBitmapContextGetBytesPerRow (dst->cgc), dst_x, dst_y, + width, height); - while (height) { - // memcpy is an alias for memmove on OS X. - memmove(dst_data, src_data, bytes); - src_data += src_pitch; - dst_data += dst_pitch; - --height; - } } else { CGRect src_rect = CGRectMake(src_x, src_y, width, height), @@ -321,7 +777,7 @@ jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc, // then fill in a solid rectangle of the fg color, using the image as an // alpha mask. (the image has only values of BlackPixel or WhitePixel.) - set_color (dpy, cgc, gcv->foreground, jwxyz_gc_depth (gc), + set_color (dpy, cgc, gcv->foreground, VTBL->gc_depth (gc), gcv->alpha_allowed_p, YES); CGContextClipToMask (cgc, dst_rect, cgi); CGContextFillRect (cgc, dst_rect); @@ -370,8 +826,14 @@ jwxyz_copy_area (Display *dpy, Drawable src, Drawable dst, GC gc, OS X. (Early 2009 Mac mini, OS X 10.10) */ # ifdef USE_IPHONE - jwxyz_gl_copy_area_copy_tex_image (dpy, src, dst, gc, src_x, src_y, - width, height, dst_x, dst_y); + + /* TODO: This might not still work. */ + jwxyz_bind_drawable (dpy, dpy->main_window, src); + jwxyz_gl_copy_area_read_tex_image (dpy, jwxyz_frame (src)->height, + src_x, src_y, width, height, dst_x, dst_y); + jwxyz_bind_drawable (dpy, dpy->main_window, dst); + jwxyz_gl_copy_area_write_tex_image (dpy, gc, src_x, src_y, + width, height, dst_x, dst_y); # else // !USE_IPHONE jwxyz_gl_copy_area_read_pixels (dpy, src, dst, gc, src_x, src_y, width, height, dst_x, dst_y); @@ -416,7 +878,7 @@ jwxyz_assert_drawable (Window main_window, Drawable d) } @catch (NSException *exception) { perror([[exception reason] UTF8String]); - abort(); + jwxyz_abort(); } #endif // !USE_IPHONE && !__OPTIMIZE__ }