| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/wm/resize_shadow.h" |
| |
| #include <memory> |
| |
| #include "base/lazy_instance.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/no_destructor.h" |
| #include "base/time/time.h" |
| #include "ui/aura/window.h" |
| #include "ui/base/hit_test.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/scoped_layer_animation_settings.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/image/canvas_image_source.h" |
| |
| namespace { |
| |
| // The width of the resize shadow that appears on the hovered edge of the |
| // window. |
| constexpr int kVisualThickness = 8; |
| |
| // The corner radius of the resize shadow, which not coincidentally matches |
| // the corner radius of the actual window. |
| static constexpr int kCornerRadiusOfResizeShadow = 2; |
| static constexpr int kCornerRadiusOfWindow = 2; |
| |
| // This class simply draws a roundrect. The layout and tiling is handled by |
| // ResizeShadow and NinePatchLayer. |
| class ResizeShadowImageSource : public gfx::CanvasImageSource { |
| public: |
| ResizeShadowImageSource() |
| : gfx::CanvasImageSource(gfx::Size(kImageSide, kImageSide), |
| false /* is opaque */) {} |
| |
| ~ResizeShadowImageSource() override = default; |
| |
| // gfx::CanvasImageSource: |
| void Draw(gfx::Canvas* canvas) override { |
| cc::PaintFlags paint; |
| paint.setAntiAlias(true); |
| paint.setColor(SK_ColorBLACK); |
| canvas->DrawRoundRect(gfx::RectF(gfx::SizeF(size())), |
| kCornerRadiusOfResizeShadow, paint); |
| } |
| |
| private: |
| // The image has to have enough space to depict the visual thickness (left and |
| // right) plus an inset for extending beneath the window's rounded corner plus |
| // one pixel for the center of the nine patch. |
| static constexpr int kImageSide = |
| 2 * (kVisualThickness + kCornerRadiusOfWindow) + 1; |
| |
| DISALLOW_COPY_AND_ASSIGN(ResizeShadowImageSource); |
| }; |
| |
| } // namespace |
| |
| namespace ash { |
| |
| ResizeShadow::ResizeShadow(aura::Window* window) |
| : window_(window), last_hit_test_(HTNOWHERE) { |
| window_->AddObserver(this); |
| |
| // Use a NinePatchLayer to tile the shadow image (which is simply a |
| // roundrect). |
| layer_.reset(new ui::Layer(ui::LAYER_NINE_PATCH)); |
| layer_->set_name("WindowResizeShadow"); |
| layer_->SetFillsBoundsOpaquely(false); |
| layer_->SetOpacity(0.f); |
| layer_->SetVisible(false); |
| |
| static base::NoDestructor<gfx::ImageSkia> shadow_image; |
| |
| if (shadow_image->isNull()) { |
| auto* source = new ResizeShadowImageSource(); |
| *shadow_image = gfx::ImageSkia(base::WrapUnique(source), source->size()); |
| } |
| layer_->UpdateNinePatchLayerImage(*shadow_image); |
| gfx::Rect aperture(shadow_image->size()); |
| constexpr gfx::Insets kApertureInsets(kVisualThickness + |
| kCornerRadiusOfWindow); |
| aperture.Inset(kApertureInsets); |
| layer_->UpdateNinePatchLayerAperture(aperture); |
| layer_->UpdateNinePatchLayerBorder( |
| gfx::Rect(kApertureInsets.left(), kApertureInsets.top(), |
| kApertureInsets.width(), kApertureInsets.height())); |
| |
| ReparentLayer(); |
| } |
| |
| ResizeShadow::~ResizeShadow() { |
| window_->RemoveObserver(this); |
| } |
| |
| void ResizeShadow::OnWindowBoundsChanged(aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) { |
| UpdateBoundsAndVisibility(); |
| } |
| |
| void ResizeShadow::OnWindowHierarchyChanged( |
| const aura::WindowObserver::HierarchyChangeParams& params) { |
| ReparentLayer(); |
| } |
| |
| void ResizeShadow::OnWindowStackingChanged(aura::Window* window) { |
| ReparentLayer(); |
| } |
| |
| void ResizeShadow::ShowForHitTest(int hit) { |
| // Don't start animations unless something changed. |
| if (hit == last_hit_test_) |
| return; |
| last_hit_test_ = hit; |
| |
| UpdateBoundsAndVisibility(); |
| } |
| |
| void ResizeShadow::Hide() { |
| ShowForHitTest(HTNOWHERE); |
| } |
| |
| void ResizeShadow::ReparentLayer() { |
| DCHECK(window_->layer()->parent()); |
| if (layer_->parent() != window_->layer()->parent()) |
| window_->layer()->parent()->Add(layer_.get()); |
| layer_->parent()->StackBelow(layer_.get(), window_->layer()); |
| } |
| |
| void ResizeShadow::UpdateBoundsAndVisibility() { |
| // The shadow layer is positioned such that one or two edges will stick out |
| // from underneath |window_|. Thus |window_| occludes the rest of the |
| // roundrect. |
| const int hit = last_hit_test_; |
| bool show_top = hit == HTTOPLEFT || hit == HTTOP || hit == HTTOPRIGHT; |
| bool show_left = hit == HTTOPLEFT || hit == HTLEFT || hit == HTBOTTOMLEFT; |
| bool show_bottom = |
| hit == HTBOTTOMLEFT || hit == HTBOTTOM || hit == HTBOTTOMRIGHT; |
| bool show_right = hit == HTTOPRIGHT || hit == HTRIGHT || hit == HTBOTTOMRIGHT; |
| |
| const int outset = -kVisualThickness; |
| gfx::Insets outsets(show_top ? outset : 0, show_left ? outset : 0, |
| show_bottom ? outset : 0, show_right ? outset : 0); |
| bool visible = !outsets.IsEmpty(); |
| if (!visible && !layer_->GetTargetVisibility()) |
| return; |
| |
| if (visible) { |
| gfx::Rect bounds = window_->bounds(); |
| bounds.Inset(outsets); |
| layer_->SetBounds(bounds); |
| } |
| |
| // The resize shadow snaps in but fades out. |
| ui::ScopedLayerAnimationSettings settings(layer_->GetAnimator()); |
| if (!visible) { |
| constexpr int kShadowFadeOutDurationMs = 100; |
| settings.SetTransitionDuration( |
| base::TimeDelta::FromMilliseconds(kShadowFadeOutDurationMs)); |
| } |
| constexpr float kShadowTargetOpacity = 0.5f; |
| layer_->SetOpacity(visible ? kShadowTargetOpacity : 0.f); |
| layer_->SetVisible(visible); |
| } |
| |
| } // namespace ash |