chromium/ash/system/unified/feature_tile.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_SYSTEM_UNIFIED_FEATURE_TILE_H_
#define ASH_SYSTEM_UNIFIED_FEATURE_TILE_H_

#include "ash/ash_export.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/color/color_id.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/button.h"

namespace gfx {
struct VectorIcon;
class Insets;
}  // namespace gfx

namespace views {
class FlexLayoutView;
class ImageButton;
class ImageView;
class InkDropContainerView;
class Label;
}  // namespace views

namespace ash {

// The main button used in FeatureTilesContainerView, which acts as an entry
// point for features in QuickSettingsView.
//
// Note: Once http://b/298692153 is complete then this will be the type of tile
// used in the VC controls bubble as well.
//
// There are two TileTypes: Primary and Compact.
//
// The primary tile has an icon and title, and may have a subtitle. The icon may
// or may not be separately clickable. The tile has one of the following
// behaviors:
// 1. Launch surface        (e.g. Screen Capture)
// 2. Toggle                (e.g. Toggle Dark Theme)
// 3. Drill-in              (e.g. Go to Accessibility detailed view)
// 4. Toggle with drill-in  (e.g. Toggle Wi-Fi | Go to Network details)
// 5. Togglable tile with decorative drill-in (e.g. Selecting a VPN network)
//
// The compact tile has an icon and a single title, which may be
// multi-line. They are always placed in pairs side by side to take up the
// space of a regular FeatureTile. Regular tiles may switch to their compact
// version when necessary, e.g. when entering TabletMode. It presents one
// of the following behaviors:
// 1. Launch surface        (e.g. Screen Capture)
// 2. Toggle                (e.g. Toggle Auto-rotate)
// 3. Drill-in              (e.g. Go to Cast detailed view)
//
// Support for download UI is in the process of being added. This will allow a
// compact tile to indicate the progress of a download it is associated with.
// The initial use-case will be DLC downloading for the "Live caption" feature
// tile in the VC controls bubble, though the API for setting download state
// should be general enough that it can be used for anything download-related.
// See http://b/298692153 for details.
class ASH_EXPORT FeatureTile : public views::Button {
  METADATA_HEADER(FeatureTile, views::Button)

 public:
  // Used in the FeatureTile constructor to set the tile view type.
  enum class TileType {
    kPrimary = 0,
    kCompact = 1,
    kMaxValue = kCompact,
  };

  // The possible states the download progress UI can be in. The download
  // progress UI is currently only supported for compact tiles.
  //
  // TODO(b/315188874): Add full support for all download states.
  enum class DownloadState {
    kNone,         // The default state, e.g. this tile is not associated
                   // with a download. If this tile is of type
                   // `TileType::kPrimary` then it should always be in this
                   // download state.
    kPending,      // The download has not yet started. The tile's label is
                   // changed to "Download pending". The tile is not
                   // interactable while in this state.
    kDownloading,  // The download is in progress. The tile's label is changed
                   // to "Downloading X%" and a download progress indicator is
                   // made visible. The tile is not interactable while in this
                   // state.
    kDownloaded,   // The download finished successfully.
    kError,        // The download finished with an error. The tile is not
                   // interactable while in this state.
  };

  // Constructor for FeatureTiles. `callback` will be called when interacting
  // with the main part of the button, which accounts for the whole tile.
  // If the icon is not separately clickable (the default), `callback` will
  // also be called when clicking on the icon.
  explicit FeatureTile(PressedCallback callback,
                       bool is_togglable = true,
                       TileType type = TileType::kPrimary);
  FeatureTile(const FeatureTile&) = delete;
  FeatureTile& operator=(const FeatureTile&) = delete;
  ~FeatureTile() override;

  // Implement this class to get notified of certain state changes to this tile.
  // Currently this only exists for testing purposes.
  class Observer : public base::CheckedObserver {
   public:
    // Called when this tile's download state changes. `download_state` is the
    // new download state, and `progress` is the new download progress
    // (currently only meaningful when the download state is
    // `DownloadState::kDownloading`).
    virtual void OnDownloadStateChanged(DownloadState download_state,
                                        int progress) = 0;
  };

  // Adds/removes an observer of this tile.
  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);

  // Sets whether the icon on the left is clickable, separate from clicking on
  // the tile itself. Use SetIconClickCallback() to set the callback. This
  // function is separate from SetIconClickCallback() because it's likely that
  // FeatureTile users will want to set the callback once but may want to switch
  // the icon between being clickable or not (e.g. the network icon based on
  // Ethernet vs. Wi-Fi).
  void SetIconClickable(bool clickable);

  // Sets the `callback` for clicks on `icon_button_`.
  void SetIconClickCallback(base::RepeatingCallback<void()> callback);

  // Sets the `on_title_container_bounds_changed_` callback.
  void SetOnTitleBoundsChangedCallback(
      base::RepeatingCallback<void()> callback);

  // Creates a decorative `drill_in_arrow_` on the right side of the tile. This
  // indicates to the user that the tile shows a detailed view when pressed.
  void CreateDecorativeDrillInArrow();

  // Updates the colors of the background and elements of the button.
  void UpdateColors();

  // Updates the `toggled_` state of the tile. If the tile is not togglable,
  // `toggled_` will always be false.
  void SetToggled(bool toggled);
  bool IsToggled() const;

  // Sets the vector icon.
  void SetVectorIcon(const gfx::VectorIcon& icon);

  // Sets margins for 'title_container_' in the tile.
  void SetTitleContainerMargins(const gfx::Insets& insets);

  // Setters to apply custom background colors.
  void SetBackgroundColorId(ui::ColorId background_color_id);
  void SetBackgroundToggledColorId(ui::ColorId background_toggled_color_id);
  void SetBackgroundDisabledColorId(ui::ColorId background_disabled_color_id);

  // Sets the radius determining the tile's curved edges.
  void SetButtonCornerRadius(const int radius);

  // Setters to apply custom foreground colors.
  void SetForegroundColorId(ui::ColorId foreground_color_id);
  void SetForegroundToggledColorId(ui::ColorId foreground_toggled_color_id);
  void SetForegroundDisabledColorId(ui::ColorId foreground_disabled_color_id);
  void SetForegroundOptionalColorId(ui::ColorId foreground_optional_color_id);
  void SetForegroundOptionalToggledColorId(
      ui::ColorId foreground_optional_toggled_color_id);

  // Sets a custom color for the tile's ink drop, when its toggled.
  void SetInkDropToggledBaseColorId(ui::ColorId ink_drop_toggled_base_color_id);

  // Sets the tile icon from an ImageSkia.
  void SetImage(gfx::ImageSkia image);

  // Sets the tooltip text of `icon_button_`.
  void SetIconButtonTooltipText(const std::u16string& text);

  // Sets the text of `label_`. If `VcDlcUi` is enabled and there is an on-going
  // download associated with this tile then the new label won't be reflected in
  // the UI until the download finishes. Also note that download-related labels
  // (like "Downloading 7%" or "Download pending") should not be specified using
  // this method - those labels are automatically set when the download state
  // changes.
  void SetLabel(const std::u16string& label);

  // Returns the maximum width for `sub_label_`.
  int GetSubLabelMaxWidth() const;

  // Sets the text of the `sub_label_`.
  void SetSubLabel(const std::u16string& sub_label);

  // Sets visibility of `sub_label_`.
  void SetSubLabelVisibility(bool visible);

  // Sets the state of this tile's download progress UI. See the documentation
  // for the `DownloadState` enum for more details on how a particular download
  // state affects the tile. `progress` is an integer in the range [0, 100], and
  // is ignored when `state` is not `DownloadState::kDownloading`.
  void SetDownloadState(DownloadState state, int progress);

  // views::View:
  void AddLayerToRegion(ui::Layer* layer, views::LayerRegion region) override;
  void RemoveLayerFromRegions(ui::Layer* layer) override;

  // views::ViewObserver:
  void OnViewBoundsChanged(views::View* observed_view) override;

  base::WeakPtr<FeatureTile> GetWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

  TileType tile_type() { return type_; }
  bool is_icon_clickable() const { return is_icon_clickable_; }
  views::ImageButton* icon_button() { return icon_button_; }
  views::Label* label() { return label_; }
  views::Label* sub_label() { return sub_label_; }
  views::ImageView* drill_in_arrow() { return drill_in_arrow_; }
  int corner_radius() const { return corner_radius_; }

  DownloadState download_state_for_testing() const { return download_state_; }
  int download_progress_for_testing() const {
    return download_progress_percent_;
  }

 private:
  friend class BluetoothFeaturePodControllerTest;
  friend class HotspotFeaturePodControllerTest;
  friend class NotificationCounterViewTest;

  // A `views::Background` that visually indicates download progress.
  // Automatically handles both LTR and RTL layouts.
  class ProgressBackground : public views::Background {
   public:
    ProgressBackground(ui::ColorId progress_color_id,
                       ui::ColorId background_color_id);
    ProgressBackground(const ProgressBackground&) = delete;
    ProgressBackground& operator=(const ProgressBackground&) = delete;
    ~ProgressBackground() override = default;

    // views::Background:
    void Paint(gfx::Canvas* canvas, views::View* view) const override;

   private:
    // The `ui::ColorId`s for both the progress- and non-progress-(i.e.
    // "background-") portions of the background.
    const ui::ColorId progress_color_id_;
    const ui::ColorId background_color_id_;
  };

  // views::Button:
  void OnSetTooltipText(const std::u16string& tooltip_text) override;

  // Creates child views of Feature Tile. The constructed view will vary
  // depending on the button's `type_`.
  void CreateChildViews();

  // Returns the color id to use for the `icon_button_` and `drill_in_arrow_`
  // based on the tile's enabled and toggled state.
  ui::ColorId GetIconColorId() const;

  // Updates the ink drop hover color and ripple color for `icon_button_`.
  void UpdateIconButtonRippleColors();

  // Updates the focus ring color for `icon_button_` for better visibility.
  void UpdateIconButtonFocusRingColor();

  // Updates the color of `drill_in_arrow_` for better visibility.
  void UpdateDrillInArrowColor();

  // Updates the accessibility properties directly in the cache, like the role
  // and the toggle state.
  void UpdateAccessibilityProperties();

  // Updates `label_` attributes depending on whether a sub-label will be
  // visible.
  void SetCompactTileLabelPreferences(bool has_sub_label);

  // Sets the tile's label to its download-related version (e.g. "Downloading
  // 7%" or "Download pending"). This is different from `SetLabel()` because
  // `SetLabel()` is intended to be used externally for setting the tile's
  // non-download-related label (i.e. the "client-specified" label), whereas
  // this method is only used internally by this class to temporarily switch to
  // a different label during download. An optional tooltip can be included if
  // the tooltip should differ from the `download_label`.
  void SetDownloadLabel(const std::u16string& download_label,
                        std::optional<std::u16string> tooltip = std::nullopt);

  // Updates the tile's label according to the current download state. Note that
  // this method assumes the download-related state (e.g. `download_state_` and
  // `download_progress_percent_`) is current, so it is up to the client to
  // perform any download-related state changes prior to calling this.
  void UpdateLabelForDownloadState();

  // Notifies all observers of this tile's current download state. Should only
  // be called when the download state actually changes.
  void NotifyDownloadStateChanged();

  // A list of this tile's observers.
  base::ObserverList<Observer> observers_;

  // Ensures the ink drop is painted above the button's background.
  raw_ptr<views::InkDropContainerView> ink_drop_container_ = nullptr;

  // The vector icon for the tile, if one is set.
  raw_ptr<const gfx::VectorIcon> vector_icon_ = nullptr;

  // Customized value for the tile's background color and foreground color.
  std::optional<ui::ColorId> background_color_;
  std::optional<ui::ColorId> background_toggled_color_;
  std::optional<ui::ColorId> background_disabled_color_;
  std::optional<ui::ColorId> foreground_color_;
  std::optional<ui::ColorId> foreground_toggled_color_;
  std::optional<ui::ColorId> foreground_optional_color_;
  std::optional<ui::ColorId> foreground_optional_toggled_color_;
  std::optional<ui::ColorId> foreground_disabled_color_;

  // Customized value for the tile's ink drop color.
  std::optional<ui::ColorId> ink_drop_toggled_base_color_;

  // Owned by views hierarchy.
  raw_ptr<views::ImageButton> icon_button_ = nullptr;
  raw_ptr<views::Label> label_ = nullptr;
  raw_ptr<views::Label> sub_label_ = nullptr;
  raw_ptr<views::ImageView> drill_in_arrow_ = nullptr;
  raw_ptr<views::FlexLayoutView> title_container_ = nullptr;

  // The radius of the tile's curved edges.
  int corner_radius_;

  // Whether the icon is separately clickable.
  bool is_icon_clickable_ = false;

  // Whether this button is togglable.
  const bool is_togglable_ = false;

  // Whether the button is currently toggled.
  bool toggled_ = false;

  // The non-download-related (a.k.a. "client-specified") text of this tile's
  // label/tooltip. The tile's label/tooltip may change when its downloading
  // state changes, so this is used to store the original, client-specified
  // label/tooltip for later reference (e.g. when a download finishes and the
  // tile needs to show the original label again).
  std::u16string client_specified_label_text_;
  std::u16string client_specified_tooltip_text_;

  // The type of the feature tile that determines how it lays out its view.
  TileType type_;

  // Used to update tile colors and to set the drill-in button enabled state
  // when the button state changes.
  base::CallbackListSubscription enabled_changed_subscription_;

  // The download state this tile is in. A tile is not associated with a
  // download by default.
  DownloadState download_state_ = DownloadState::kNone;

  // True when `UpdateLabelForDownloadState()` is happening.
  bool updating_download_state_labels_ = false;

  // The download progress, as an integer percentage in the range [0, 100]. Only
  // has meaning when the tile is in an active download state.
  int download_progress_percent_ = 0;

  // Runs when `title_container_`'s bounds is changed.
  base::RepeatingClosure on_title_container_bounds_changed_;

  base::WeakPtrFactory<FeatureTile> weak_ptr_factory_{this};
};

}  // namespace ash

#endif  // ASH_SYSTEM_UNIFIED_FEATURE_TILE_H_