From http://www.jwz.org/xscreensaver/xscreensaver-5.37.tar.gz
[xscreensaver] / jwxyz / jwxyz-cocoa.m
index 10da646f92a81e3b0b0856480b8a1a2b8c338242..4585e6584ce231202379e5ef32a529bf0ea6d9f9 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 1991-2016 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 1991-2017 Jamie Zawinski <jwz@jwz.org>
  *
  * 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 <stdarg.h>
 
 # import <OpenGLES/ES1/gl.h>
 # import <OpenGLES/ES1/glext.h>
 # 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 <CoreText/CTLine.h>
+#import <CoreText/CTRun.h>
+
 /* OS X/iOS-specific JWXYZ implementation. */
 
 void
@@ -59,6 +71,7 @@ jwxyz_abort (const char *fmt, ...)
                                   encoding:NSUTF8StringEncoding]
                 userInfo: nil]
     raise];
+# undef abort
   abort();  // not reached
 }
 
@@ -79,6 +92,452 @@ 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.
+# endif
+
+  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 &larr; 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, int utf8_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)
 {
@@ -377,8 +836,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);
@@ -423,7 +888,7 @@ jwxyz_assert_drawable (Window main_window, Drawable d)
   }
   @catch (NSException *exception) {
     perror([[exception reason] UTF8String]);
-    abort();
+    jwxyz_abort();
   }
 #endif // !USE_IPHONE && !__OPTIMIZE__
 }