chromium/ash/wm_mode/pie_menu_view.h

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef ASH_WM_MODE_PIE_MENU_VIEW_H_
#define ASH_WM_MODE_PIE_MENU_VIEW_H_

#include <stack>
#include <string>
#include <vector>

#include "ash/ash_export.h"
#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/views/view.h"

namespace gfx {
struct VectorIcon;
}  // namespace gfx

namespace views {
class ImageButton;
}  // namespace views

namespace ash {

class PieMenuButton;
class PieMenuView;

// -----------------------------------------------------------------------------
// PieSubMenuContainerView:

// Defines a container for buttons representing menu items in a pie menu.
class ASH_EXPORT PieSubMenuContainerView : public views::View {
  METADATA_HEADER(PieSubMenuContainerView, views::View)

 public:
  PieSubMenuContainerView(const PieSubMenuContainerView&) = delete;
  PieSubMenuContainerView& operator=(const PieSubMenuContainerView&) = delete;
  ~PieSubMenuContainerView() override;

  size_t button_count() const { return buttons_.size(); }

  // Adds a new menu item button with the given `button_id`,
  // `button_label_text`, and an optional `icon` (if non-null). The `button_id`
  // must be unique among all the IDs of the current existing buttons hosted by
  // parent `PieMenuView` and all its sub menus.
  // Returns a pointer to the newly added button.
  views::View* AddMenuButton(int button_id,
                             const std::u16string& button_label_text,
                             const gfx::VectorIcon* icon);

  // Removes all the currently added buttons in this container. This can be used
  // to rebuild the contents of this sub menu from scratch.
  void RemoveAllButtons();

 private:
  friend class PieMenuView;

  explicit PieSubMenuContainerView(PieMenuView* owner_menu_view);

  // The parent owner pie menu that hosts this container. Not null.
  const raw_ptr<PieMenuView> owner_menu_view_;

  // The buttons on this container, which will be painted as slices of a circle
  // in their same order in this vector.
  std::vector<raw_ptr<PieMenuButton, VectorExperimental>> buttons_;
};

// -----------------------------------------------------------------------------
// PieMenuView:

// Defines a pie menu that lists its menu items as radial slices of a circle.
// Each menu item button can open its sub menu with its own buttons, replacing
// the current content of this pie view. A back button is then shown to go back
// to the previous sub menu in the stack. There is always a main menu container
// at all times.
class ASH_EXPORT PieMenuView : public views::View {
  METADATA_HEADER(PieMenuView, views::View)

 public:
  // Defines an interface for the delegate of this class which will be informed
  // when a button on this pie view is pressed.
  class Delegate {
   public:
    // Called when the button with the given `button_id` is pressed.
    virtual void OnPieMenuButtonPressed(int button_id) = 0;

   protected:
    virtual ~Delegate() = default;
  };

  explicit PieMenuView(Delegate* delegate);
  PieMenuView(const PieMenuView&) = delete;
  PieMenuView& operator=(const PieMenuView&) = delete;
  ~PieMenuView() override;

  PieSubMenuContainerView* main_menu_container() {
    return main_menu_container_;
  }

  // Returns the `PieSubMenuContainerView` that the button whose ID is
  // `button_id` opens when pressed if any, or creates one for it and associates
  // it with that button. A button with `button_id` must exist in this pie menu.
  PieSubMenuContainerView* GetOrAddSubMenuForButton(int button_id);

  // Sets the label of the button whose ID is `button_id` to the given `text`.
  // A button with `button_id` must exist in this pie menu.
  void SetButtonLabelText(int button_id, const std::u16string& text);

  // Pops all the sub menus (if any) and shows the main menu.
  void ReturnToMainMenu();

  // Similar to `GetButtonById()` below but returns the button as a
  // `views::View`. This prevents the need for exposing the actual type of
  // `PieMenuButton`.
  views::View* GetButtonByIdAsView(int button_id) const;

  // Gets the center point in screen coordinates of the contents of the button
  // whose ID is `button_id`. If no such button is found, an empty point is
  // returned.
  gfx::Point GetButtonContentsCenterInScreen(int button_id) const;

  // views::View:
  void Layout(PassKey) override;
  void AddedToWidget() override;
  void OnThemeChanged() override;

 private:
  friend class PieSubMenuContainerView;

  // Called when the given `button` is added on any of the sub menu containers
  // hosted by this view.
  void OnPieMenuButtonAdded(PieMenuButton* button);

  // Called when the given `button` is removed from any of the sub menu
  // containers hosted by this view.
  void OnPieMenuButtonRemoved(PieMenuButton* button);

  // Called when the given `button` is pressed. This will inform the `delegate_`
  // via the `OnPieMenuButtonPressed()` API.
  void OnPieMenuButtonPressed(PieMenuButton* button);

  // Opens the given `sub_menu` by making it the only visible sub menu container
  // which visually replaces the contents of this pie view with the buttons
  // hosted by the given `sub_menu`. The `back_button_` will be shown to allow
  // the user to go back to the previous sub menu in `active_sub_menus_stack_`
  // (if any) or to the `main_menu_container_`.
  void OpenSubMenu(PieSubMenuContainerView* sub_menu);

  // If there are active sub menus in `active_sub_menus_stack_`, this function
  // pops the top-most one such that the previous sub menu shows. Finally when
  // there are no more sub menus, the `main_menu_container_` will show, and the
  // `back_button_` will hide.
  void MaybePopSubMenu();

  // Returns the button whose ID is `button_id` or nullptr if it doesn't exist.
  PieMenuButton* GetButtonById(int button_id) const;

  // The delegate of this view which takes care of handling button presses. Not
  // null.
  const raw_ptr<Delegate, DanglingUntriaged> delegate_;

  // The container hosting the buttons on the main menu of this view. When this
  // is visible, `active_sub_menus_stack_` should be empty, and `back_button_`
  // should be hidden.
  const raw_ptr<PieSubMenuContainerView> main_menu_container_;

  // Since a button on a sub menu can open another sub menu and so on, we keep
  // a stack of currently active sub menus (other than the main menu), so that
  // pressing on `back_button_` pops the top-most sub menu to show the previous
  // one, until there are no more active sub menus, at which point
  // `main_menu_container_` shows up and `back_button_` hides.
  std::stack<raw_ptr<PieSubMenuContainerView, CtnExperimental>>
      active_sub_menus_stack_;

  const raw_ptr<views::ImageButton> back_button_;

  // Maps all the buttons on all sub menu containers of this view by their IDs.
  base::flat_map</*button_id=*/int, PieMenuButton*> buttons_by_id_;
};

}  // namespace ash

#endif  // ASH_WM_MODE_PIE_MENU_VIEW_H_