chromium/ash/wm/window_cycle/window_cycle_item_view.cc

// Copyright 2021 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/window_cycle/window_cycle_item_view.h"

#include <algorithm>
#include <memory>

#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/wm/snap_group/snap_group.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/window_cycle/window_cycle_controller.h"
#include "ash/wm/window_cycle/window_cycle_list.h"
#include "ash/wm/window_cycle/window_cycle_view.h"
#include "ash/wm/window_mini_view_header_view.h"
#include "ash/wm/window_preview_view.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_constants.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/aura/window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/insets.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/views/accessibility/view_accessibility.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/view.h"

namespace ash {

namespace {

// Spacing between the `WindowCycleItemView`s hosted by the container view.
constexpr int kBetweenCycleItemsSpacing = 4;

// Fixed preview height to windows in portrait-oriented snap layouts.
constexpr int kFixedPreviewHeightForVerticalSnapGroupDp =
    (WindowCycleItemView::kFixedPreviewHeightDp - kWindowMiniViewHeaderHeight -
     kBetweenCycleItemsSpacing) /
    2;

// The border padding value of the container view.
constexpr auto kInsideContainerBorderInset = gfx::Insets(2);

// Returns true if the given `window` belongs to a snap group with a vertical
// split layout.
bool IsWindowInVerticalSnapGroup(const aura::Window* window) {
  if (SnapGroupController* snap_group_controller = SnapGroupController::Get()) {
    if (SnapGroup* snap_group =
            snap_group_controller->GetSnapGroupForGivenWindow(window);
        snap_group && !snap_group->IsSnapGroupLayoutHorizontal()) {
      return true;
    }
  }

  return false;
}

// Calculates fixed preview height for `window`. In vertical snap groups,
// applies `kFixedPreviewHeightForVerticalSnapGroupDp` to maintain equal height
// with other item preview views.
int GetPreviewFixedHeight(const aura::Window* window) {
  return IsWindowInVerticalSnapGroup(window)
             ? kFixedPreviewHeightForVerticalSnapGroupDp
             : WindowCycleItemView::kFixedPreviewHeightDp;
}

}  // namespace

WindowCycleItemView::WindowCycleItemView(aura::Window* window)
    : WindowMiniView(window, /*use_custom_focus_predicate=*/true),
      window_cycle_controller_(Shell::Get()->window_cycle_controller()) {
  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetNotifyEnterExitOnChild(true);

  // The parent of these views is not drawn due to its size, so we need to need
  // to make this a layer.
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
}

WindowCycleItemView::~WindowCycleItemView() = default;

void WindowCycleItemView::OnMouseEntered(const ui::MouseEvent& event) {
  window_cycle_controller_->SetFocusedWindow(source_window());
}

bool WindowCycleItemView::OnMousePressed(const ui::MouseEvent& event) {
  window_cycle_controller_->SetFocusedWindow(source_window());
  window_cycle_controller_->CompleteCycling();
  return true;
}

gfx::Size WindowCycleItemView::GetPreviewViewSize() const {
  // When the preview is not shown, do an estimate of the expected size.
  // |this| will not be visible anyways, and will get corrected once
  // ShowPreview() is called.
  if (!preview_view()) {
    gfx::SizeF source_size(source_window()->bounds().size());
    // Windows may have no size in tests.
    if (source_size.IsEmpty())
      return gfx::Size();
    const float aspect_ratio = source_size.width() / source_size.height();
    return gfx::Size(kFixedPreviewHeightDp * aspect_ratio,
                     kFixedPreviewHeightDp);
  }

  // Returns the size for the preview view, scaled to fit within the max
  // bounds. Scaling is always 1:1 and we only scale down, never up.
  gfx::Size preview_pref_size = preview_view()->GetPreferredSize();
  const int preview_view_height = GetPreviewFixedHeight(source_window());
  const int max_preview_width = 2 * preview_view_height;
  if (preview_pref_size.width() > max_preview_width ||
      preview_pref_size.height() > kFixedPreviewHeightDp) {
    const float scale = std::min(
        max_preview_width / static_cast<float>(preview_pref_size.width()),
        kFixedPreviewHeightDp / static_cast<float>(preview_pref_size.height()));
    preview_pref_size =
        gfx::ScaleToRoundedSize(preview_pref_size, scale, scale);
  }

  return preview_pref_size;
}

void WindowCycleItemView::Layout(PassKey) {
  LayoutSuperclass<WindowMiniView>(this);

  if (!preview_view())
    return;

  // Show the backdrop if the preview view does not take up all the bounds
  // allocated for it.
  gfx::Rect preview_max_bounds = GetContentsBounds();
  preview_max_bounds.Subtract(GetHeaderBounds());
  const gfx::Rect preview_area_bounds = preview_view()->bounds();
  SetBackdropVisibility(preview_max_bounds.size() !=
                        preview_area_bounds.size());

  if (!chromeos::features::IsRoundedWindowsEnabled()) {
    return;
  }

  if (!layer_tree_synchronizer_) {
    layer_tree_synchronizer_ = std::make_unique<ScopedLayerTreeSynchronizer>(
        layer(), /*restore_tree=*/false);
  }

  // In order to draw the final result without requiring the rendering of
  // surfaces, the rounded corners bounds of the layer tree, that is rooted at
  // WindowCycleItemView, are synchronized.
  // Since the rounded corners of the WindowPreviewView layer may overlap with
  // those of the mirrored window (as well as its mirrored transient windows),
  // and the overlapping corners might have different radii, the use of render
  // surfaces would be necessary. However, by matching (synchronizing) the
  // radii, the need for render surfaces is eliminated.
  layer_tree_synchronizer_->SynchronizeRoundedCorners(
      layer(),
      gfx::RRectF(gfx::RectF(preview_max_bounds),
                  window_util::GetMiniWindowRoundedCorners(
                      source_window(), /*include_header_rounding=*/false)));
}

gfx::Size WindowCycleItemView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  // Previews can range in width from half to double of
  // |kFixedPreviewHeightDp|. Padding will be added to the
  // sides to achieve this if the preview is too narrow.
  gfx::Size preview_size = GetPreviewViewSize();

  const int preview_height = GetPreviewFixedHeight(source_window());

  // All previews are the same height (this may add padding on top and
  // bottom).
  preview_size.set_height(preview_height);

  // Previews should never be narrower than half or wider than double their
  // fixed height.
  const int min_preview_width = preview_height / 2;
  const int max_preview_width = preview_height * 2;
  preview_size.set_width(
      std::clamp(preview_size.width(), min_preview_width, max_preview_width));

  const int margin = GetInsets().width();
  preview_size.Enlarge(margin, margin + kWindowMiniViewHeaderHeight);
  return preview_size;
}

bool WindowCycleItemView::HandleAccessibleAction(
    const ui::AXActionData& action_data) {
  // Since this class destroys itself on mouse press, and
  // View::HandleAccessibleAction calls OnEvent twice (first with a mouse press
  // event, then with a mouse release event), override the base impl from
  // triggering that behavior which leads to a UAF.
  if (action_data.action == ax::mojom::Action::kDoDefault) {
    window_cycle_controller_->SetFocusedWindow(source_window());
    window_cycle_controller_->CompleteCycling();
    return true;
  }

  return View::HandleAccessibleAction(action_data);
}

void WindowCycleItemView::RefreshItemVisuals() {
  header_view()->UpdateIconView(source_window());
  RefreshHeaderViewRoundedCorners();
  RefreshPreviewRoundedCorners();
  RefreshFocusRingVisuals();
}

BEGIN_METADATA(WindowCycleItemView)
END_METADATA

GroupContainerCycleView::GroupContainerCycleView(SnapGroup* snap_group)
    : is_layout_horizontal_(snap_group->IsSnapGroupLayoutHorizontal()) {
  mini_views_.push_back(AddChildView(std::make_unique<WindowCycleItemView>(
      snap_group->GetPhysicallyLeftOrTopWindow())));
  mini_views_.push_back(AddChildView(std::make_unique<WindowCycleItemView>(
      snap_group->GetPhysicallyRightOrBottomWindow())));
  SetShowPreview(/*show=*/true);
  RefreshItemVisuals();

  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  views::BoxLayout* layout =
      SetLayoutManager(std::make_unique<views::BoxLayout>(
          is_layout_horizontal_ ? views::BoxLayout::Orientation::kHorizontal
                                : views::BoxLayout::Orientation::kVertical,
          kInsideContainerBorderInset, kBetweenCycleItemsSpacing));
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
  GetViewAccessibility().SetDescription(
      l10n_util::GetStringUTF16(IDS_ASH_SNAP_GROUP_WINDOW_CYCLE_DESCRIPTION));
}

GroupContainerCycleView::~GroupContainerCycleView() = default;

bool GroupContainerCycleView::Contains(aura::Window* window) const {
  return base::ranges::any_of(mini_views_,
                              [window](const WindowCycleItemView* mini_view) {
                                return mini_view->Contains(window);
                              });
}

aura::Window* GroupContainerCycleView::GetWindowAtPoint(
    const gfx::Point& screen_point) const {
  for (WindowCycleItemView* mini_view : mini_views_) {
    if (auto* window = mini_view->GetWindowAtPoint(screen_point)) {
      return window;
    }
  }
  return nullptr;
}

void GroupContainerCycleView::SetShowPreview(bool show) {
  for (WindowCycleItemView* mini_view : mini_views_) {
    mini_view->SetShowPreview(show);
  }
}

void GroupContainerCycleView::RefreshItemVisuals() {
  if (mini_views_.size() == 2u) {
    mini_views_[0]->SetRoundedCornersRadius(
        window_util::GetMiniWindowRoundedCorners(
            mini_views_[0]->source_window(), /*include_header_rounding=*/true));
    mini_views_[1]->SetRoundedCornersRadius(
        window_util::GetMiniWindowRoundedCorners(
            mini_views_[1]->source_window(),
            /*include_header_rounding=*/true));
  }

  for (WindowCycleItemView* mini_view : mini_views_) {
    mini_view->RefreshItemVisuals();
  }
}

int GroupContainerCycleView::TryRemovingChildItem(
    aura::Window* destroying_window) {
  for (auto it = mini_views_.begin(); it != mini_views_.end();) {
    // Explicitly reset the current visuals so that the default rounded
    // corners i.e. rounded corners on four corners will be applied on the
    // remaining item.
    (*it)->ResetRoundedCorners();
    if ((*it)->Contains(destroying_window)) {
      RemoveChildViewT(*it);
      it = mini_views_.erase(it);
    } else {
      ++it;
    }
  }

  RefreshItemVisuals();
  return mini_views_.size();
}

void GroupContainerCycleView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  for (WindowCycleItemView* mini_view : mini_views_) {
    if (mini_view->is_mini_view_focused()) {
      mini_view->GetViewAccessibility().GetAccessibleNodeData(node_data);
      break;
    }
  }
}

gfx::RoundedCornersF GroupContainerCycleView::GetRoundedCorners() const {
  if (mini_views_.empty()) {
    return gfx::RoundedCornersF();
  }

  if (mini_views_.size() == 1u) {
    return mini_views_[0]->GetRoundedCorners();
  }

  CHECK_EQ(mini_views_.size(), 2u);

  // For horizontal window layout, the left corners (`upper_left` and
  // `lower_left`) will depend on the primary snapped window, and likewise for
  // the right corners.
  // For vertical window layout, the top corners (`upper_left` and
  // `upper_right`) will depend on the primary snapped window, and likewise for
  // the bottom corners.
  const float upper_left = mini_views_[0]->GetRoundedCorners().upper_left();
  const float upper_right =
      is_layout_horizontal_ ? mini_views_[1]->GetRoundedCorners().upper_right()
                            : mini_views_[0]->GetRoundedCorners().upper_right();
  const float lower_right = mini_views_[1]->GetRoundedCorners().lower_right();
  const float lower_left =
      is_layout_horizontal_ ? mini_views_[0]->GetRoundedCorners().lower_left()
                            : mini_views_[1]->GetRoundedCorners().lower_left();
  return gfx::RoundedCornersF(upper_left, upper_right, lower_right, lower_left);
}

void GroupContainerCycleView::SetSelectedWindowForFocus(aura::Window* window) {
  const bool old_is_first_focus_selection_request =
      is_first_focus_selection_request_;
  is_first_focus_selection_request_ = false;

  if (mini_views_.size() == 1u) {
    mini_views_[0]->UpdateFocusState(/*focus=*/true);
    return;
  }

  CHECK_EQ(mini_views_.size(), 2u);
  // If `this` is the first item in the cycle list with secondary snapped window
  // focused, cycle the primary snapped window first.
  if (old_is_first_focus_selection_request &&
      window_util::GetActiveWindow() == mini_views_[1]->source_window()) {
    mini_views_[0]->UpdateFocusState(/*focus=*/true);
    NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
  } else {
    // For normal use case, follow the window cycle order and `UpdateFocusState`
    // on the cycle item that contains the target window.
    for (WindowCycleItemView* mini_view : mini_views_) {
      if (mini_view->Contains(window)) {
        mini_view->UpdateFocusState(/*focus=*/true);
        NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
        break;
      }
    }
  }
}

void GroupContainerCycleView::ClearFocusSelection() {
  for (WindowCycleItemView* mini_view : mini_views_) {
    mini_view->UpdateFocusState(/*focus=*/false);
  }
}

BEGIN_METADATA(GroupContainerCycleView)
END_METADATA

}  // namespace ash