chromium/ash/user_education/welcome_tour/welcome_tour_scrim.cc

// Copyright 2023 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/user_education/welcome_tour/welcome_tour_scrim.h"

#include <array>
#include <vector>

#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ash/style/ash_color_provider_source.h"
#include "ash/user_education/user_education_help_bubble_controller.h"
#include "ash/user_education/user_education_types.h"
#include "base/callback_list.h"
#include "base/check.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/core/SkRect.h"
#include "ui/aura/window.h"
#include "ui/aura/window_observer.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_source_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/layer_owner.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {
namespace {

// Used to ensure existence of only a single `WelcomeTourScrim` at a time.
WelcomeTourScrim* g_instance = nullptr;

// Appearance.
constexpr float kBlurSigma = 3.f;
constexpr float kClipCornerRadius = 24.f;

// Helpers ---------------------------------------------------------------------

gfx::RectF TranslateRectFromScreen(const aura::Window* root_window,
                                   const gfx::Rect& rect_in_screen) {
  gfx::RectF rect_in_root_window(rect_in_screen);
  ::wm::TranslateRectFromScreen(root_window, &rect_in_root_window);
  return rect_in_root_window;
}

std::vector<gfx::RectF> GetHelpBubbleAnchorBoundsInRootWindow(
    const aura::Window* root_window) {
  std::vector<gfx::RectF> bounds_in_root_window;
  if (auto* ctrl = UserEducationHelpBubbleController::Get()) {
    for (const auto& [_, metadata] : ctrl->help_bubble_metadata_by_key()) {
      if (metadata.anchor_root_window == root_window) {
        bounds_in_root_window.emplace_back(TranslateRectFromScreen(
            metadata.anchor_root_window, metadata.anchor_bounds_in_screen));
      }
    }
  }
  return bounds_in_root_window;
}

// MaskLayerOwner --------------------------------------------------------------

// The class which owns the mask layer for a `WelcomeTour::Scrim`. The mask
// layer is responsible for clipping the scrim around help bubble anchor views
// so that they are emphasized by the scrim and not obstructed by it.
class MaskLayerOwner : public ui::LayerOwner, public ui::LayerDelegate {
 public:
  explicit MaskLayerOwner(const aura::Window* root_window)
      : ui::LayerOwner(std::make_unique<ui::Layer>(ui::LAYER_TEXTURED)),
        root_window_(root_window) {
    Init();
  }

  MaskLayerOwner(const MaskLayerOwner&) = delete;
  MaskLayerOwner& operator=(const MaskLayerOwner&) = delete;
  ~MaskLayerOwner() override = default;

 private:
  // ui::LayerDelegate:
  void OnDeviceScaleFactorChanged(float old_device_scale_factory,
                                  float new_device_scale_factor) override {
    Invalidate();
  }

  void OnPaintLayer(const ui::PaintContext& context) override {
    // In the absence of help bubble anchor views, the scrim should be fully
    // visible. As such, the mask layer for the scrim should be fully opaque.
    gfx::SizeF size(layer()->size());
    SkPath path(SkPath::Rect(gfx::RectFToSkRect(gfx::RectF(size)),
                             SkPathDirection::kCW));

    // Clip the otherwise fully opaque mask layer around help bubble anchor
    // views so that they are emphasized by the scrim and not obstructed by it.
    for (const auto& bounds :
         GetHelpBubbleAnchorBoundsInRootWindow(root_window_)) {
      path.addRRect(
          SkRRect(gfx::RRectF(bounds, gfx::RoundedCornersF(kClipCornerRadius))),
          SkPathDirection::kCCW);
    }

    // Configure `canvas`.
    ui::PaintRecorder recorder(context, gfx::ToFlooredSize(size));
    gfx::Canvas* const canvas = recorder.canvas();

    // Configure `flags`.
    cc::PaintFlags flags;
    flags.setAntiAlias(true);
    flags.setColor(SK_ColorBLACK);
    flags.setStyle(cc::PaintFlags::kFill_Style);

    // Draw `path`.
    canvas->DrawPath(path, flags);
  }

  // Invoked once to initialize `this`.
  void Init() {
    // Configure static mask layer properties.
    layer()->SetFillsBoundsOpaquely(false);
    layer()->SetName(WelcomeTourScrim::kMaskLayerName);
    layer()->set_delegate(this);

    // Invalidate the mask layer in response to help bubble related events in
    // order to clip the scrim around help bubble anchor views so that they are
    // emphasized by the scrim and not obstructed by it.
    if (auto* controller = UserEducationHelpBubbleController::Get()) {
      base::RepeatingCallback invalidate = base::BindRepeating(
          &MaskLayerOwner::Invalidate, base::Unretained(this));
      help_bubble_event_subscriptions_[0] =
          controller->AddHelpBubbleAnchorBoundsChangedCallback(invalidate);
      help_bubble_event_subscriptions_[1] =
          controller->AddHelpBubbleClosedCallback(invalidate);
      help_bubble_event_subscriptions_[2] =
          controller->AddHelpBubbleShownCallback(invalidate);
    }
  }

  // Invoked as needed to schedule repaint of the mask layer.
  void Invalidate() { layer()->SchedulePaint(gfx::Rect(layer()->size())); }

  // Pointer to the root window associated with `this` mask.
  const raw_ptr<const aura::Window> root_window_;

  // Subscriptions for help bubble related events which are held for the life of
  // `this` object in order to clip the scrim around help bubble anchor views so
  // that they are emphasized by the scrim and not obstructed by it.
  std::array<base::CallbackListSubscription, 3>
      help_bubble_event_subscriptions_;
};

}  // namespace

// WelcomeTourScrim::Scrim -----------------------------------------------------

// The class which applies a scrim to the help bubble container for a single
// root window while in existence. On destruction, the scrim for the associated
// root window is automatically removed.
class WelcomeTourScrim::Scrim : public aura::WindowObserver,
                                public ui::ColorProviderSourceObserver {
 public:
  explicit Scrim(aura::Window* root_window)
      : root_window_(root_window),
        layer_owner_(std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR)),
        mask_layer_owner_(root_window) {
    Init();
  }

  Scrim(const Scrim&) = delete;
  Scrim& operator=(const Scrim&) = delete;
  ~Scrim() override = default;

 private:
  // aura::WindowObserver:
  void OnWindowBoundsChanged(aura::Window* window,
                             const gfx::Rect& old_bounds,
                             const gfx::Rect& new_bounds,
                             ui::PropertyChangeReason reason) override {
    UpdateBounds();
  }

  void OnWindowDestroying(aura::Window* window) override {
    window_observation_.Reset();
  }

  // ui::ColorProviderSourceObserver:
  void OnColorProviderChanged() override { UpdateColor(); }

  // Returns the help bubble container associated with `this` scrim.
  aura::Window* GetHelpBubbleContainer() {
    return root_window_->GetChildById(kShellWindowId_HelpBubbleContainer);
  }

  // Returns the root window controller associated with `this` scrim.
  RootWindowController* GetRootWindowController() {
    return RootWindowController::ForWindow(root_window_);
  }

  // Invoked once to initialize `this` scrim.
  void Init() {
    // Configure static scrim layer properties.
    layer_owner_.layer()->SetFillsBoundsOpaquely(false);
    layer_owner_.layer()->SetMaskLayer(mask_layer_owner_.layer());
    layer_owner_.layer()->SetName(WelcomeTourScrim::kLayerName);

    // Configure dynamic scrim layer properties.
    UpdateBlur();
    UpdateBounds();
    UpdateColor();

    // Add the scrim layer to the bottom of the `help_bubble_container`.
    aura::Window* const help_bubble_container = GetHelpBubbleContainer();
    help_bubble_container->layer()->Add(layer_owner_.layer());
    help_bubble_container->layer()->StackAtBottom(layer_owner_.layer());

    // Update blur in response to help bubble related events in order to apply
    // blur behind the scrim layer if and only if help bubbles are present.
    if (auto* controller = UserEducationHelpBubbleController::Get()) {
      base::RepeatingCallback update_blur = base::BindRepeating(
          &WelcomeTourScrim::Scrim::UpdateBlur, base::Unretained(this));
      help_bubble_event_subscriptions_[0] =
          controller->AddHelpBubbleClosedCallback(update_blur);
      help_bubble_event_subscriptions_[1] =
          controller->AddHelpBubbleShownCallback(update_blur);
    }

    // Observe the `help_bubble_container` and associated color provider source
    // so that dynamic scrim layer properties can be updated appropriately.
    window_observation_.Observe(help_bubble_container);
    Observe(GetRootWindowController()->color_provider_source());
  }

  // Invoked to update blur applied behind the scrim layer. Note that blur is
  // applied if and only if help bubbles are present.
  void UpdateBlur() {
    if (auto* controller = UserEducationHelpBubbleController::Get()) {
      layer_owner_.layer()->SetBackgroundBlur(
          controller->help_bubble_metadata_by_key().empty() ? 0.f : kBlurSigma);
    }
  }

  // Invoked to update bounds of the scrim and mask layers. Note that scrim and
  // mask layer bounds must remain in sync.
  void UpdateBounds() {
    const gfx::Rect bounds(GetHelpBubbleContainer()->bounds().size());
    layer_owner_.layer()->SetBounds(bounds);
    mask_layer_owner_.layer()->SetBounds(bounds);
  }

  // Invoked to update color of the scrim layer.
  void UpdateColor() {
    layer_owner_.layer()->SetColor(GetRootWindowController()
                                       ->color_provider_source()
                                       ->GetColorProvider()
                                       ->GetColor(cros_tokens::kCrosSysScrim));
  }

  // Pointer to the root window associated with `this` scrim.
  const raw_ptr<aura::Window> root_window_;

  // Owner for the scrim layer applied to the associated help bubble container.
  ui::LayerOwner layer_owner_;

  // Owner for the mask layer which is applied to the scrim layer to clip the
  // scrim around help bubble anchor views so that they are emphasized by the
  // scrim and not obstructed by it.
  MaskLayerOwner mask_layer_owner_;

  // Subscriptions for help bubble related events which are held for the life of
  // `this` object in order to apply blur behind the scrim layer if and only if
  // help bubbles are present.
  std::array<base::CallbackListSubscription, 2>
      help_bubble_event_subscriptions_;

  // Used to observe the associated help bubble container in order to keep the
  // bounds of the scrim layer in sync.
  base::ScopedObservation<aura::Window, aura::WindowObserver>
      window_observation_{this};
};

// WelcomeTourScrim ------------------------------------------------------------

WelcomeTourScrim::WelcomeTourScrim() {
  CHECK_EQ(g_instance, nullptr);
  g_instance = this;

  // Cache `shell` and associated window tree host manager.
  auto* shell = Shell::Get();
  CHECK(shell);
  auto* window_tree_host_mgr = shell->window_tree_host_manager();
  CHECK(window_tree_host_mgr);

  // Create a scrim for every root window.
  for (aura::Window* root_window : window_tree_host_mgr->GetAllRootWindows()) {
    Init(root_window);
  }

  // Observe `shell` so that scrims can be dynamically created/destroyed when
  // root windows are added/removed.
  shell_observation_.Observe(shell);

  // Observe the window tree host manager so that scrims can be destroyed when
  // the window tree host manager is shutdown.
  window_tree_host_manager_observation_.Observe(window_tree_host_mgr);
}

WelcomeTourScrim::~WelcomeTourScrim() {
  CHECK_EQ(g_instance, this);
  g_instance = nullptr;
}

void WelcomeTourScrim::OnRootWindowAdded(aura::Window* root_window) {
  Init(root_window);
}

void WelcomeTourScrim::OnRootWindowWillShutdown(aura::Window* root_window) {
  Reset(root_window);
}

void WelcomeTourScrim::OnWindowTreeHostManagerShutdown() {
  // Cache `shell` and associated window tree host manager.
  auto* shell = Shell::Get();
  CHECK(shell);
  auto* window_tree_host_mgr = shell->window_tree_host_manager();
  CHECK(window_tree_host_mgr);

  // Reset observation.
  CHECK(window_tree_host_manager_observation_.IsObservingSource(
      window_tree_host_mgr));
  window_tree_host_manager_observation_.Reset();

  // Destroy scrims for every root window.
  for (aura::Window* root_window : window_tree_host_mgr->GetAllRootWindows()) {
    Reset(root_window);
  }
}

void WelcomeTourScrim::Init(aura::Window* root_window) {
  auto [it, inserted] = scrims_by_root_window_.emplace(
      root_window, std::make_unique<Scrim>(root_window));
  CHECK(inserted);
}

void WelcomeTourScrim::Reset(aura::Window* root_window) {
  scrims_by_root_window_.erase(root_window);
}

}  // namespace ash