chromium/ash/system/mahi/mahi_panel_widget.cc

// Copyright 2024 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/system/mahi/mahi_panel_widget.h"

#include <cstdint>
#include <memory>

#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/system/mahi/mahi_panel_view.h"
#include "ash/system/mahi/mahi_ui_controller.h"
#include "ash/system/mahi/refresh_banner_view.h"
#include "ash/wm/work_area_insets.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_type.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/outsets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/unique_widget_ptr.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

constexpr int kRefreshBannerHeight = 32;
constexpr int kPanelBoundsPadding = 8;

constexpr char kWidgetName[] = "MahiPanel";

// TODO(b/319731776): Use panel bounds in size calculations instead of
// `kPanelDefaultWidth` and `kPanelDefaultHeight` when the panel is resizable.

bool IsSpaceAvailableOnRight(const gfx::Rect& screen_work_area,
                             const gfx::Rect& mahi_menu_bounds) {
  return mahi_menu_bounds.x() + mahi_constants::kPanelDefaultWidth <
         screen_work_area.right();
}

int CalculateAvailableSpaceOnBottom(const gfx::Rect& screen_work_area,
                                    const gfx::Rect& mahi_menu_bounds) {
  return screen_work_area.bottom() - mahi_menu_bounds.y() -
         mahi_constants::kPanelDefaultHeight - kPanelBoundsPadding;
}

gfx::Rect CalculateAnimationStartBounds(const gfx::Rect& mahi_menu_bounds) {
  const gfx::Rect screen_work_area = display::Screen::GetScreen()
                                         ->GetDisplayMatching(mahi_menu_bounds)
                                         .work_area();

  return gfx::Rect(
      IsSpaceAvailableOnRight(screen_work_area, mahi_menu_bounds)
          ? mahi_menu_bounds.x()
          : mahi_menu_bounds.right() - mahi_constants::kPanelDefaultWidth,
      CalculateAvailableSpaceOnBottom(screen_work_area, mahi_menu_bounds)
          ? mahi_menu_bounds.y()
          : mahi_menu_bounds.bottom() - mahi_menu_bounds.height(),
      mahi_constants::kPanelDefaultWidth, mahi_menu_bounds.height());
}

gfx::Rect CalculateInitialWidgetBounds(const gfx::Rect& mahi_menu_bounds) {
  const gfx::Rect screen_work_area = display::Screen::GetScreen()
                                         ->GetDisplayMatching(mahi_menu_bounds)
                                         .work_area();

  int available_space_on_bottom =
      CalculateAvailableSpaceOnBottom(screen_work_area, mahi_menu_bounds);

  // The `MahiPanelWidget` will be aligned with the top of the
  // mahi context menu when possible. Otherwise we will try to keep the top of
  // the `MahiPanelWidget` as close to the top of the mahi context menu as
  // possible. This is to provide a seamless transition between the context menu
  // and the panel.
  return gfx::Rect(
      IsSpaceAvailableOnRight(screen_work_area, mahi_menu_bounds)
          ? mahi_menu_bounds.x()
          : mahi_menu_bounds.right() - mahi_constants::kPanelDefaultWidth,
      available_space_on_bottom > 0
          ? mahi_menu_bounds.y()
          : mahi_menu_bounds.y() + available_space_on_bottom,
      mahi_constants::kPanelDefaultWidth, mahi_constants::kPanelDefaultHeight);
}

}  // namespace

MahiPanelWidget::MahiPanelWidget(InitParams params,
                                 MahiUiController* ui_controller)
    : views::Widget(std::move(params)) {
  auto* contents_view = SetContentsView(
      views::Builder<views::BoxLayoutView>()
          // We need to set a negative value for between child spacing here
          // because we need the `RefreshBannerView` to overlap with the
          // `MahiPanelView`.
          .SetBetweenChildSpacing(-mahi_constants::kRefreshBannerStackDepth)
          .SetOrientation(views::BoxLayout::Orientation::kVertical)
          .Build());

  refresh_view_ = contents_view->AddChildView(
      std::make_unique<RefreshBannerView>(ui_controller));
  refresh_view_observation_.Observe(refresh_view_);

  auto* panel_view = contents_view->AddChildView(
      std::make_unique<MahiPanelView>(ui_controller));

  // Make sure the `MahiPanelView` is sized to fill up the available space.
  contents_view->SetFlexForView(panel_view, 1.0);
  shelf_observation_.Observe(Shelf::ForWindow(Shell::GetPrimaryRootWindow()));
}

MahiPanelWidget::~MahiPanelWidget() = default;

// static
views::UniqueWidgetPtr MahiPanelWidget::CreateAndShowPanelWidget(
    int64_t display_id,
    const gfx::Rect& mahi_menu_bounds,
    MahiUiController* ui_controller) {
  auto* root_window = Shell::GetRootWindowForDisplayId(display_id);

  views::Widget::InitParams params(
      views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.name = GetName();
  // `SystemModalContainer` can travel across displays, is not automatically
  // resizable on limited screen size and stays on top on full-screen.
  params.parent =
      Shell::GetContainer(root_window, kShellWindowId_SystemModalContainer);

  // The widget's view handles round corners and blur via layers.
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.layer_type = ui::LAYER_NOT_DRAWN;

  views::UniqueWidgetPtr widget =
      std::make_unique<MahiPanelWidget>(std::move(params), ui_controller);
  widget->SetBounds(CalculateInitialWidgetBounds(mahi_menu_bounds));

  widget->Show();

  views::AsViewClass<MahiPanelView>(widget->GetContentsView()->GetViewByID(
                                        mahi_constants::ViewId::kMahiPanelView))
      ->AnimatePopIn(CalculateAnimationStartBounds(mahi_menu_bounds));

  return widget;
}

// static
const char* MahiPanelWidget::GetName() {
  return kWidgetName;
}

void MahiPanelWidget::OnShelfWorkAreaInsetsChanged() {
  gfx::Rect work_area_bounds =
      WorkAreaInsets::ForWindow(GetNativeWindow())->user_work_area_bounds();
  gfx::Rect panel_bounds = GetWindowBoundsInScreen();

  // If the panel widget does not fit inside of the work area bounds (e.g. due
  // to opening the virtual keyboard), its bounds will be updated so it's
  // partially visible, prioritizing the bottom part of the panel.
  if (panel_bounds.bottom() + kPanelBoundsPadding >=
      work_area_bounds.bottom()) {
    SetY(work_area_bounds.bottom() - mahi_constants::kPanelDefaultHeight -
         kPanelBoundsPadding);
  } else if (panel_bounds.y() - kPanelBoundsPadding <= work_area_bounds.y()) {
    SetY(work_area_bounds.y() + kPanelBoundsPadding);
  }
}

void MahiPanelWidget::OnViewVisibilityChanged(views::View* observed_view,
                                              views::View* starting_view) {
  CHECK_EQ(observed_view, refresh_view_);

  if (is_refresh_view_visible_ == observed_view->GetVisible()) {
    return;
  }
  is_refresh_view_visible_ = observed_view->GetVisible();

  // Update widget bounds to provide space for the refresh banner if needed.
  gfx::Rect widget_bounds = GetWindowBoundsInScreen();
  if (is_refresh_view_visible_) {
    widget_bounds.Outset(gfx::Outsets::TLBR(kRefreshBannerHeight, 0, 0, 0));
  } else {
    widget_bounds.Inset(gfx::Insets::TLBR(kRefreshBannerHeight, 0, 0, 0));
  }
  SetBounds(widget_bounds);
}

void MahiPanelWidget::OnViewIsDeleting(views::View* observed_view) {
  CHECK_EQ(observed_view, refresh_view_);

  refresh_view_observation_.Reset();
  is_refresh_view_visible_ = false;
  refresh_view_ = nullptr;
}

}  // namespace ash