Bug 1533562 - Implement titlebar gradient drawing with a new TitlebarGradientView. r=spohl
☠☠ backed out by 73e8dcb8be07 ☠ ☠
authorMarkus Stange <mstange@themasta.com>
Mon, 22 Apr 2019 19:26:56 +0000
changeset 470394 8970cdb3c04a11ddbe86c53d5e759685949a0cb3
parent 470393 498cd34eea782ce73c1eaffd193427add877117c
child 470395 b4440b1833ece120e7680763b55168913b18e0d4
push id35905
push userdvarga@mozilla.com
push dateTue, 23 Apr 2019 09:53:27 +0000
treeherdermozilla-central@831918f009f6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspohl
bugs1533562
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1533562 - Implement titlebar gradient drawing with a new TitlebarGradientView. r=spohl Differential Revision: https://phabricator.services.mozilla.com/D22646
widget/cocoa/nsCocoaWindow.h
widget/cocoa/nsCocoaWindow.mm
widget/cocoa/nsWindowMap.mm
--- a/widget/cocoa/nsCocoaWindow.h
+++ b/widget/cocoa/nsCocoaWindow.h
@@ -154,20 +154,27 @@ typedef struct _nsCocoaWindowList {
 - (id)initWithGeckoWindow:(nsCocoaWindow*)geckoWind;
 - (void)windowDidResize:(NSNotification*)aNotification;
 - (nsCocoaWindow*)geckoWidget;
 - (bool)toplevelActiveState;
 - (void)sendToplevelActivateEvents;
 - (void)sendToplevelDeactivateEvents;
 @end
 
-@class ToolbarWindow;
+@interface TitlebarGradientView : NSView
+@end
 
 // NSWindow subclass for handling windows with toolbars.
 @interface ToolbarWindow : BaseWindow {
+  // This window's titlebar gradient view, if present.
+  // Will be nil if drawsContentsIntoWindowFrame is YES.
+  // This view is a subview of the window's content view and gets created and
+  // destroyed by updateTitlebarGradientViewPresence.
+  TitlebarGradientView* mTitlebarGradientView;  // [STRONG]
+
   CGFloat mUnifiedToolbarHeight;
   CGFloat mSheetAttachmentPosition;
   NSRect mWindowButtonsRect;
   NSRect mFullScreenButtonRect;
 }
 - (void)setUnifiedToolbarHeight:(CGFloat)aHeight;
 - (CGFloat)unifiedToolbarHeight;
 - (CGFloat)titlebarHeight;
@@ -175,16 +182,17 @@ typedef struct _nsCocoaWindowList {
 - (void)setTitlebarNeedsDisplay;
 - (void)setDrawsContentsIntoWindowFrame:(BOOL)aState;
 - (void)setSheetAttachmentPosition:(CGFloat)aY;
 - (CGFloat)sheetAttachmentPosition;
 - (void)placeWindowButtons:(NSRect)aRect;
 - (void)placeFullScreenButton:(NSRect)aRect;
 - (NSPoint)windowButtonsPositionWithDefaultPosition:(NSPoint)aDefaultPosition;
 - (NSPoint)fullScreenButtonPositionWithDefaultPosition:(NSPoint)aDefaultPosition;
+- (void)windowMainStateChanged;
 @end
 
 class nsCocoaWindow final : public nsBaseWidget, public nsPIWidgetCocoa {
  private:
   typedef nsBaseWidget Inherited;
 
  public:
   nsCocoaWindow();
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -2400,31 +2400,40 @@ already_AddRefed<nsIWidget> nsIWidget::C
   ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
   // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
   // app modally. If one of those is up then we want it to retain its menu bar.
   if ([NSApp _isRunningAppModal]) return;
   NSWindow* window = [aNotification object];
   if (window) [WindowDelegate paintMenubarForWindow:window];
 
+  if ([window isKindOfClass:[ToolbarWindow class]]) {
+    [(ToolbarWindow*)window windowMainStateChanged];
+  }
+
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 - (void)windowDidResignMain:(NSNotification*)aNotification {
   RollUpPopups();
   ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
   // [NSApp _isRunningAppModal] will return true if we're running an OS dialog
   // app modally. If one of those is up then we want it to retain its menu bar.
   if ([NSApp _isRunningAppModal]) return;
   RefPtr<nsMenuBarX> hiddenWindowMenuBar = nsMenuUtilsX::GetHiddenWindowMenuBar();
   if (hiddenWindowMenuBar) {
     // printf("painting hidden window menu bar due to window losing main status\n");
     hiddenWindowMenuBar->Paint();
   }
+
+  NSWindow* window = [aNotification object];
+  if ([window isKindOfClass:[ToolbarWindow class]]) {
+    [(ToolbarWindow*)window windowMainStateChanged];
+  }
 }
 
 - (void)windowDidBecomeKey:(NSNotification*)aNotification {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   RollUpPopups();
   ChildViewMouseTracker::ReEvaluateMouseEnterState();
 
@@ -2844,19 +2853,16 @@ static const NSString* kStateCollectionB
   return state;
 }
 
 - (void)setDrawsContentsIntoWindowFrame:(BOOL)aState {
   bool changed = (aState != mDrawsIntoWindowFrame);
   mDrawsIntoWindowFrame = aState;
   if (changed) {
     [self reflowTitlebarElements];
-    if ([self respondsToSelector:@selector(setTitlebarAppearsTransparent:)]) {
-      [self setTitlebarAppearsTransparent:mDrawsIntoWindowFrame];
-    }
   }
 }
 
 - (BOOL)drawsContentsIntoWindowFrame {
   return mDrawsIntoWindowFrame;
 }
 
 - (NSRect)childViewRectForFrameRect:(NSRect)aFrameRect {
@@ -3079,73 +3085,157 @@ static const NSString* kStateCollectionB
 }
 
 - (void)releaseJSObjects {
   [mTouchBar releaseJSObjects];
 }
 
 @end
 
-// This class allows us to exercise control over the window's title bar. This
-// allows for a "unified toolbar" look without having to extend the content
-// area into the title bar.
+@interface NSView (NSThemeFrame)
+- (void)_drawTitleStringInClip:(NSRect)aRect;
+- (void)_maskCorners:(NSUInteger)aFlags clipRect:(NSRect)aRect;
+@end
+
+@implementation TitlebarGradientView
+
+- (void)drawRect:(NSRect)aRect {
+  CGContextRef ctx = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
+  ToolbarWindow* window = (ToolbarWindow*)[self window];
+  nsNativeThemeCocoa::DrawNativeTitlebar(ctx, NSRectToCGRect([self bounds]),
+                                         [window unifiedToolbarHeight], [window isMainWindow], NO);
+
+  // The following is only necessary because we're not using
+  // NSFullSizeContentViewWindowMask yet: We need to mask our drawing to the
+  // rounded top corners of the window, and we need to draw the title string
+  // on top. That's because the title string is drawn as part of the frame view
+  // and this view covers that drawing up.
+  // Once we use NSFullSizeContentViewWindowMask and remove our override of
+  // _wantsFloatingTitlebar, Cocoa will draw the title string as part of a
+  // separate view which sits on top of the window's content view, and we'll be
+  // able to remove the code below.
+
+  NSView* frameView = [[[self window] contentView] superview];
+  if (!frameView || ![frameView respondsToSelector:@selector(_maskCorners:clipRect:)] ||
+      ![frameView respondsToSelector:@selector(_drawTitleStringInClip:)]) {
+    return;
+  }
+
+  NSPoint offsetToFrameView = [self convertPoint:NSZeroPoint toView:frameView];
+  NSRect clipRect = {offsetToFrameView, [self bounds].size};
+
+  // Both this view and frameView return NO from isFlipped. Switch into
+  // frameView's coordinate system using a translation by the offset.
+  CGContextSaveGState(ctx);
+  CGContextTranslateCTM(ctx, -offsetToFrameView.x, -offsetToFrameView.y);
+
+  [frameView _maskCorners:2 clipRect:clipRect];
+  [frameView _drawTitleStringInClip:clipRect];
+
+  CGContextRestoreGState(ctx);
+}
+
+- (BOOL)isOpaque {
+  return YES;
+}
+
+- (BOOL)mouseDownCanMoveWindow {
+  return YES;
+}
+
+- (NSView*)hitTest:(NSPoint)aPoint {
+  return nil;
+}
+
+@end
+
+// This class allows us to exercise control over the window's title bar. It is
+// used for all windows with titlebars.
 //
-// Drawing the unified gradient in the titlebar and the toolbar works like this:
+// ToolbarWindow supports two modes:
+//  - drawsContentsIntoWindowFrame mode: In this mode, the Gecko ChildView is
+//    sized to cover the entire window frame and manages titlebar drawing.
+//  - separate titlebar mode, with support for unified toolbars: In this mode,
+//    the Gecko ChildView does not extend into the titlebar. However, this
+//    window's content view (which is the ChildView's superview) *does* extend
+//    into the titlebar. Moreover, in this mode, we place a TitlebarGradientView
+//    in the content view, as a sibling of the ChildView.
+//
+// The "separate titlebar mode" supports the "unified toolbar" look:
+// If there's a toolbar right below the titlebar, the two can "connect" and
+// form a single gradient without a separator line in between.
+//
+// The following mechanism communicates the height of the unified toolbar to
+// the ToolbarWindow:
+//
 // 1) In the style sheet we set the toolbar's -moz-appearance to toolbar.
 // 2) When the toolbar is visible and we paint the application chrome
 //    window, the array that Gecko passes nsChildView::UpdateThemeGeometries
 //    will contain an entry for the widget type StyleAppearance::Toolbar.
-// 3) nsChildView::UpdateThemeGeometries finds the toolbar frame's ToolbarWindow
-//    and passes the toolbar frame's height to setUnifiedToolbarHeight.
-// 4) If the toolbar height has changed, a titlebar redraw is triggered and the
-//    upper part of the unified gradient is drawn in the titlebar.
-// 5) The lower part of the unified gradient in the toolbar is drawn during
-//    normal window content painting in nsNativeThemeCocoa::DrawUnifiedToolbar.
+// 3) nsChildView::UpdateThemeGeometries passes the toolbar's height, plus the
+//    titlebar height, to -[ToolbarWindow setUnifiedToolbarHeight:].
 //
-// Whenever the unified gradient is drawn in the titlebar or the toolbar, both
-// titlebar height and toolbar height must be known in order to construct the
-// correct gradient. But you can only get from the toolbar frame
-// to the containing window - the other direction doesn't work. That's why the
-// toolbar height is cached in the ToolbarWindow but nsNativeThemeCocoa can simply
-// query the window for its titlebar height when drawing the toolbar.
-//
-// Note that in drawsContentsIntoWindowFrame mode, titlebar drawing works in a
-// completely different way: In that mode, the window's mainChildView will
-// cover the titlebar completely and nothing that happens in the window
-// background will reach the screen.
+// The actual drawing of the gradient happens in two parts: The titlebar part
+// (i.e. the top 22 pixels of the gradient) is drawn by the TitlebarGradientView,
+// which is a subview of the window's content view and a sibling of the ChildView.
+// The rest of the gradient is drawn by Gecko into the ChildView, as part of the
+// -moz-appearance rendering of the toolbar.
 @implementation ToolbarWindow
 
 - (id)initWithContentRect:(NSRect)aContentRect
                 styleMask:(NSUInteger)aStyle
                   backing:(NSBackingStoreType)aBufferingType
                     defer:(BOOL)aFlag {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
 
   if ((self = [super initWithContentRect:aContentRect
                                styleMask:aStyle
                                  backing:aBufferingType
                                    defer:aFlag])) {
+    mTitlebarGradientView = nil;
     mUnifiedToolbarHeight = 22.0f;
     mSheetAttachmentPosition = aContentRect.size.height;
     mWindowButtonsRect = NSZeroRect;
     mFullScreenButtonRect = NSZeroRect;
+
+    if ([self respondsToSelector:@selector(setTitlebarAppearsTransparent:)]) {
+      [self setTitlebarAppearsTransparent:YES];
+    }
+
+    [self updateTitlebarGradientViewPresence];
   }
   return self;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NIL;
 }
 
+- (void)dealloc {
+  [mTitlebarGradientView release];
+  [super dealloc];
+}
+
+- (void)updateTitlebarGradientViewPresence {
+  BOOL needTitlebarView = ![self drawsContentsIntoWindowFrame];
+  if (needTitlebarView && !mTitlebarGradientView) {
+    mTitlebarGradientView = [[TitlebarGradientView alloc] initWithFrame:[self titlebarRect]];
+    mTitlebarGradientView.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
+    [self.contentView addSubview:mTitlebarGradientView];
+  } else if (!needTitlebarView && mTitlebarGradientView) {
+    [mTitlebarGradientView removeFromSuperview];
+    [mTitlebarGradientView release];
+    mTitlebarGradientView = nil;
+  }
+}
+
+- (void)windowMainStateChanged {
+  [self setTitlebarNeedsDisplay];
+}
+
 - (void)setTitlebarNeedsDisplay {
-  NSRect rect = [self titlebarRect];
-  if (NSIsEmptyRect(rect)) return;
-
-  NSView* borderView = [[self contentView] superview];
-  if (!borderView) return;
-
-  [borderView setNeedsDisplayInRect:rect];
+  [mTitlebarGradientView setNeedsDisplay:YES];
 }
 
 - (NSRect)titlebarRect {
   CGFloat titlebarHeight = [self titlebarHeight];
   return NSMakeRect(0, [self frame].size.height - titlebarHeight, [self frame].size.width,
                     titlebarHeight);
 }
 
@@ -3194,16 +3284,18 @@ static const NSString* kStateCollectionB
 
     // Resizing the content area causes a reflow which would send a synthesized
     // mousemove event to the old mouse position relative to the top left
     // corner of the content area. But the mouse has shifted relative to the
     // content area, so that event would have wrong position information. So
     // we'll send a mouse move event with the correct new position.
     ChildViewMouseTracker::ResendLastMouseMoveEvent();
   }
+
+  [self updateTitlebarGradientViewPresence];
 }
 
 - (void)setWantsTitleDrawn:(BOOL)aDrawTitle {
   [super setWantsTitleDrawn:aDrawTitle];
   [self setTitlebarNeedsDisplay];
 }
 
 - (void)setSheetAttachmentPosition:(CGFloat)aY {
--- a/widget/cocoa/nsWindowMap.mm
+++ b/widget/cocoa/nsWindowMap.mm
@@ -225,31 +225,27 @@
   NSWindow* window = (NSWindow*)[inNotification object];
 
   id delegate = [window delegate];
   if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) {
     [TopLevelWindowData activateInWindowViews:window];
   } else if ([window isSheet]) {
     [TopLevelWindowData activateInWindow:window];
   }
-
-  [[window contentView] setNeedsDisplay:YES];
 }
 
 - (void)windowResignedKey:(NSNotification*)inNotification {
   NSWindow* window = (NSWindow*)[inNotification object];
 
   id delegate = [window delegate];
   if (!delegate || ![delegate isKindOfClass:[WindowDelegate class]]) {
     [TopLevelWindowData deactivateInWindowViews:window];
   } else if ([window isSheet]) {
     [TopLevelWindowData deactivateInWindow:window];
   }
-
-  [[window contentView] setNeedsDisplay:YES];
 }
 
 // The appearance of a top-level window depends on its main state (not its key
 // state).  So (for non-embedders) we need to ensure that a top-level window
 // is main when an NS_ACTIVATE event is sent to Gecko for it.
 - (void)windowBecameMain:(NSNotification*)inNotification {
   NSWindow* window = (NSWindow*)[inNotification object];