chromium/ash/wm/resize_shadow.cc

// Copyright 2012 The Chromium Authors
// 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 <map>

#include "ash/root_window_controller.h"
#include "base/no_destructor.h"
#include "base/time/time.h"
#include "ui/aura/window.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_source.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/image/canvas_image_source.h"

namespace {

// The features that uniquely described a shadow's appearance.
struct ShadowFeaturesKey {
  bool operator==(const ShadowFeaturesKey& other) const {
    return MakeTuple() == other.MakeTuple();
  }
  bool operator<(const ShadowFeaturesKey& other) const {
    return MakeTuple() < other.MakeTuple();
  }

  std::tuple<int, int, SkColor> MakeTuple() const {
    return std::make_tuple(width, corner_radius, color);
  }

  // The total width of the shadow region.
  int width = 0;
  int corner_radius = 0;
  SkColor color = gfx::kPlaceholderColor;
};

// Get shadow image size with given init params.
int ShadowImageSize(const ash::ResizeShadow::InitParams& params) {
  //  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.
  return 2 * (params.thickness + params.window_corner_radius) + 1;
}

// This class simply draws a roundrect. The layout and tiling is handled by
// ResizeShadow and NinePatchLayer.
class ResizeShadowImageSource : public gfx::CanvasImageSource {
 public:
  explicit ResizeShadowImageSource(const ShadowFeaturesKey& features)
      : gfx::CanvasImageSource(gfx::Size(features.width, features.width)),
        shadow_corner_radius_(features.corner_radius),
        color_(features.color) {}
  ResizeShadowImageSource(const ResizeShadowImageSource&) = delete;
  ResizeShadowImageSource& operator=(const ResizeShadowImageSource&) = delete;
  ~ResizeShadowImageSource() override = default;

  // gfx::CanvasImageSource:
  void Draw(gfx::Canvas* canvas) override {
    cc::PaintFlags paint;
    paint.setAntiAlias(true);
    paint.setColor(color_);
    canvas->DrawRoundRect(gfx::RectF(gfx::SizeF(size())), shadow_corner_radius_,
                          paint);
  }

  int shadow_corner_radius_;
  SkColor color_;
};

// Calculate outsets of the |window_| based on a |hit_test| code and |thickness|
const gfx::Insets CalculateOutsets(int hit, int thickness) {
  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 = -thickness;
  return gfx::Insets::TLBR(show_top ? outset : 0, show_left ? outset : 0,
                           show_bottom ? outset : 0, show_right ? outset : 0);
}

// static
const gfx::ImageSkia& MakeShadowImageOnce(
    const ash::ResizeShadow::InitParams& params,
    const ui::ColorProvider* color_provider) {
  // Resolve the color with color type. If given a color ID, use color provider
  // to get the color value.
  SkColor color;
  if (absl::holds_alternative<SkColor>(params.color)) {
    color = absl::get<SkColor>(params.color);
  } else {
    CHECK(!!color_provider);
    color = color_provider->GetColor(absl::get<ui::ColorId>(params.color));
  }

  // Generate the shadow features key.
  const ShadowFeaturesKey features_key{ShadowImageSize(params),
                                       params.shadow_corner_radius, color};

  // Create a cache saving the shadow textures for each shadow features key.
  static base::NoDestructor<std::map<ShadowFeaturesKey, gfx::ImageSkia>>
      shadow_image_cache;
  auto iter = shadow_image_cache->find(features_key);

  // If requiring a new shadow texture, evict unused textures and create a new
  // one with given shadow features.
  if (iter == shadow_image_cache->end()) {
    std::erase_if(*shadow_image_cache, [](auto& key_and_image_source) {
      return key_and_image_source.second.IsUniquelyOwned();
    });

    iter =
        shadow_image_cache
            ->emplace(
                features_key,
                gfx::CanvasImageSource::MakeImageSkia<ResizeShadowImageSource>(
                    features_key))
            .first;
  }

  return iter->second;
}

}  // namespace

namespace ash {

ResizeShadow::ResizeShadow(aura::Window* window,
                           const InitParams& params,
                           ResizeShadowType type)
    : window_(window), params_(params), type_(type) {
  // Use a NinePatchLayer to tile the shadow image (which is simply a
  // roundrect).
  layer_ = std::make_unique<ui::Layer>(ui::LAYER_NINE_PATCH);
  layer_->SetName("WindowResizeShadow");
  layer_->SetFillsBoundsOpaquely(false);
  layer_->SetOpacity(0.f);
  layer_->SetVisible(false);
  // If use static color, create the shadow image. Otherwise, observe the color
  // provider source to update the shadow color.
  if (absl::holds_alternative<SkColor>(params.color)) {
    UpdateShadowLayer();
  } else {
    Observe(RootWindowController::ForWindow(window)->color_provider_source());
  }

  const gfx::Insets aperture_insets(params_.thickness +
                                    params_.window_corner_radius);
  const int image_size = ShadowImageSize(params_);
  gfx::Rect aperture(gfx::Size(image_size, image_size));
  aperture.Inset(aperture_insets);
  layer_->UpdateNinePatchLayerAperture(aperture);
  layer_->UpdateNinePatchLayerBorder(
      gfx::Rect(aperture_insets.left(), aperture_insets.top(),
                aperture_insets.width(), aperture_insets.height()));

  ReparentLayer();
}

ResizeShadow::~ResizeShadow() = default;

void ResizeShadow::OnColorProviderChanged() {
  // This function will also be called when the color provider source is
  // destroyed. We should guarantee the color provider exists.
  if (absl::holds_alternative<ui::ColorId>(params_.color) &&
      GetColorProviderSource()) {
    UpdateShadowLayer();
  }
}

void ResizeShadow::OnWindowParentToRootWindow() {
  if (absl::holds_alternative<ui::ColorId>(params_.color)) {
    Observe(RootWindowController::ForWindow(window_)->color_provider_source());
  }
}

void ResizeShadow::UpdateShadowLayer() {
  auto* color_provider_source = GetColorProviderSource();
  const auto& shadow_image = MakeShadowImageOnce(
      params_, color_provider_source ? color_provider_source->GetColorProvider()
                                     : nullptr);
  layer_->UpdateNinePatchLayerImage(shadow_image);
}

void ResizeShadow::ShowForHitTest(int hit) {
  UpdateHitTest(hit);
  visible_ = true;
  UpdateBoundsAndVisibility();
}

void ResizeShadow::Hide() {
  UpdateHitTest(HTNOWHERE);
  visible_ = false;
  UpdateBoundsAndVisibility();
}

void ResizeShadow::UpdateHitTest(int hit) {
  // Don't start animations unless something changed.
  if (hit == last_hit_test_)
    return;
  last_hit_test_ = hit;
}

void ResizeShadow::UpdateBoundsAndVisibility() {
  UpdateBounds(window_->bounds());
}

void ResizeShadow::UpdateBounds(const gfx::Rect& window_bounds) {
  // 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 gfx::Insets outsets =
      params_.hit_test_enabled
          ? CalculateOutsets(last_hit_test_, params_.thickness)
          : gfx::Insets(-params_.thickness);

  if (outsets.IsEmpty() && !layer_->GetTargetVisibility())
    return;

  visible_ &= !outsets.IsEmpty();
  if (visible_) {
    gfx::Rect bounds = window_bounds;
    bounds.Inset(outsets);
    layer_->SetBounds(bounds);
  }

  ui::ScopedLayerAnimationSettings settings(layer_->GetAnimator());
  if (!visible_)
    settings.SetTransitionDuration(
        base::Milliseconds(params_.hide_duration_ms));
  layer_->SetOpacity(visible_ ? params_.opacity : 0.f);
  layer_->SetVisible(visible_);
}

void ResizeShadow::ReparentLayer() {
  // This shadow could belong to a window that has been removed from its parent.
  // In that case, we should not try to access `window_`'s parent.
  if (!window_->layer()->parent())
    return;

  if (layer_->parent() != window_->layer()->parent())
    window_->layer()->parent()->Add(layer_.get());
  layer_->parent()->StackBelow(layer_.get(), window_->layer());
}

}  // namespace ash