diff options
author | tapted <tapted@chromium.org> | 2015-07-21 01:24:39 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-21 08:25:20 +0000 |
commit | ecd11011377bb533122e27f97569414ab04bea7e (patch) | |
tree | e0333e7754af901c28cd0e1e2be40ad2bb0401ee | |
parent | da9ab7ea905b7ec4ff60889911078a5994473043 (diff) | |
download | chromium_src-ecd11011377bb533122e27f97569414ab04bea7e.zip chromium_src-ecd11011377bb533122e27f97569414ab04bea7e.tar.gz chromium_src-ecd11011377bb533122e27f97569414ab04bea7e.tar.bz2 |
MacViews: Use native shadows for dialogs
OSX is able to calculate shadows from a composited layer, the window
server just needs to be told when the shadow needs to be recalculated.
Widgets have composited layers by default, so this is when a buffer swap
message arrives from the GPU process.
Add a views::BubbleBorder::NO_ASSETS property for the BubbleFrameView to
use. This is a border with no border assets - it just has rounded
corners. Bubble arrows also need no assets -- just appropriate thickness
constants for the arrow size and offset from the corner.
The only tricky part is dialogs that animate in using CGSSetWindowWarp.
It seems a "warp" that doesn't keep the centrepoint of a window
stationary causes a shadow calculated from the layer-hosting view to be
be applied incorrectly. Fix by invalidating the shadow each frame, which
works OK. (Animating the translate part of the animation with regular
setFrame calls might be an alternative).
Screenshots at http://crbug.com/507965#c1
BUG=507965
Review URL: https://codereview.chromium.org/1209043009
Cr-Commit-Position: refs/heads/master@{#339627}
-rw-r--r-- | chrome/browser/ui/views/app_list/app_list_dialog_container.cc | 9 | ||||
-rw-r--r-- | ui/views/BUILD.gn | 1 | ||||
-rw-r--r-- | ui/views/bubble/bubble_border.cc | 40 | ||||
-rw-r--r-- | ui/views/bubble/bubble_border.h | 11 | ||||
-rw-r--r-- | ui/views/bubble/bubble_border_unittest.cc | 30 | ||||
-rw-r--r-- | ui/views/cocoa/bridged_native_widget.h | 9 | ||||
-rw-r--r-- | ui/views/cocoa/bridged_native_widget.mm | 53 | ||||
-rw-r--r-- | ui/views/cocoa/bridged_native_widget_unittest.mm | 68 | ||||
-rw-r--r-- | ui/views/examples/bubble_example.cc | 8 | ||||
-rw-r--r-- | ui/views/examples/bubble_example.h | 2 | ||||
-rw-r--r-- | ui/views/views.gyp | 5 | ||||
-rw-r--r-- | ui/views/widget/native_widget_mac_unittest.mm | 128 | ||||
-rw-r--r-- | ui/views/window/dialog_delegate.cc | 15 |
13 files changed, 360 insertions, 19 deletions
diff --git a/chrome/browser/ui/views/app_list/app_list_dialog_container.cc b/chrome/browser/ui/views/app_list/app_list_dialog_container.cc index d9a947b..4b23b2e 100644 --- a/chrome/browser/ui/views/app_list/app_list_dialog_container.cc +++ b/chrome/browser/ui/views/app_list/app_list_dialog_container.cc @@ -30,8 +30,11 @@ namespace { #if defined(OS_MACOSX) const ui::ModalType kModalType = ui::MODAL_TYPE_CHILD; +const views::BubbleBorder::Shadow kShadowType = views::BubbleBorder::NO_ASSETS; #else const ui::ModalType kModalType = ui::MODAL_TYPE_WINDOW; +const views::BubbleBorder::Shadow kShadowType = + views::BubbleBorder::SMALL_SHADOW; #endif // The background for App List dialogs, which appears as a rounded rectangle @@ -210,10 +213,8 @@ class NativeDialogContainer : public BaseDialogContainer { views::NonClientFrameView* CreateNonClientFrameView( views::Widget* widget) override { FullSizeBubbleFrameView* frame = new FullSizeBubbleFrameView(); - scoped_ptr<views::BubbleBorder> border( - new views::BubbleBorder(views::BubbleBorder::FLOAT, - views::BubbleBorder::SMALL_SHADOW, - SK_ColorRED)); + scoped_ptr<views::BubbleBorder> border(new views::BubbleBorder( + views::BubbleBorder::FLOAT, kShadowType, SK_ColorRED)); border->set_use_theme_background_color(true); frame->SetBubbleBorder(border.Pass()); return frame; diff --git a/ui/views/BUILD.gn b/ui/views/BUILD.gn index 6baf80e..c14f089 100644 --- a/ui/views/BUILD.gn +++ b/ui/views/BUILD.gn @@ -270,6 +270,7 @@ test("views_unittests") { "controls/native/native_view_host_unittest.cc", "widget/window_reorderer_unittest.cc", ] + deps += [ "//ui/accelerated_widget_mac" ] } } diff --git a/ui/views/bubble/bubble_border.cc b/ui/views/bubble/bubble_border.cc index f7ac794..2345bfa 100644 --- a/ui/views/bubble/bubble_border.cc +++ b/ui/views/bubble/bubble_border.cc @@ -25,19 +25,25 @@ BorderImages::BorderImages(const int border_image_ids[], int border_interior_thickness, int arrow_interior_thickness, int corner_radius) - : border_painter(Painter::CreateImageGridPainter(border_image_ids)), - border_thickness(0), + : border_thickness(border_interior_thickness), border_interior_thickness(border_interior_thickness), - arrow_thickness(0), + arrow_thickness(arrow_interior_thickness), arrow_interior_thickness(arrow_interior_thickness), + arrow_width(2 * arrow_interior_thickness), corner_radius(corner_radius) { + if (!border_image_ids) + return; + + border_painter.reset(Painter::CreateImageGridPainter(border_image_ids)); ResourceBundle& rb = ResourceBundle::GetSharedInstance(); border_thickness = rb.GetImageSkiaNamed(border_image_ids[0])->width(); + if (arrow_image_ids[0] != 0) { left_arrow = *rb.GetImageSkiaNamed(arrow_image_ids[0]); top_arrow = *rb.GetImageSkiaNamed(arrow_image_ids[1]); right_arrow = *rb.GetImageSkiaNamed(arrow_image_ids[2]); bottom_arrow = *rb.GetImageSkiaNamed(arrow_image_ids[3]); + arrow_width = top_arrow.width(); arrow_thickness = top_arrow.height(); } } @@ -113,6 +119,9 @@ BorderImages* GetBorderImages(BubbleBorder::Shadow shadow) { case BubbleBorder::SMALL_SHADOW: set = new BorderImages(kSmallShadowImages, kSmallShadowArrows, 5, 6, 2); break; + case BubbleBorder::NO_ASSETS: + set = new BorderImages(nullptr, nullptr, 17, 8, 2); + break; case BubbleBorder::SHADOW_COUNT: NOTREACHED(); break; @@ -195,7 +204,7 @@ int BubbleBorder::GetArrowOffset(const gfx::Size& border_size) const { return edge_length / 2; // Calculate the minimum offset to not overlap arrow and corner images. - const int min = images_->border_thickness + (images_->top_arrow.width() / 2); + const int min = images_->border_thickness + (images_->arrow_width / 2); // Ensure the returned value will not cause image overlap, if possible. return std::max(min, std::min(arrow_offset_, edge_length - min)); } @@ -205,7 +214,12 @@ void BubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) { bounds.Inset(-GetBorderThickness(), -GetBorderThickness()); const gfx::Rect arrow_bounds = GetArrowRect(view.GetLocalBounds()); if (arrow_bounds.IsEmpty()) { - Painter::PaintPainterAt(canvas, images_->border_painter.get(), bounds); + if (images_->border_painter) + Painter::PaintPainterAt(canvas, images_->border_painter.get(), bounds); + return; + } + if (!images_->border_painter) { + DrawArrow(canvas, arrow_bounds); return; } @@ -248,7 +262,7 @@ gfx::Size BubbleBorder::GetSizeForContentsSize( // Ensure the bubble is large enough to not overlap border and arrow images. const int min = 2 * images_->border_thickness; - const int min_with_arrow_width = min + images_->top_arrow.width(); + const int min_with_arrow_width = min + images_->arrow_width; const int min_with_arrow_thickness = images_->border_thickness + std::max(images_->arrow_thickness + images_->border_interior_thickness, images_->border_thickness); @@ -279,7 +293,7 @@ gfx::Rect BubbleBorder::GetArrowRect(const gfx::Rect& bounds) const { gfx::Point origin; int offset = GetArrowOffset(bounds.size()); - const int half_length = images_->top_arrow.width() / 2; + const int half_length = images_->arrow_width / 2; const gfx::Insets insets = GetInsets(); if (is_arrow_on_horizontal(arrow_)) { @@ -299,7 +313,17 @@ gfx::Rect BubbleBorder::GetArrowRect(const gfx::Rect& bounds) const { else origin.set_x(bounds.width() - insets.right()); } - return gfx::Rect(origin, GetArrowImage()->size()); + + if (shadow_ != NO_ASSETS) + return gfx::Rect(origin, GetArrowImage()->size()); + + // With no assets, return the size enclosing the path filled in DrawArrow(). + DCHECK_EQ(2 * images_->arrow_interior_thickness, images_->arrow_width); + int width = images_->arrow_width; + int height = images_->arrow_interior_thickness; + if (!is_arrow_on_horizontal(arrow_)) + std::swap(width, height); + return gfx::Rect(origin, gfx::Size(width, height)); } void BubbleBorder::DrawArrow(gfx::Canvas* canvas, diff --git a/ui/views/bubble/bubble_border.h b/ui/views/bubble/bubble_border.h index 4e60fb0..88356fd 100644 --- a/ui/views/bubble/bubble_border.h +++ b/ui/views/bubble/bubble_border.h @@ -38,11 +38,18 @@ struct BorderImages { // The thickness of border and arrow images and their interior areas. // Thickness is the width of left/right and the height of top/bottom images. - // The interior is measured without including stroke or shadow pixels. + // The interior is measured without including stroke or shadow pixels. The tip + // of the arrow is |arrow_interior_thickness| from the border and the base is + // always twice that; drawn in the background color. int border_thickness; int border_interior_thickness; int arrow_thickness; int arrow_interior_thickness; + + // Width of an arrow (on the horizontal), including any shadows. Defaults to + // the width of the |top_arrow| asset. + int arrow_width; + // The corner radius of the bubble's rounded-rect interior area. int corner_radius; }; @@ -87,6 +94,7 @@ class VIEWS_EXPORT BubbleBorder : public Border { NO_SHADOW_OPAQUE_BORDER, BIG_SHADOW, SMALL_SHADOW, + NO_ASSETS, SHADOW_COUNT, }; @@ -203,6 +211,7 @@ class VIEWS_EXPORT BubbleBorder : public Border { private: FRIEND_TEST_ALL_PREFIXES(BubbleBorderTest, GetSizeForContentsSizeTest); FRIEND_TEST_ALL_PREFIXES(BubbleBorderTest, GetBoundsOriginTest); + FRIEND_TEST_ALL_PREFIXES(BubbleBorderTest, ShadowTypes); // The border and arrow stroke size used in image assets, in pixels. static const int kStroke; diff --git a/ui/views/bubble/bubble_border_unittest.cc b/ui/views/bubble/bubble_border_unittest.cc index f22df5d..d129434 100644 --- a/ui/views/bubble/bubble_border_unittest.cc +++ b/ui/views/bubble/bubble_border_unittest.cc @@ -6,6 +6,7 @@ #include "base/memory/scoped_ptr.h" #include "base/strings/stringprintf.h" +#include "ui/gfx/canvas.h" #include "ui/gfx/geometry/rect.h" #include "ui/views/test/views_test_base.h" @@ -413,4 +414,33 @@ TEST_F(BubbleBorderTest, GetBoundsOriginTest) { } } +// Ensure all the shadow types pass some size validation and paint sanely. +TEST_F(BubbleBorderTest, ShadowTypes) { + const gfx::Rect rect(0, 0, 320, 200); + View paint_view; + paint_view.SetBoundsRect(rect); + + for (int i = 0; i < BubbleBorder::SHADOW_COUNT; ++i) { + BubbleBorder::Shadow shadow = static_cast<BubbleBorder::Shadow>(i); + SCOPED_TRACE(testing::Message() << "BubbleBorder::Shadow: " << shadow); + gfx::Canvas canvas(gfx::Size(640, 480), 1.0f, false); + BubbleBorder border(BubbleBorder::TOP_LEFT, shadow, SK_ColorWHITE); + internal::BorderImages* border_images = border.GetImagesForTest(); + + // Arrow assets should always be at least as big as the drawn arrow. + EXPECT_GE(border_images->arrow_thickness, + border_images->arrow_interior_thickness); + EXPECT_GE(border_images->arrow_width, + 2 * border_images->arrow_interior_thickness); + + // Border assets should always be at least as thick as the hittable border. + EXPECT_GE(border_images->border_thickness, + border_images->border_interior_thickness); + + // For a TOP_LEFT arrow, the x-offset always matches the border thickness. + EXPECT_EQ(border.GetArrowRect(rect).x(), border_images->border_thickness); + border.Paint(paint_view, &canvas); + } +} + } // namespace views diff --git a/ui/views/cocoa/bridged_native_widget.h b/ui/views/cocoa/bridged_native_widget.h index 6e4cff5..288b0c7 100644 --- a/ui/views/cocoa/bridged_native_widget.h +++ b/ui/views/cocoa/bridged_native_widget.h @@ -27,6 +27,9 @@ class InputMethod; } namespace views { +namespace test { +class BridgedNativeWidgetTestApi; +} class CocoaMouseCapture; class NativeWidgetMac; @@ -172,6 +175,8 @@ class VIEWS_EXPORT BridgedNativeWidget bool DispatchKeyEventPostIME(const ui::KeyEvent& key) override; private: + friend class test::BridgedNativeWidgetTestApi; + // Closes all child windows. BridgedNativeWidget children will be destroyed. void RemoveOrDestroyChildren(); @@ -271,6 +276,10 @@ class VIEWS_EXPORT BridgedNativeWidget // currently hidden due to having a hidden parent. bool wants_to_be_visible_; + // If true, the window has been made visible or changed shape and the window + // shadow needs to be invalidated when a frame is received for the new shape. + bool invalidate_shadow_on_frame_swap_ = false; + DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidget); }; diff --git a/ui/views/cocoa/bridged_native_widget.mm b/ui/views/cocoa/bridged_native_widget.mm index eaa875b..0769dae 100644 --- a/ui/views/cocoa/bridged_native_widget.mm +++ b/ui/views/cocoa/bridged_native_widget.mm @@ -44,6 +44,30 @@ } @end +// This class overrides NSAnimation methods to invalidate the shadow for each +// frame. It is required because the show animation uses CGSSetWindowWarp() +// which is touchy about the consistency of the points it is given. The show +// animation includes a translate, which fails to apply properly to the window +// shadow, when that shadow is derived from a layer-hosting view. So invalidate +// it. This invalidation is only needed to cater for the translate. It is not +// required if CGSSetWindowWarp() is used in a way that keeps the center point +// of the window stationary (e.g. a scale). It's also not required for the hide +// animation: in that case, the shadow is never invalidated so retains the +// shadow calculated before a translate is applied. +@interface ModalShowAnimationWithLayer : ConstrainedWindowAnimationShow +@end + +@implementation ModalShowAnimationWithLayer +- (void)stopAnimation { + [super stopAnimation]; + [window_ invalidateShadow]; +} +- (void)setCurrentProgress:(NSAnimationProgress)progress { + [super setCurrentProgress:progress]; + [window_ invalidateShadow]; +} +@end + namespace { int kWindowPropertiesKey; @@ -269,6 +293,19 @@ void BridgedNativeWidget::Init(base::scoped_nsobject<NSWindow> window, } } + // OSX likes to put shadows on most things. However, frameless windows (with + // styleMask = NSBorderlessWindowMask) default to no shadow. So change that. + // SHADOW_TYPE_DROP is used for Menus, which get the same shadow style on Mac. + switch (params.shadow_type) { + case Widget::InitParams::SHADOW_TYPE_NONE: + [window_ setHasShadow:NO]; + break; + case Widget::InitParams::SHADOW_TYPE_DEFAULT: + case Widget::InitParams::SHADOW_TYPE_DROP: + [window_ setHasShadow:YES]; + break; + } // No default case, to pick up new types. + // Set a meaningful initial bounds. Note that except for frameless widgets // with no WidgetDelegate, the bounds will be set again by Widget after // initializing the non-client view. In the former case, if bounds were not @@ -424,7 +461,7 @@ void BridgedNativeWidget::SetVisibilityState(WindowVisibilityState new_state) { // the window appear. if (native_widget_mac_->GetWidget()->IsModal()) { base::scoped_nsobject<NSAnimation> show_animation( - [[ConstrainedWindowAnimationShow alloc] initWithWindow:window_]); + [[ModalShowAnimationWithLayer alloc] initWithWindow:window_]); // The default mode is blocking, which would block the UI thread for the // duration of the animation, but would keep it smooth. The window also // hasn't yet received a frame from the compositor at this stage, so it is @@ -824,6 +861,15 @@ bool BridgedNativeWidget::AcceleratedWidgetShouldIgnoreBackpressure() const { void BridgedNativeWidget::AcceleratedWidgetSwapCompleted( const std::vector<ui::LatencyInfo>& latency_info) { + // Ignore frames arriving "late" for an old size. A frame at the new size + // should arrive soon. + if (!compositor_widget_->HasFrameOfSize(GetClientAreaSize())) + return; + + if (invalidate_shadow_on_frame_swap_) { + invalidate_shadow_on_frame_swap_ = false; + [window_ invalidateShadow]; + } } void BridgedNativeWidget::AcceleratedWidgetHitError() { @@ -1007,6 +1053,11 @@ void BridgedNativeWidget::UpdateLayerProperties() { float scale_factor = GetDeviceScaleFactorFromView(compositor_superview_); compositor_->SetScaleAndSize(scale_factor, ConvertSizeToPixel(scale_factor, size_in_dip)); + + // For a translucent window, the shadow calculation needs to be carried out + // after the frame from the compositor arrives. + if (![window_ isOpaque]) + invalidate_shadow_on_frame_swap_ = true; } NSMutableDictionary* BridgedNativeWidget::GetWindowProperties() const { diff --git a/ui/views/cocoa/bridged_native_widget_unittest.mm b/ui/views/cocoa/bridged_native_widget_unittest.mm index 479203e..3f0d110 100644 --- a/ui/views/cocoa/bridged_native_widget_unittest.mm +++ b/ui/views/cocoa/bridged_native_widget_unittest.mm @@ -14,6 +14,7 @@ #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" #import "testing/gtest_mac.h" +#import "ui/base/cocoa/window_size_constants.h" #include "ui/base/ime/input_method.h" #import "ui/gfx/test/ui_cocoa_test_helper.h" #import "ui/views/cocoa/bridged_content_view.h" @@ -273,7 +274,34 @@ TEST_F(BridgedNativeWidgetTest, GetInputMethodShouldNotReturnNull) { } // A simpler test harness for testing initialization flows. -typedef BridgedNativeWidgetTestBase BridgedNativeWidgetInitTest; +class BridgedNativeWidgetInitTest : public BridgedNativeWidgetTestBase { + public: + BridgedNativeWidgetInitTest() {} + + // Prepares a new |window_| and |widget_| for a call to PerformInit(). + void CreateNewWidgetToInit(NSUInteger style_mask) { + window_.reset( + [[NSWindow alloc] initWithContentRect:ui::kWindowSizeDeterminedLater + styleMask:style_mask + backing:NSBackingStoreBuffered + defer:NO]); + [window_ setReleasedWhenClosed:NO]; // Owned by scoped_nsobject. + widget_.reset(new Widget); + native_widget_mac_ = new MockNativeWidgetMac(widget_.get()); + init_params_.native_widget = native_widget_mac_; + } + + void PerformInit() { + widget_->Init(init_params_); + bridge()->Init(window_, init_params_); + } + + protected: + base::scoped_nsobject<NSWindow> window_; + + private: + DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetInitTest); +}; // Test that BridgedNativeWidget remains sane if Init() is never called. TEST_F(BridgedNativeWidgetInitTest, InitNotCalled) { @@ -282,6 +310,44 @@ TEST_F(BridgedNativeWidgetInitTest, InitNotCalled) { bridge().reset(); } +// Tests the shadow type given in InitParams. +TEST_F(BridgedNativeWidgetInitTest, ShadowType) { + // Verify Widget::InitParam defaults and arguments added from SetUp(). + EXPECT_EQ(Widget::InitParams::TYPE_WINDOW_FRAMELESS, init_params_.type); + EXPECT_EQ(Widget::InitParams::OPAQUE_WINDOW, init_params_.opacity); + EXPECT_EQ(Widget::InitParams::SHADOW_TYPE_DEFAULT, init_params_.shadow_type); + + CreateNewWidgetToInit(NSBorderlessWindowMask); + EXPECT_FALSE([window_ hasShadow]); // Default for NSBorderlessWindowMask. + PerformInit(); + + // Borderless is 0, so isn't really a mask. Check that nothing is set. + EXPECT_EQ(NSBorderlessWindowMask, [window_ styleMask]); + EXPECT_TRUE([window_ hasShadow]); // SHADOW_TYPE_DEFAULT means a shadow. + + CreateNewWidgetToInit(NSBorderlessWindowMask); + init_params_.shadow_type = Widget::InitParams::SHADOW_TYPE_NONE; + PerformInit(); + EXPECT_FALSE([window_ hasShadow]); // Preserves lack of shadow. + + // Default for Widget::InitParams::TYPE_WINDOW. + NSUInteger kBorderedMask = + NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | + NSResizableWindowMask | NSTexturedBackgroundWindowMask; + CreateNewWidgetToInit(kBorderedMask); + EXPECT_TRUE([window_ hasShadow]); // Default for non-borderless. + PerformInit(); + EXPECT_FALSE([window_ hasShadow]); // SHADOW_TYPE_NONE removes shadow. + + init_params_.shadow_type = Widget::InitParams::SHADOW_TYPE_DEFAULT; + CreateNewWidgetToInit(kBorderedMask); + PerformInit(); + EXPECT_TRUE([window_ hasShadow]); // Preserves shadow. + + window_.reset(); + widget_.reset(); +} + // Test getting complete string using text input protocol. TEST_F(BridgedNativeWidgetTest, TextInput_GetCompleteString) { const std::string kTestString = "foo bar baz"; diff --git a/ui/views/examples/bubble_example.cc b/ui/views/examples/bubble_example.cc index 0e0b6aa..09cca52 100644 --- a/ui/views/examples/bubble_example.cc +++ b/ui/views/examples/bubble_example.cc @@ -75,10 +75,14 @@ void BubbleExample::CreateExampleView(View* container) { container->SetLayoutManager(new BoxLayout(BoxLayout::kHorizontal, 0, 0, 10)); no_shadow_ = new LabelButton(this, ASCIIToUTF16("No Shadow")); container->AddChildView(no_shadow_); + no_shadow_opaque_ = new LabelButton(this, ASCIIToUTF16("Opaque Border")); + container->AddChildView(no_shadow_opaque_); big_shadow_ = new LabelButton(this, ASCIIToUTF16("Big Shadow")); container->AddChildView(big_shadow_); small_shadow_ = new LabelButton(this, ASCIIToUTF16("Small Shadow")); container->AddChildView(small_shadow_); + no_assets_ = new LabelButton(this, ASCIIToUTF16("No Assets")); + container->AddChildView(no_assets_); align_to_edge_ = new LabelButton(this, ASCIIToUTF16("Align To Edge")); container->AddChildView(align_to_edge_); persistent_ = new LabelButton(this, ASCIIToUTF16("Persistent")); @@ -100,10 +104,14 @@ void BubbleExample::ButtonPressed(Button* sender, const ui::Event& event) { if (sender == no_shadow_) bubble->set_shadow(BubbleBorder::NO_SHADOW); + else if (sender == no_shadow_opaque_) + bubble->set_shadow(BubbleBorder::NO_SHADOW_OPAQUE_BORDER); else if (sender == big_shadow_) bubble->set_shadow(BubbleBorder::BIG_SHADOW); else if (sender == small_shadow_) bubble->set_shadow(BubbleBorder::SMALL_SHADOW); + else if (sender == no_assets_) + bubble->set_shadow(BubbleBorder::NO_ASSETS); if (sender == persistent_) bubble->set_close_on_deactivate(false); diff --git a/ui/views/examples/bubble_example.h b/ui/views/examples/bubble_example.h index 8ff25d7..eee0347 100644 --- a/ui/views/examples/bubble_example.h +++ b/ui/views/examples/bubble_example.h @@ -26,8 +26,10 @@ class VIEWS_EXAMPLES_EXPORT BubbleExample : public ExampleBase, void ButtonPressed(Button* sender, const ui::Event& event) override; Button* no_shadow_; + Button* no_shadow_opaque_; Button* big_shadow_; Button* small_shadow_; + Button* no_assets_; Button* align_to_edge_; Button* persistent_; diff --git a/ui/views/views.gyp b/ui/views/views.gyp index de876a8..d5fff14 100644 --- a/ui/views/views.gyp +++ b/ui/views/views.gyp @@ -893,7 +893,10 @@ 'bubble/bubble_window_targeter_unittest.cc', 'controls/native/native_view_host_unittest.cc', 'widget/window_reorderer_unittest.cc', - ] + ], + 'dependencies': [ + '../accelerated_widget_mac/accelerated_widget_mac.gyp:accelerated_widget_mac', + ], }], ], }, # target_name: views_unittests diff --git a/ui/views/widget/native_widget_mac_unittest.mm b/ui/views/widget/native_widget_mac_unittest.mm index 2f0104c..25183e6 100644 --- a/ui/views/widget/native_widget_mac_unittest.mm +++ b/ui/views/widget/native_widget_mac_unittest.mm @@ -6,6 +6,7 @@ #import <Cocoa/Cocoa.h> +#import "base/mac/foundation_util.h" #import "base/mac/scoped_nsobject.h" #import "base/mac/scoped_objc_class_swizzler.h" #include "base/run_loop.h" @@ -13,11 +14,15 @@ #include "base/strings/sys_string_conversions.h" #include "base/test/test_timeouts.h" #import "testing/gtest_mac.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkCanvas.h" #import "ui/base/cocoa/constrained_window/constrained_window_animation.h" +#import "ui/base/cocoa/window_size_constants.h" #import "ui/events/test/cocoa_test_event_utils.h" #include "ui/events/test/event_generator.h" #import "ui/gfx/mac/coordinate_conversion.h" #import "ui/views/cocoa/bridged_native_widget.h" +#import "ui/views/cocoa/native_widget_mac_nswindow.h" #include "ui/views/controls/button/label_button.h" #include "ui/views/controls/label.h" #include "ui/views/native_cursor.h" @@ -39,9 +44,67 @@ - (BOOL)_isTitleHidden; @end +// Test NSWindow that provides hooks via method overrides to verify behavior. +@interface NativeWidetMacTestWindow : NativeWidgetMacNSWindow { + @private + int invalidateShadowCount_; +} +@property(readonly, nonatomic) int invalidateShadowCount; +@end + namespace views { namespace test { +// BridgedNativeWidget friend to access private members. +class BridgedNativeWidgetTestApi { + public: + explicit BridgedNativeWidgetTestApi(NSWindow* window) { + bridge_ = NativeWidgetMac::GetBridgeForNativeWindow(window); + } + + // Simulate a frame swap from the compositor. Assumes scale factor of 1.0f. + void SimulateFrameSwap(const gfx::Size& size) { + const float kScaleFactor = 1.0f; + SkBitmap bitmap; + bitmap.allocN32Pixels(size.width(), size.height()); + SkCanvas canvas(bitmap); + bridge_->compositor_widget_->GotSoftwareFrame(kScaleFactor, &canvas); + std::vector<ui::LatencyInfo> latency_info; + bridge_->AcceleratedWidgetSwapCompleted(latency_info); + } + + private: + BridgedNativeWidget* bridge_; + + DISALLOW_COPY_AND_ASSIGN(BridgedNativeWidgetTestApi); +}; + +// Custom native_widget to create a NativeWidgetMacTestWindow. +class TestWindowNativeWidgetMac : public NativeWidgetMac { + public: + explicit TestWindowNativeWidgetMac(Widget* delegate) + : NativeWidgetMac(delegate) {} + + protected: + // NativeWidgetMac: + gfx::NativeWindow CreateNSWindow(const Widget::InitParams& params) override { + NSUInteger style_mask = NSBorderlessWindowMask; + if (params.type == Widget::InitParams::TYPE_WINDOW) { + style_mask = NSTexturedBackgroundWindowMask | NSTitledWindowMask | + NSClosableWindowMask | NSMiniaturizableWindowMask | + NSResizableWindowMask; + } + return [[[NativeWidetMacTestWindow alloc] + initWithContentRect:ui::kWindowSizeDeterminedLater + styleMask:style_mask + backing:NSBackingStoreBuffered + defer:NO] autorelease]; + } + + private: + DISALLOW_COPY_AND_ASSIGN(TestWindowNativeWidgetMac); +}; + // Tests for parts of NativeWidgetMac not covered by BridgedNativeWidget, which // need access to Cocoa APIs. class NativeWidgetMacTest : public WidgetTest { @@ -63,6 +126,19 @@ class NativeWidgetMacTest : public WidgetTest { return native_parent_; } + // Create a Widget backed by the NativeWidetMacTestWindow NSWindow subclass. + Widget* CreateWidgetWithTestWindow(Widget::InitParams params, + NativeWidetMacTestWindow** window) { + Widget* widget = new Widget; + params.native_widget = new TestWindowNativeWidgetMac(widget); + widget->Init(params); + widget->Show(); + *window = base::mac::ObjCCastStrict<NativeWidetMacTestWindow>( + widget->GetNativeWindow()); + EXPECT_TRUE(*window); + return widget; + } + private: base::scoped_nsobject<NSWindow> native_parent_; @@ -796,6 +872,47 @@ TEST_F(NativeWidgetMacTest, DoesHideTitle) { widget->CloseNow(); } +// Test calls to invalidate the shadow when composited frames arrive. +TEST_F(NativeWidgetMacTest, InvalidateShadow) { + NativeWidetMacTestWindow* window; + const gfx::Rect rect(0, 0, 100, 200); + Widget::InitParams init_params = + CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS); + init_params.bounds = rect; + Widget* widget = CreateWidgetWithTestWindow(init_params, &window); + + // Simulate the initial paint. + BridgedNativeWidgetTestApi(window).SimulateFrameSwap(rect.size()); + + // Default is an opaque window, so shadow doesn't need to be invalidated. + EXPECT_EQ(0, [window invalidateShadowCount]); + widget->CloseNow(); + + init_params.opacity = Widget::InitParams::TRANSLUCENT_WINDOW; + widget = CreateWidgetWithTestWindow(init_params, &window); + BridgedNativeWidgetTestApi test_api(window); + + // First paint on a translucent window needs to invalidate the shadow. Once. + EXPECT_EQ(0, [window invalidateShadowCount]); + test_api.SimulateFrameSwap(rect.size()); + EXPECT_EQ(1, [window invalidateShadowCount]); + test_api.SimulateFrameSwap(rect.size()); + EXPECT_EQ(1, [window invalidateShadowCount]); + + // Resizing the window also needs to trigger a shadow invalidation. + [window setContentSize:NSMakeSize(123, 456)]; + // A "late" frame swap at the old size should do nothing. + test_api.SimulateFrameSwap(rect.size()); + EXPECT_EQ(1, [window invalidateShadowCount]); + + test_api.SimulateFrameSwap(gfx::Size(123, 456)); + EXPECT_EQ(2, [window invalidateShadowCount]); + test_api.SimulateFrameSwap(gfx::Size(123, 456)); + EXPECT_EQ(2, [window invalidateShadowCount]); + + widget->CloseNow(); +} + } // namespace test } // namespace views @@ -804,3 +921,14 @@ TEST_F(NativeWidgetMacTest, DoesHideTitle) { views::test::ScopedSwizzleWaiter::GetMethodAndMarkCalled()(self, _cmd); } @end + +@implementation NativeWidetMacTestWindow + +@synthesize invalidateShadowCount = invalidateShadowCount_; + +- (void)invalidateShadow { + ++invalidateShadowCount_; + [super invalidateShadow]; +} + +@end diff --git a/ui/views/window/dialog_delegate.cc b/ui/views/window/dialog_delegate.cc index b9f5503..ce4cced 100644 --- a/ui/views/window/dialog_delegate.cc +++ b/ui/views/window/dialog_delegate.cc @@ -61,8 +61,11 @@ Widget* DialogDelegate::CreateDialogWidgetWithBounds(WidgetDelegate* delegate, if (!dialog || dialog->UseNewStyleForThisDialog()) { params.opacity = Widget::InitParams::TRANSLUCENT_WINDOW; params.remove_standard_frame = true; - // The bubble frame includes its own shadow; remove any native shadowing. +#if !defined(OS_MACOSX) + // Except on Mac, the bubble frame includes its own shadow; remove any + // native shadowing. On Mac, the window server provides the shadow. params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE; +#endif } params.context = context; params.parent = parent; @@ -189,8 +192,14 @@ NonClientFrameView* DialogDelegate::CreateNonClientFrameView(Widget* widget) { // static NonClientFrameView* DialogDelegate::CreateDialogFrameView(Widget* widget) { BubbleFrameView* frame = new BubbleFrameView(gfx::Insets()); - scoped_ptr<BubbleBorder> border(new BubbleBorder( - BubbleBorder::FLOAT, BubbleBorder::SMALL_SHADOW, SK_ColorRED)); +#if defined(OS_MACOSX) + // On Mac, dialogs have no border stroke and use a shadow provided by the OS. + const BubbleBorder::Shadow kShadow = BubbleBorder::NO_ASSETS; +#else + const BubbleBorder::Shadow kShadow = BubbleBorder::SMALL_SHADOW; +#endif + scoped_ptr<BubbleBorder> border( + new BubbleBorder(BubbleBorder::FLOAT, kShadow, SK_ColorRED)); border->set_use_theme_background_color(true); frame->SetBubbleBorder(border.Pass()); DialogDelegate* delegate = widget->widget_delegate()->AsDialogDelegate(); |