chromium/ash/wm/splitview/split_view_highlight_view.cc

// Copyright 2018 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/splitview/split_view_highlight_view.h"

#include "ash/display/screen_orientation_controller.h"
#include "ash/shell.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_types.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/views/background.h"
#include "ui/views/highlight_border.h"
#include "ui/views/view_observer.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

// The amount of round applied to the corners of the highlight views.
constexpr int kHighlightScreenRoundRectRadius = 12;

// Self deleting animation observer that removes clipping on View's layer and
// optionally sets bounds after the animation ends.
class ClippingObserver : public ui::ImplicitAnimationObserver,
                         public views::ViewObserver {
 public:
  ClippingObserver(views::View* view, std::optional<gfx::Rect> bounds)
      : view_(view), bounds_(bounds) {
    view_->AddObserver(this);
  }
  ~ClippingObserver() override { view_->RemoveObserver(this); }

  // ui::ImplicitAnimationObserver:
  void OnImplicitAnimationsCompleted() override {
    view_->layer()->SetClipRect(gfx::Rect());
    if (bounds_)
      view_->SetBoundsRect(*bounds_);
    delete this;
  }

  // views::ViewObserver:
  void OnViewIsDeleting(views::View* observed_view) override {
    DCHECK_EQ(view_, observed_view);
    delete this;
  }

 private:
  const raw_ptr<views::View> view_;
  std::optional<gfx::Rect> bounds_;
};

}  // namespace

SplitViewHighlightView::SplitViewHighlightView(bool is_right_or_bottom)
    : is_right_or_bottom_(is_right_or_bottom) {
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  SetBackground(views::CreateThemedRoundedRectBackground(
      cros_tokens::kCrosSysPrimary, kHighlightScreenRoundRectRadius));

  SetBorder(std::make_unique<views::HighlightBorder>(
      kHighlightScreenRoundRectRadius,
      views::HighlightBorder::Type::kHighlightBorderNoShadow));
}

SplitViewHighlightView::~SplitViewHighlightView() = default;

void SplitViewHighlightView::SetBounds(
    const gfx::Rect& bounds,
    const std::optional<SplitviewAnimationType>& animation_type) {
  if (bounds == this->bounds())
    return;

  if (!animation_type) {
    SetBoundsRect(bounds);
    return;
  }

  const gfx::Rect old_bounds = this->bounds();
  // Note: This is passed on the assumption that the highlights either.
  // 1) Slide out - x or y increases and other dimension stays the same.
  // 2) Slide in - x or y decreases and other dimension stays the same.
  // 3) Expands(Nix inset) - x and y both increase by a small amount.
  const bool grows = bounds.size().GetArea() > old_bounds.size().GetArea();

  // If the highlight grows, set the final bounds and clip the rect to the
  // current bounds and animate. Otherwise, start the clip animation and set the
  // bounds after the animation is complete.
  if (grows)
    SetBoundsRect(bounds);

  // The origin of the clip rect needs to be shifted depending on whether we are
  // growing or shrinking for right/bottom views since their animations are
  // mirrored.
  gfx::Point start_origin, end_origin;
  const bool nix_animation =
      *animation_type == SPLITVIEW_ANIMATION_PREVIEW_AREA_NIX_INSET;
  if (is_right_or_bottom_ || nix_animation) {
    gfx::Vector2d clip_offset = bounds.origin() - old_bounds.origin();
    // Make sure a widget exists because the test might not add it.
    DCHECK(GetWidget());
    // RTL is a special case since for the right highlight we will receive a
    // mirrored rect whose origin will not change. In this case the clip rect
    // offset should be the change in width. Portrait mode does not care since
    // it is unaffected by RTL and the nix inset animation will supply the
    // current bounds offset.
    if (base::i18n::IsRTL() &&
        IsLayoutHorizontal(GetWidget()->GetNativeWindow()) && !nix_animation) {
      clip_offset = gfx::Vector2d(bounds.width() - old_bounds.width(), 0);
    }

    clip_offset.set_x(std::abs(clip_offset.x()));
    clip_offset.set_y(std::abs(clip_offset.y()));
    if (grows)
      start_origin += clip_offset;
    else
      end_origin += clip_offset;
  }

  layer()->SetClipRect(gfx::Rect(start_origin, old_bounds.size()));
  DoSplitviewClipRectAnimation(
      layer(), *animation_type, gfx::Rect(end_origin, bounds.size()),
      std::make_unique<ClippingObserver>(
          this, grows ? std::nullopt : std::make_optional(bounds)));
}

void SplitViewHighlightView::OnWindowDraggingStateChanged(
    SplitViewDragIndicators::WindowDraggingState window_dragging_state,
    SplitViewDragIndicators::WindowDraggingState previous_window_dragging_state,
    bool previews_only,
    bool can_dragged_window_be_snapped) {
  // No top indicator for dragging from the top in portrait orientation.
  if (window_dragging_state ==
          SplitViewDragIndicators::WindowDraggingState::kFromTop &&
      !IsCurrentScreenOrientationLandscape() && !is_right_or_bottom_) {
    return;
  }

  if (window_dragging_state ==
      SplitViewDragIndicators::WindowDraggingState::kOtherDisplay) {
    DoSplitviewOpacityAnimation(layer(),
                                SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT);
    return;
  }

  const SnapPosition preview_position =
      SplitViewDragIndicators::GetSnapPosition(window_dragging_state);
  const SnapPosition previous_preview_position =
      SplitViewDragIndicators::GetSnapPosition(previous_window_dragging_state);

  aura::Window* window = GetWidget()->GetNativeWindow();

  if (window_dragging_state ==
      SplitViewDragIndicators::WindowDraggingState::kNoDrag) {
    if (previous_preview_position == SnapPosition::kNone) {
      DoSplitviewOpacityAnimation(layer(),
                                  SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT);
      return;
    }
    if (is_right_or_bottom_ !=
        IsPhysicallyLeftOrTop(previous_preview_position, window)) {
      DoSplitviewOpacityAnimation(layer(),
                                  SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_OUT);
    }
    return;
  }

  if (preview_position != SnapPosition::kNone) {
    DoSplitviewOpacityAnimation(
        layer(),
        is_right_or_bottom_ != IsPhysicallyLeftOrTop(preview_position, window)
            ? SPLITVIEW_ANIMATION_PREVIEW_AREA_FADE_IN
            : SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_OUT);
    return;
  }

  if (previous_preview_position != SnapPosition::kNone) {
    // There was a snap preview showing, but now the user has dragged away from
    // the edge of the screen, so that the preview should go away.
    if (is_right_or_bottom_ !=
        IsPhysicallyLeftOrTop(previous_preview_position, window)) {
      // This code is for the preview. If |previews_only|, just fade out. Else
      // fade in from |kPreviewAreaHighlightOpacity| to |kHighlightOpacity|.
      DoSplitviewOpacityAnimation(
          layer(),
          previews_only
              ? SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_OUT
              : can_dragged_window_be_snapped
                    ? SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN
                    : SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN_CANNOT_SNAP);
    } else {
      // This code is for the other highlight. If |previews_only|, just stay
      // hidden (in other words, do nothing). Else fade in.
      DCHECK_EQ(0.f, layer()->GetTargetOpacity());
      if (!previews_only) {
        DoSplitviewOpacityAnimation(
            layer(),
            can_dragged_window_be_snapped
                ? SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN
                : SPLITVIEW_ANIMATION_OTHER_HIGHLIGHT_FADE_IN_CANNOT_SNAP);
      }
    }
    return;
  }

  // The drag just started or came in from another display, and is not currently
  // in a snap area. If |previews_only|, there is nothing to do. Else fade in.
  DCHECK_EQ(0.f, layer()->GetTargetOpacity());
  if (!previews_only) {
    DoSplitviewOpacityAnimation(
        layer(), can_dragged_window_be_snapped
                     ? SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN
                     : SPLITVIEW_ANIMATION_HIGHLIGHT_FADE_IN_CANNOT_SNAP);
    return;
  }
}

BEGIN_METADATA(SplitViewHighlightView)
END_METADATA

}  // namespace ash