From http://www.jwz.org/xscreensaver/xscreensaver-5.38.tar.gz
[xscreensaver] / OSX / XScreenSaverView.m
index bc7f21a3e79221f2f682b23082e84994172c0c5d..1ec05a7df80254fd1fa92b42ab892dcb5a2075a4 100644 (file)
@@ -1,4 +1,4 @@
-/* xscreensaver, Copyright (c) 2006-2016 Jamie Zawinski <jwz@jwz.org>
+/* xscreensaver, Copyright (c) 2006-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
  *
  * Permission to use, copy, modify, distribute, and sell this software and its
  * documentation for any purpose is hereby granted without fee, provided that
@@ -22,7 +22,7 @@
 #import "XScreenSaverConfigSheet.h"
 #import "Updater.h"
 #import "screenhackI.h"
 #import "XScreenSaverConfigSheet.h"
 #import "Updater.h"
 #import "screenhackI.h"
-#import "xlockmoreI.h"
+#import "pow2.h"
 #import "jwxyzI.h"
 #import "jwxyz-cocoa.h"
 #import "jwxyz-timers.h"
 #import "jwxyzI.h"
 #import "jwxyz-cocoa.h"
 #import "jwxyz-timers.h"
 #ifndef  MAC_OS_X_VERSION_10_6
 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
 #endif
 #ifndef  MAC_OS_X_VERSION_10_6
 # define MAC_OS_X_VERSION_10_6 1060  /* undefined in 10.4 SDK, grr */
 #endif
-#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6  /* 10.6 SDK */
+#ifndef  MAC_OS_X_VERSION_10_12
+# define MAC_OS_X_VERSION_10_12 101200
+#endif
+#if (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6 && \
+     MAC_OS_X_VERSION_MAX_ALLOWED <  MAC_OS_X_VERSION_10_12)
+  /* 10.6 SDK or later, and earlier than 10.12 SDK */
 # import <objc/objc-auto.h>
 # define DO_GC_HACKERY
 #endif
 # import <objc/objc-auto.h>
 # define DO_GC_HACKERY
 #endif
@@ -113,6 +118,7 @@ extern NSDictionary *make_function_table_dict(void);  // ios-function-table.m
 
 
 @interface XScreenSaverView (Private)
 
 
 @interface XScreenSaverView (Private)
+- (void) stopAndClose;
 - (void) stopAndClose:(Bool)relaunch;
 @end
 
 - (void) stopAndClose:(Bool)relaunch;
 @end
 
@@ -433,6 +439,11 @@ add_default_options (const XrmOptionDescRec *opts,
   [self setBackgroundColor:[NSColor blackColor]];
 # endif // USE_IPHONE
 
   [self setBackgroundColor:[NSColor blackColor]];
 # endif // USE_IPHONE
 
+# ifdef JWXYZ_QUARTZ
+  // Colorspaces and CGContexts only happen with non-GL hacks.
+  colorspace = CGColorSpaceCreateDeviceRGB ();
+# endif
+
   return self;
 }
 
   return self;
 }
 
@@ -593,8 +604,6 @@ add_default_options (const XrmOptionDescRec *opts,
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled =
     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled =
     ([UIDevice currentDevice].batteryState != UIDeviceBatteryStateUnplugged);
-  [[UIApplication sharedApplication]
-    setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
 # endif
 
   xwindow = (Window) calloc (1, sizeof(*xwindow));
 # endif
 
   xwindow = (Window) calloc (1, sizeof(*xwindow));
@@ -635,6 +644,9 @@ add_default_options (const XrmOptionDescRec *opts,
     // from initWithFrame.
     [ogl_ctx setView:self];
 
     // from initWithFrame.
     [ogl_ctx setView:self];
 
+    // Get device pixels instead of points.
+    self.wantsBestResolutionOpenGLSurface = YES;
+
     // This may not be necessary if there's FBO support.
 #  ifdef JWXYZ_GL
     xwindow->window.pixfmt = pixfmt;
     // This may not be necessary if there's FBO support.
 #  ifdef JWXYZ_GL
     xwindow->window.pixfmt = pixfmt;
@@ -666,6 +678,11 @@ add_default_options (const XrmOptionDescRec *opts,
 
     new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
 
 
     new_backbuffer_size = NSSizeToCGSize ([self bounds].size);
 
+    // Scale factor for desktop retina displays
+    double s = [self hackedContentScaleFactor];
+    new_backbuffer_size.width *= s;
+    new_backbuffer_size.height *= s;
+
 # else  // USE_IPHONE
     if (!ogl_ctx) {
       CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
 # else  // USE_IPHONE
     if (!ogl_ctx) {
       CAEAGLLayer *eagl_layer = (CAEAGLLayer *) self.layer;
@@ -718,6 +735,50 @@ add_default_options (const XrmOptionDescRec *opts,
 
   [self setViewport];
   [self createBackbuffer:new_backbuffer_size];
 
   [self setViewport];
   [self createBackbuffer:new_backbuffer_size];
+
+# ifdef USE_TOUCHBAR
+  static BOOL created_touchbar = NO;
+
+  if (!touchbar_view &&
+      //#### !self.isPreview &&
+      self.window.screen == [[NSScreen screens] objectAtIndex: 0] &&
+      !created_touchbar) {
+
+    // Figure out which NSScreen has the touchbar on it;
+    // find its bounds; create a saver there.
+
+    created_touchbar = YES;
+    NSScreen *tbs = [[NSScreen screens] lastObject]; // #### write me
+    NSRect rect = [tbs visibleFrame];
+
+    // #### debugging
+    rect.origin.x += 40;
+    rect.origin.x += 40;
+    rect.size.width /= 4;
+    rect.size.height /= 4;
+    NSLog(@"## TB %.0f, %.0f  %.0f x %.0f",
+          rect.origin.x, rect.origin.y, rect.size.width, rect.size.height);
+
+    touchbar_view = [[[self class] alloc]
+                      initWithFrame:rect
+                      saverName:[NSString stringWithCString:xsft->progclass
+                                          encoding:NSISOLatin1StringEncoding]
+                      isPreview:self.isPreview];
+    [touchbar_view setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
+
+    touchbar_window = [[NSWindow alloc]
+                        initWithContentRect:rect
+                        styleMask: (NSTitledWindowMask|NSResizableWindowMask)
+                        backing:NSBackingStoreBuffered
+                        defer:YES
+                        screen:tbs];
+    [touchbar_window setTitle: @"XScreenSaver Touchbar"];
+    [[touchbar_window contentView] addSubview: touchbar_view];
+    [touchbar_window makeKeyAndOrderFront:touchbar_window];
+  }
+
+  if (touchbar_view) [touchbar_view startAnimation];
+# endif // USE_TOUCHBAR
 }
 
 - (void)stopAnimation
 }
 
 - (void)stopAnimation
@@ -729,16 +790,17 @@ add_default_options (const XrmOptionDescRec *opts,
     [self lockFocus];       // in case something tries to draw from here
     [self prepareContext];
 
     [self lockFocus];       // in case something tries to draw from here
     [self prepareContext];
 
-    /* I considered just not even calling the free callback at all...
-       But webcollage-cocoa needs it, to kill the inferior webcollage
+    /* All of the xlockmore hacks need to have their release functions
+       called, or launching the same saver twice does not work.  Also
+       webcollage-cocoa needs it in order to kill the inferior webcollage
        processes (since the screen saver framework never generates a
        processes (since the screen saver framework never generates a
-       SIGPIPE for them...)  Instead, I turned off the free call in
-       xlockmore.c, which is where all of the bogus calls are anyway.
+       SIGPIPE for them).
      */
      */
-    xsft->free_cb (xdpy, xwindow, xdata);
+     if (xdata)
+       xsft->free_cb (xdpy, xwindow, xdata);
     [self unlockFocus];
 
     [self unlockFocus];
 
-    jwxyz_free_display (xdpy);
+    jwxyz_quartz_free_display (xdpy);
     xdpy = NULL;
 # if defined JWXYZ_GL && !defined USE_IPHONE
     CFRelease (xwindow->ogl_ctx);
     xdpy = NULL;
 # if defined JWXYZ_GL && !defined USE_IPHONE
     CFRelease (xwindow->ogl_ctx);
@@ -768,8 +830,6 @@ add_default_options (const XrmOptionDescRec *opts,
   //
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled = NO;
   //
 # ifdef USE_IPHONE
   [UIApplication sharedApplication].idleTimerDisabled = NO;
-  [[UIApplication sharedApplication]
-    setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
 # endif
 
   // Without this, the GL frame stays on screen when switching tabs
 # endif
 
   // Without this, the GL frame stays on screen when switching tabs
@@ -791,6 +851,19 @@ add_default_options (const XrmOptionDescRec *opts,
   backbuffer_data = NULL;
   backbuffer_len = 0;
 # endif
   backbuffer_data = NULL;
   backbuffer_len = 0;
 # endif
+
+# ifdef USE_TOUCHBAR
+  if (touchbar_view) {
+    [touchbar_view stopAnimation];
+    [touchbar_window close];
+
+    [touchbar_view release];
+    [touchbar_window release];
+
+    touchbar_view = nil;
+    touchbar_window = nil;
+  }
+# endif
 }
 
 
 }
 
 
@@ -826,18 +899,12 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
 }
 
 
 }
 
 
-#ifdef USE_IPHONE
+/* Some of the older X11 savers look bad if a "pixel" is not a thing you can
+   see.  They expect big, chunky, luxurious 1990s pixels, and if they use
+   "device" pixels on a Retina screen, everything just disappears.
 
 
-/* On iPhones with Retina displays, we can draw the savers in "real"
-   pixels, and that works great.  The 320x480 "point" screen is really
-   a 640x960 *pixel* screen.  However, Retina iPads have 768x1024
-   point screens which are 1536x2048 pixels, and apparently that's
-   enough pixels that copying those bits to the screen is slow.  Like,
-   drops us from 15fps to 7fps.  So, on Retina iPads, we don't draw in
-   real pixels.  This will probably make the savers look better
-   anyway, since that's a higher resolution than most desktop monitors
-   have even today.  (This is only true for X11 programs, not GL 
-   programs.  Those are fine at full rez.)
+   Retina iPads have 768x1024 point screens which are 1536x2048 pixels,
+   2017 iMac screens are 5120x2880 in device pixels.
 
    This method is overridden in XScreenSaverGLView, since this kludge
    isn't necessary for GL programs, being resolution independent by
 
    This method is overridden in XScreenSaverGLView, since this kludge
    isn't necessary for GL programs, being resolution independent by
@@ -845,32 +912,27 @@ screenhack_do_fps (Display *dpy, Window w, fps_state *fpst, void *closure)
  */
 - (CGFloat) hackedContentScaleFactor
 {
  */
 - (CGFloat) hackedContentScaleFactor
 {
-  NSSize bsize = [self bounds].size;
-
-  CGFloat
-    max_bsize = bsize.width > bsize.height ? bsize.width : bsize.height;
-
-  // Ratio of screen size in pixels to view size in points.
+# ifdef USE_IPHONE
   CGFloat s = self.contentScaleFactor;
   CGFloat s = self.contentScaleFactor;
+# else
+  CGFloat s = self.window.backingScaleFactor;
+# endif
 
 
-  // Two constraints:
-
-  // 1. Don't exceed -- let's say 1280 pixels in either direction.
-  //    (Otherwise the frame rate gets bad.)
-  //    Actually let's make that 1440 since iPhone 6 is natively 1334.
-  CGFloat mag0 = ceil(max_bsize * s / 1440);
-
-  // 2. Don't let the pixel size get too small.
-  //    (Otherwise pixels in IFS and similar are too fine.)
-  //    So don't let the result be > 2 pixels per point.
-  CGFloat mag1 = ceil(s / 2);
+  if (_lowrez_p) {
+    NSSize b = [self bounds].size;
+    CGFloat wh = b.width > b.height ? b.width : b.height;
+    const int max = 800; // maybe 1024?
+    wh *= s;
+    if (wh > max)
+      s *= max / wh;
+  }
 
 
-  // As of iPhone 6, mag0 is always >= mag1. This may not be true in the future.
-  // (desired scale factor) = s / (desired magnification factor)
-  return s / (mag0 > mag1 ? mag0 : mag1);
+  return s;
 }
 
 
 }
 
 
+#ifdef USE_IPHONE
+
 double
 current_device_rotation (void)
 {
 double
 current_device_rotation (void)
 {
@@ -895,7 +957,8 @@ current_device_rotation (void)
           the opposite direction."
         */
     /* statusBarOrientation deprecated in iOS 9 */
           the opposite direction."
         */
     /* statusBarOrientation deprecated in iOS 9 */
-    o = [UIApplication sharedApplication].statusBarOrientation;
+    o = (UIDeviceOrientation)  // from UIInterfaceOrientation
+      [UIApplication sharedApplication].statusBarOrientation;
   }
 
   switch (o) {
   }
 
   switch (o) {
@@ -907,29 +970,37 @@ current_device_rotation (void)
 }
 
 
 }
 
 
-- (void)alertView:(UIAlertView *)av clickedButtonAtIndex:(NSInteger)i
-{
-  if (i == 0) exit (-1);       // Cancel
-  [self stopAndClose:NO];      // Keep going
-}
-
 - (void) handleException: (NSException *)e
 {
   NSLog (@"Caught exception: %@", e);
 - (void) handleException: (NSException *)e
 {
   NSLog (@"Caught exception: %@", e);
-  [[[UIAlertView alloc] initWithTitle:
-                          [NSString stringWithFormat: @"%s crashed!",
-                                    xsft->progclass]
-                        message:
-                          [NSString stringWithFormat:
-                                      @"The error message was:"
-                                    "\n\n%@\n\n"
-                                    "If it keeps crashing, try "
-                                    "resetting its options.",
-                                    e]
-                        delegate: self
-                        cancelButtonTitle: @"Exit"
-                        otherButtonTitles: @"Keep going", nil]
-    show];
+  UIAlertController *c = [UIAlertController
+                           alertControllerWithTitle:
+                             [NSString stringWithFormat: @"%s crashed!",
+                                       xsft->progclass]
+                           message: [NSString stringWithFormat:
+                                                @"The error message was:"
+                                              "\n\n%@\n\n"
+                                              "If it keeps crashing, try "
+                                              "resetting its options.",
+                                              e]
+                           preferredStyle:UIAlertControllerStyleAlert];
+
+  [c addAction: [UIAlertAction actionWithTitle: @"Exit"
+                               style: UIAlertActionStyleDefault
+                               handler: ^(UIAlertAction *a) {
+    exit (-1);
+  }]];
+  [c addAction: [UIAlertAction actionWithTitle: @"Keep going"
+                               style: UIAlertActionStyleDefault
+                               handler: ^(UIAlertAction *a) {
+    [self stopAndClose:NO];
+  }]];
+
+  UIViewController *vc =
+    [UIApplication sharedApplication].keyWindow.rootViewController;
+  while (vc.presentedViewController)
+    vc = vc.presentedViewController;
+  [vc presentViewController:c animated:YES completion:nil];
   [self stopAnimation];
 }
 
   [self stopAnimation];
 }
 
@@ -997,7 +1068,7 @@ gl_check_ver (const struct gl_version *caps,
   gl_texture_target = GL_TEXTURE_2D;
 # endif
 
   gl_texture_target = GL_TEXTURE_2D;
 # endif
 
-  glBindTexture (gl_texture_target, &backbuffer_texture);
+  glBindTexture (gl_texture_target, backbuffer_texture);
   glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
   // GL_LINEAR might make sense on Retina iPads.
   glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
   glTexParameteri (gl_texture_target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
   // GL_LINEAR might make sense on Retina iPads.
   glTexParameteri (gl_texture_target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
@@ -1054,31 +1125,6 @@ gl_check_ver (const struct gl_version *caps,
 }
 
 
 }
 
 
-static GLsizei
-to_pow2 (size_t x)
-{
-  if (x <= 1)
-    return 1;
-
-  size_t mask = (size_t)-1;
-  unsigned bits = sizeof(x) * CHAR_BIT;
-  unsigned log2 = bits;
-
-  --x;
-  while (bits) {
-    if (!(x & mask)) {
-      log2 -= bits;
-      x <<= bits;
-    }
-
-    bits >>= 1;
-    mask <<= bits;
-  }
-
-  return 1 << log2;
-}
-
-
 #ifdef USE_IPHONE
 - (BOOL) suppressRotationAnimation
 {
 #ifdef USE_IPHONE
 - (BOOL) suppressRotationAnimation
 {
@@ -1102,11 +1148,10 @@ to_pow2 (size_t x)
 
 #  ifdef USE_IPHONE
   GLfloat s = self.contentScaleFactor;
 
 #  ifdef USE_IPHONE
   GLfloat s = self.contentScaleFactor;
-  GLfloat hs = self.hackedContentScaleFactor;
 #  else // !USE_IPHONE
 #  else // !USE_IPHONE
-  const GLfloat s = 1;
-  const GLfloat hs = s;
+  const GLfloat s = self.window.backingScaleFactor;
 #  endif
 #  endif
+  GLfloat hs = self.hackedContentScaleFactor;
 
   // On OS X this almost isn't necessary, except for the ugly aliasing
   // artifacts.
 
   // On OS X this almost isn't necessary, except for the ugly aliasing
   // artifacts.
@@ -1140,25 +1185,6 @@ to_pow2 (size_t x)
  */
 - (void) createBackbuffer:(CGSize)new_size
 {
  */
 - (void) createBackbuffer:(CGSize)new_size
 {
-  // Colorspaces and CGContexts only happen with non-GL hacks.
-  if (colorspace)
-    CGColorSpaceRelease (colorspace);
-
-  NSWindow *window = [self window];
-
-  if (window && xdpy) {
-    [self lockFocus];
-
-# ifdef BACKBUFFER_OPENGL
-    // Was apparently faster until 10.9.
-    colorspace = CGColorSpaceCreateDeviceRGB ();
-# endif // BACKBUFFER_OPENGL
-
-    [self unlockFocus];
-  } else {
-    colorspace = CGColorSpaceCreateDeviceRGB();
-  }
-
   CGSize osize = CGSizeZero;
   if (backbuffer) {
     osize.width = CGBitmapContextGetWidth(backbuffer);
   CGSize osize = CGSizeZero;
   if (backbuffer) {
     osize.width = CGBitmapContextGetWidth(backbuffer);
@@ -1172,7 +1198,7 @@ to_pow2 (size_t x)
 
   CGContextRef ob = backbuffer;
   void *odata = backbuffer_data;
 
   CGContextRef ob = backbuffer;
   void *odata = backbuffer_data;
-  size_t olen = backbuffer_len;
+  GLsizei olen = backbuffer_len;
 
 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   NSLog(@"backbuffer %.0fx%.0f",
 
 # if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
   NSLog(@"backbuffer %.0fx%.0f",
@@ -1220,11 +1246,11 @@ to_pow2 (size_t x)
   if (!gl_limited_npot_p)
 # endif
   {
   if (!gl_limited_npot_p)
 # endif
   {
-    gl_texture_w = to_pow2 (gl_texture_w);
-    gl_texture_h = to_pow2 (gl_texture_h);
+    gl_texture_w = (GLsizei) to_pow2 (gl_texture_w);
+    gl_texture_h = (GLsizei) to_pow2 (gl_texture_h);
   }
 
   }
 
-  size_t bytes_per_row = gl_texture_w * 4;
+  GLsizei bytes_per_row = gl_texture_w * 4;
 
 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
   // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
 
 # if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
   // APPLE_client_storage requires texture width to be aligned to 32 bytes, or
@@ -1475,17 +1501,19 @@ to_pow2 (size_t x)
   // landscape shape, swap width and height to keep the backbuffer
   // in portrait.
   //
   // landscape shape, swap width and height to keep the backbuffer
   // in portrait.
   //
-  if ([self ignoreRotation] && new_size.width > new_size.height) {
+  double rot = current_device_rotation();
+  if ([self ignoreRotation] && (rot == 90 || rot == -90)) {
     CGFloat swap    = new_size.width;
     new_size.width  = new_size.height;
     new_size.height = swap;
   }
     CGFloat swap    = new_size.width;
     new_size.width  = new_size.height;
     new_size.height = swap;
   }
+#  endif // USE_IPHONE
 
   double s = self.hackedContentScaleFactor;
   new_size.width *= s;
   new_size.height *= s;
 
   double s = self.hackedContentScaleFactor;
   new_size.width *= s;
   new_size.height *= s;
-#  endif // USE_IPHONE
 
 
+  [self prepareContext];
   [self setViewport];
 
   // On first resize, xwindow->frame is 0x0.
   [self setViewport];
 
   // On first resize, xwindow->frame is 0x0.
@@ -1493,8 +1521,6 @@ to_pow2 (size_t x)
       xwindow->frame.height == new_size.height)
     return;
 
       xwindow->frame.height == new_size.height)
     return;
 
-  [self prepareContext];
-
 #  if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
   [ogl_ctx update];
 #  endif // BACKBUFFER_OPENGL && !USE_IPHONE
 #  if defined(BACKBUFFER_OPENGL) && !defined(USE_IPHONE)
   [ogl_ctx update];
 #  endif // BACKBUFFER_OPENGL && !USE_IPHONE
@@ -1567,11 +1593,13 @@ to_pow2 (size_t x)
 
   if (!initted_p) {
 
 
   if (!initted_p) {
 
+    resized_p = NO;
+
     if (! xdpy) {
 # ifdef JWXYZ_QUARTZ
       xwindow->cgc = backbuffer;
 # endif // JWXYZ_QUARTZ
     if (! xdpy) {
 # ifdef JWXYZ_QUARTZ
       xwindow->cgc = backbuffer;
 # endif // JWXYZ_QUARTZ
-      xdpy = jwxyz_make_display (xwindow);
+      xdpy = jwxyz_quartz_make_display (xwindow);
 
 # if defined USE_IPHONE
       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
 
 # if defined USE_IPHONE
       /* Some X11 hacks (fluidballs) want to ignore all rotation events. */
@@ -1583,6 +1611,24 @@ to_pow2 (size_t x)
 #  endif // !JWXYZ_GL
 # endif // USE_IPHONE
 
 #  endif // !JWXYZ_GL
 # endif // USE_IPHONE
 
+      _lowrez_p = get_boolean_resource (xdpy, "lowrez", "Lowrez");
+      if (_lowrez_p) {
+        resized_p = YES;
+
+# if !defined __OPTIMIZE__ || TARGET_IPHONE_SIMULATOR
+        NSSize  b = [self bounds].size;
+        CGFloat s = self.hackedContentScaleFactor;
+#  ifdef USE_IPHONE
+        CGFloat o = self.contentScaleFactor;
+#  else
+        CGFloat o = self.window.backingScaleFactor;
+#  endif
+        NSLog(@"lowrez: scaling %.0fx%.0f -> %.0fx%.0f (%.02f)",
+              b.width * o, b.height * o,
+              b.width * s, b.height * s, s);
+# endif
+      }
+
       [self resize_x11];
     }
 
       [self resize_x11];
     }
 
@@ -1592,7 +1638,6 @@ to_pow2 (size_t x)
         xsft->setup_cb (xsft, xsft->setup_arg);
     }
     initted_p = YES;
         xsft->setup_cb (xsft, xsft->setup_arg);
     }
     initted_p = YES;
-    resized_p = NO;
     NSAssert(!xdata, @"xdata already initialized");
 
 
     NSAssert(!xdata, @"xdata already initialized");
 
 
@@ -1632,10 +1677,11 @@ to_pow2 (size_t x)
 
     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
       fpst = fps_init (xdpy, xwindow);
 
     if (get_boolean_resource (xdpy, "doFPS", "DoFPS")) {
       fpst = fps_init (xdpy, xwindow);
-      if (! xsft->fps_cb) xsft->fps_cb = screenhack_do_fps;
+      fps_cb = xsft->fps_cb;
+      if (! fps_cb) fps_cb = screenhack_do_fps;
     } else {
       fpst = NULL;
     } else {
       fpst = NULL;
-      xsft->fps_cb = 0;
+      fps_cb = 0;
     }
 
 # ifdef USE_IPHONE
     }
 
 # ifdef USE_IPHONE
@@ -1736,8 +1782,8 @@ to_pow2 (size_t x)
   // NSAssert(xdata, @"no xdata when drawing");
   if (! xdata) abort();
   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
   // NSAssert(xdata, @"no xdata when drawing");
   if (! xdata) abort();
   unsigned long delay = xsft->draw_cb (xdpy, xwindow, xdata);
-  if (fpst && xsft->fps_cb)
-    xsft->fps_cb (xdpy, xwindow, fpst, xdata);
+  if (fpst && fps_cb)
+    fps_cb (xdpy, xwindow, fpst, xdata);
 
   gettimeofday (&tv, 0);
   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
 
   gettimeofday (&tv, 0);
   now = tv.tv_sec + (tv.tv_usec / 1000000.0);
@@ -1810,6 +1856,10 @@ to_pow2 (size_t x)
 # if defined USE_IPHONE && defined JWXYZ_QUARTZ
   UIGraphicsPopContext();
 # endif
 # if defined USE_IPHONE && defined JWXYZ_QUARTZ
   UIGraphicsPopContext();
 # endif
+
+# ifdef USE_TOUCHBAR
+  if (touchbar_view) [touchbar_view animateOneFrame];
+# endif
 }
 
 
 }
 
 
@@ -2008,11 +2058,7 @@ to_pow2 (size_t x)
   
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
   
   NSPoint p = [[[e window] contentView] convertPoint:[e locationInWindow]
                                             toView:self];
-# ifdef USE_IPHONE
   double s = [self hackedContentScaleFactor];
   double s = [self hackedContentScaleFactor];
-# else
-  int s = 1;
-# endif
   int x = s * p.x;
   int y = s * ([self bounds].size.height - p.y);
 
   int x = s * p.x;
   int y = s * ([self bounds].size.height - p.y);
 
@@ -2030,7 +2076,7 @@ to_pow2 (size_t x)
                              [e deltaX] < 0 ? Button7 :
                              0);
       else
                              [e deltaX] < 0 ? Button7 :
                              0);
       else
-        xe.xbutton.button = [e buttonNumber] + 1;
+        xe.xbutton.button = (unsigned int) [e buttonNumber] + 1;
       break;
     case MotionNotify:
       xe.xmotion.x = x;
       break;
     case MotionNotify:
       xe.xmotion.x = x;
@@ -2084,9 +2130,9 @@ to_pow2 (size_t x)
             case NSF12FunctionKey:       k = XK_F12;       break;
             default:
               {
             case NSF12FunctionKey:       k = XK_F12;       break;
             default:
               {
-                const char *s =
+                const char *ss =
                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
                   [ns cStringUsingEncoding:NSISOLatin1StringEncoding];
-                k = (s && *s ? *s : 0);
+                k = (ss && *ss ? *ss : 0);
               }
               break;
             }
               }
               break;
             }
@@ -2238,6 +2284,12 @@ to_pow2 (size_t x)
 #else  // USE_IPHONE
 
 
 #else  // USE_IPHONE
 
 
+- (void) stopAndClose
+{
+  [self stopAndClose:NO];
+}
+
+
 - (void) stopAndClose:(Bool)relaunch_p
 {
   if ([self isAnimating])
 - (void) stopAndClose:(Bool)relaunch_p
 {
   if ([self isAnimating])
@@ -2269,11 +2321,10 @@ to_pow2 (size_t x)
 /* We distinguish between taps and drags.
 
    - Drags/pans (down, motion, up) are sent to the saver to handle.
 /* We distinguish between taps and drags.
 
    - Drags/pans (down, motion, up) are sent to the saver to handle.
-   - Single-taps exit the saver.
+   - Single-taps are sent to the saver to handle.
    - Double-taps are sent to the saver as a "Space" keypress.
    - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
    - Double-taps are sent to the saver as a "Space" keypress.
    - Swipes (really, two-finger drags/pans) send Up/Down/Left/RightArrow keys.
-
-   This means a saver cannot respond to a single-tap.  Only a few try to.
+   - All taps expose the momentary "Close" button.
  */
 
 - (void)initGestures
  */
 
 - (void)initGestures
@@ -2286,7 +2337,7 @@ to_pow2 (size_t x)
 
   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
                                    initWithTarget:self
 
   UITapGestureRecognizer *stap = [[UITapGestureRecognizer alloc]
                                    initWithTarget:self
-                                   action:@selector(handleTap)];
+                                   action:@selector(handleTap:)];
   stap.numberOfTapsRequired = 1;
   stap.numberOfTouchesRequired = 1;
  
   stap.numberOfTapsRequired = 1;
   stap.numberOfTouchesRequired = 1;
  
@@ -2314,10 +2365,16 @@ to_pow2 (size_t x)
   hold.numberOfTouchesRequired = 1;
   hold.minimumPressDuration = 0.25;   /* 1/4th second */
 
   hold.numberOfTouchesRequired = 1;
   hold.minimumPressDuration = 0.25;   /* 1/4th second */
 
+  // Two finger pinch to zoom in on the view.
+  UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] 
+                                      initWithTarget:self 
+                                      action:@selector(handlePinch:)];
+
   [stap requireGestureRecognizerToFail: dtap];
   [stap requireGestureRecognizerToFail: hold];
   [dtap requireGestureRecognizerToFail: hold];
   [pan  requireGestureRecognizerToFail: hold];
   [stap requireGestureRecognizerToFail: dtap];
   [stap requireGestureRecognizerToFail: hold];
   [dtap requireGestureRecognizerToFail: hold];
   [pan  requireGestureRecognizerToFail: hold];
+  [pan2 requireGestureRecognizerToFail: pinch];
 
   [self setMultipleTouchEnabled:YES];
 
 
   [self setMultipleTouchEnabled:YES];
 
@@ -2326,12 +2383,14 @@ to_pow2 (size_t x)
   [self addGestureRecognizer: pan];
   [self addGestureRecognizer: pan2];
   [self addGestureRecognizer: hold];
   [self addGestureRecognizer: pan];
   [self addGestureRecognizer: pan2];
   [self addGestureRecognizer: hold];
+  [self addGestureRecognizer: pinch];
 
   [dtap release];
   [stap release];
   [pan  release];
   [pan2 release];
   [hold release];
 
   [dtap release];
   [stap release];
   [pan  release];
   [pan2 release];
   [hold release];
+  [pinch release];
 }
 
 
 }
 
 
@@ -2372,7 +2431,7 @@ to_pow2 (size_t x)
 {
   CGFloat xx = p->x, yy = p->y;
 
 {
   CGFloat xx = p->x, yy = p->y;
 
-# if TARGET_IPHONE_SIMULATOR
+# if 0 // TARGET_IPHONE_SIMULATOR
   {
     XWindowAttributes xgwa;
     XGetWindowAttributes (xdpy, xwindow, &xgwa);
   {
     XWindowAttributes xgwa;
     XGetWindowAttributes (xdpy, xwindow, &xgwa);
@@ -2431,7 +2490,7 @@ to_pow2 (size_t x)
   p->x = xx * s;
   p->y = yy * s;
 
   p->x = xx * s;
   p->y = yy * s;
 
-# if TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
+# if 0 // TARGET_IPHONE_SIMULATOR || !defined __OPTIMIZE__
   {
     XWindowAttributes xgwa;
     XGetWindowAttributes (xdpy, xwindow, &xgwa);
   {
     XWindowAttributes xgwa;
     XGetWindowAttributes (xdpy, xwindow, &xgwa);
@@ -2450,9 +2509,36 @@ to_pow2 (size_t x)
 
 /* Single click exits saver.
  */
 
 /* Single click exits saver.
  */
-- (void) handleTap
+- (void) handleTap:(UIGestureRecognizer *)sender
 {
 {
-  [self stopAndClose:NO];
+  if (!xwindow)
+    return;
+
+  XEvent xe;
+  memset (&xe, 0, sizeof(xe));
+
+  [self showCloseButton];
+
+  CGPoint p = [sender locationInView:self];  // this is in points, not pixels
+  [self convertMouse:&p];
+  NSAssert (xwindow->type == WINDOW, @"not a window");
+  xwindow->window.last_mouse_x = p.x;
+  xwindow->window.last_mouse_y = p.y;
+
+  xe.xany.type = ButtonPress;
+  xe.xbutton.button = 1;
+  xe.xbutton.x = p.x;
+  xe.xbutton.y = p.y;
+
+  if (! [self sendEvent: &xe])
+    ; //[self beep];
+
+  xe.xany.type = ButtonRelease;
+  xe.xbutton.button = 1;
+  xe.xbutton.x = p.x;
+  xe.xbutton.y = p.y;
+
+  [self sendEvent: &xe];
 }
 
 
 }
 
 
@@ -2462,6 +2548,8 @@ to_pow2 (size_t x)
 {
   if (!xsft->event_cb || !xwindow) return;
 
 {
   if (!xsft->event_cb || !xwindow) return;
 
+  [self showCloseButton];
+
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
   xe.xkey.keycode = ' ';
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
   xe.xkey.keycode = ' ';
@@ -2480,6 +2568,8 @@ to_pow2 (size_t x)
 {
   if (!xsft->event_cb || !xwindow) return;
 
 {
   if (!xsft->event_cb || !xwindow) return;
 
+  [self showCloseButton];
+
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
 
   XEvent xe;
   memset (&xe, 0, sizeof(xe));
 
@@ -2536,6 +2626,8 @@ to_pow2 (size_t x)
 {
   if (!xsft->event_cb || !xwindow) return;
 
 {
   if (!xsft->event_cb || !xwindow) return;
 
+  [self showCloseButton];
+
   if (sender.state != UIGestureRecognizerStateEnded)
     return;
 
   if (sender.state != UIGestureRecognizerStateEnded)
     return;
 
@@ -2558,6 +2650,102 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
+/* Pinch with 2 fingers: zoom in around the center of the fingers.
+ */
+- (void) handlePinch:(UIPinchGestureRecognizer *)sender
+{
+  if (!xsft->event_cb || !xwindow) return;
+
+  [self showCloseButton];
+
+  if (sender.state == UIGestureRecognizerStateBegan)
+    pinch_transform = self.transform;  // Save the base transform
+
+  switch (sender.state) {
+  case UIGestureRecognizerStateBegan:
+  case UIGestureRecognizerStateChanged:
+    {
+      double scale = sender.scale;
+
+      if (scale < 1)
+        return;
+
+      self.transform = CGAffineTransformScale (pinch_transform, scale, scale);
+
+      CGPoint p = [sender locationInView: self];
+      p.x /= self.layer.bounds.size.width;
+      p.y /= self.layer.bounds.size.height;
+
+      CGPoint np = CGPointMake (self.bounds.size.width * p.x,
+                                self.bounds.size.height * p.y);
+      CGPoint op = CGPointMake (self.bounds.size.width *
+                                self.layer.anchorPoint.x, 
+                                self.bounds.size.height *
+                                self.layer.anchorPoint.y);
+      np = CGPointApplyAffineTransform (np, self.transform);
+      op = CGPointApplyAffineTransform (op, self.transform);
+
+      CGPoint pos = self.layer.position;
+      pos.x -= op.x;
+      pos.x += np.x;
+      pos.y -= op.y;
+      pos.y += np.y;
+      self.layer.position = pos;
+      self.layer.anchorPoint = p;
+    }
+    break;
+
+  case UIGestureRecognizerStateEnded:
+    {
+      // When released, snap back to the default zoom (but animate it).
+
+      CABasicAnimation *a1 = [CABasicAnimation
+                               animationWithKeyPath:@"position.x"];
+      a1.fromValue = [NSNumber numberWithFloat: self.layer.position.x];
+      a1.toValue   = [NSNumber numberWithFloat: self.bounds.size.width / 2];
+
+      CABasicAnimation *a2 = [CABasicAnimation
+                               animationWithKeyPath:@"position.y"];
+      a2.fromValue = [NSNumber numberWithFloat: self.layer.position.y];
+      a2.toValue   = [NSNumber numberWithFloat: self.bounds.size.height / 2];
+
+      CABasicAnimation *a3 = [CABasicAnimation
+                               animationWithKeyPath:@"anchorPoint.x"];
+      a3.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.x];
+      a3.toValue   = [NSNumber numberWithFloat: 0.5];
+
+      CABasicAnimation *a4 = [CABasicAnimation
+                               animationWithKeyPath:@"anchorPoint.y"];
+      a4.fromValue = [NSNumber numberWithFloat: self.layer.anchorPoint.y];
+      a4.toValue   = [NSNumber numberWithFloat: 0.5];
+
+      CABasicAnimation *a5 = [CABasicAnimation
+                               animationWithKeyPath:@"transform.scale"];
+      a5.fromValue = [NSNumber numberWithFloat: sender.scale];
+      a5.toValue   = [NSNumber numberWithFloat: 1.0];
+
+      CAAnimationGroup *group = [CAAnimationGroup animation];
+      group.duration     = 0.3;
+      group.repeatCount  = 1;
+      group.autoreverses = NO;
+      group.animations = @[ a1, a2, a3, a4, a5 ];
+      group.timingFunction = [CAMediaTimingFunction
+                               functionWithName:
+                                 kCAMediaTimingFunctionEaseIn];
+      [self.layer addAnimation:group forKey:@"unpinch"];
+
+      self.transform = pinch_transform;
+      self.layer.anchorPoint = CGPointMake (0.5, 0.5);
+      self.layer.position = CGPointMake (self.bounds.size.width / 2,
+                                         self.bounds.size.height / 2);
+    }
+    break;
+  default:
+    abort();
+  }
+}
+
+
 /* We need this to respond to "shake" gestures
  */
 - (BOOL)canBecomeFirstResponder
 /* We need this to respond to "shake" gestures
  */
 - (BOOL)canBecomeFirstResponder
@@ -2582,6 +2770,117 @@ to_pow2 (size_t x)
 }
 
 
 }
 
 
+- (void) showCloseButton
+{
+  double iw = 24;
+  double ih = iw;
+  double off = 4;
+
+  if (!closeBox) {
+    int width = self.bounds.size.width;
+    closeBox = [[UIView alloc]
+                initWithFrame:CGRectMake(0, 0, width, ih + off)];
+    closeBox.backgroundColor = [UIColor clearColor];
+    closeBox.autoresizingMask =
+      UIViewAutoresizingFlexibleBottomMargin |
+      UIViewAutoresizingFlexibleWidth;
+
+    // Add the buttons to the bar
+    UIImage *img1 = [UIImage imageNamed:@"stop"];
+    UIImage *img2 = [UIImage imageNamed:@"settings"];
+
+    UIButton *button = [[UIButton alloc] init];
+    [button setFrame: CGRectMake(off, off, iw, ih)];
+    [button setBackgroundImage:img1 forState:UIControlStateNormal];
+    [button addTarget:self
+            action:@selector(stopAndClose)
+            forControlEvents:UIControlEventTouchUpInside];
+    [closeBox addSubview:button];
+    [button release];
+
+    button = [[UIButton alloc] init];
+    [button setFrame: CGRectMake(width - iw - off, off, iw, ih)];
+    [button setBackgroundImage:img2 forState:UIControlStateNormal];
+    [button addTarget:self
+            action:@selector(stopAndOpenSettings)
+            forControlEvents:UIControlEventTouchUpInside];
+    button.autoresizingMask =
+      UIViewAutoresizingFlexibleBottomMargin |
+      UIViewAutoresizingFlexibleLeftMargin;
+    [closeBox addSubview:button];
+    [button release];
+
+    [self addSubview:closeBox];
+  }
+
+  // Don't hide the buttons under the iPhone X bezel.
+  UIEdgeInsets is = { 0, };
+  if ([self respondsToSelector:@selector(safeAreaInsets)]) {
+#   pragma clang diagnostic push   // "only available on iOS 11.0 or newer"
+#   pragma clang diagnostic ignored "-Wunguarded-availability-new"
+    is = [self safeAreaInsets];
+#   pragma clang diagnostic pop
+    [closeBox setFrame:CGRectMake(is.left, is.top,
+                                  self.bounds.size.width - is.right - is.left,
+                                  ih + off)];
+  }
+
+  if (closeBox.layer.opacity <= 0) {  // Fade in
+
+    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+    anim.duration     = 0.2;
+    anim.repeatCount  = 1;
+    anim.autoreverses = NO;
+    anim.fromValue    = [NSNumber numberWithFloat:0.0];
+    anim.toValue      = [NSNumber numberWithFloat:1.0];
+    [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
+    closeBox.layer.opacity = 1;
+  }
+
+  // Fade out N seconds from now.
+  if (closeBoxTimer)
+    [closeBoxTimer invalidate];
+  closeBoxTimer = [NSTimer scheduledTimerWithTimeInterval: 3
+                           target:self
+                           selector:@selector(closeBoxOff)
+                           userInfo:nil
+                           repeats:NO];
+}
+
+
+- (void)closeBoxOff
+{
+  if (closeBoxTimer) {
+    [closeBoxTimer invalidate];
+    closeBoxTimer = 0;
+  }
+  if (!closeBox)
+    return;
+
+  CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
+  anim.duration     = 0.2;
+  anim.repeatCount  = 1;
+  anim.autoreverses = NO;
+  anim.fromValue    = [NSNumber numberWithFloat: 1];
+  anim.toValue      = [NSNumber numberWithFloat: 0];
+  [closeBox.layer addAnimation:anim forKey:@"animateOpacity"];
+  closeBox.layer.opacity = 0;
+}
+
+
+- (void) stopAndOpenSettings
+{
+  NSString *s = [NSString stringWithCString:xsft->progclass
+                          encoding:NSISOLatin1StringEncoding];
+  if ([self isAnimating])
+    [self stopAnimation];
+  [self resignFirstResponder];
+  [_delegate wantsFadeOut:self];
+  [_delegate openPreferences: s];
+
+}
+
+
 - (void)setScreenLocked:(BOOL)locked
 {
   if (screenLocked == locked) return;
 - (void)setScreenLocked:(BOOL)locked
 {
   if (screenLocked == locked) return;