chromium/ash/glanceables/common/glanceables_time_management_bubble_view.h

// 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.

#ifndef ASH_GLANCEABLES_COMMON_GLANCEABLES_TIME_MANAGEMENT_BUBBLE_VIEW_H_
#define ASH_GLANCEABLES_COMMON_GLANCEABLES_TIME_MANAGEMENT_BUBBLE_VIEW_H_

#include <memory>
#include <string>

#include "ash/ash_export.h"
#include "ash/style/counter_expand_button.h"
#include "ash/style/error_message_toast.h"
#include "base/functional/callback_forward.h"
#include "ui/base/models/combobox_model.h"
#include "ui/compositor/throughput_tracker.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/layout/flex_layout_view.h"
#include "ui/views/view.h"

namespace gfx {
struct VectorIcon;
}  // namespace gfx

namespace ash {

class Combobox;
class GlanceablesContentsScrollView;
class GlanceablesListFooterView;
class GlanceablesProgressBarView;

// Glanceables Time Management bubble container that is a child of
// `GlanceableTrayChildBubble`.
class ASH_EXPORT GlanceablesTimeManagementBubbleView
    : public views::FlexLayoutView,
      public gfx::AnimationDelegate {
  METADATA_HEADER(GlanceablesTimeManagementBubbleView, views::FlexLayoutView)

 public:
  // The attribute that describes what type this view is used for.
  // Note that the enum values should not be reordered or reused as the values
  // are used in prefs (kGlanceablesTimeManagementLastExpandedBubble).
  enum class Context { kTasks = 0, kClassroom = 1 };

  class Observer : public base::CheckedObserver {
   public:
    // Called when the bubble view expand state change to `is_expanded`.
    // `expand_by_overscroll` is set to true if the bubble view is expanded by
    // overscroll. Details can be found in `GlanceablesContentsScrollView`.
    virtual void OnExpandStateChanged(Context context,
                                      bool is_expanded,
                                      bool expand_by_overscroll) = 0;
  };

  struct InitParams {
    InitParams();
    InitParams(InitParams&& other);
    ~InitParams();

    Context context;
    std::unique_ptr<ui::ComboboxModel> combobox_model;
    std::u16string combobox_tooltip;
    int expand_button_tooltip_id = 0;
    int collapse_button_tooltip_id = 0;
    std::u16string footer_title;
    std::u16string footer_tooltip;
    raw_ptr<const gfx::VectorIcon> header_icon = nullptr;
    int header_icon_tooltip_id = 0;
  };

  explicit GlanceablesTimeManagementBubbleView(InitParams params);
  GlanceablesTimeManagementBubbleView(
      const GlanceablesTimeManagementBubbleView&) = delete;
  GlanceablesTimeManagementBubbleView& operator=(
      const GlanceablesTimeManagementBubbleView&) = delete;
  ~GlanceablesTimeManagementBubbleView() override;

  // views::View:
  void ChildPreferredSizeChanged(View* child) override;
  void Layout(PassKey) override;
  gfx::Size GetMinimumSize() const override;
  gfx::Size CalculatePreferredSize(
      const views::SizeBounds& available_size) const override;

  // gfx::AnimationDelegate:
  void AnimationEnded(const gfx::Animation* animation) override;
  void AnimationProgressed(const gfx::Animation* animation) override;
  void AnimationCanceled(const gfx::Animation* animation) override;

  void AddObserver(Observer* observer);
  void RemoveObserver(Observer* observer);

  // Creates `this` view's own background and updates layout accordingly.
  void CreateElevatedBackground();

  // Updates `is_expanded_` and the view layout.
  // `expand_by_overscroll` indicates whether the expand state change is
  // triggered by overscroll. More information can be found in
  // `GlanceablesContentsScrollView` class.
  void SetExpandState(bool is_expanded, bool expand_by_overscroll);

  // Returns the preferred height of `this` in the collapsed state. This is used
  // to calculate the available size for glanceables. This should be constant
  // after the view is laid out.
  int GetCollapsedStatePreferredHeight() const;

  // Returns the expanded/collapsed state of the bubble view.
  bool IsExpanded() const { return is_expanded_; }

  bool is_animating_resize() const {
    return resize_animation_ && resize_animation_->is_animating();
  }

  void SetAnimationEndedClosureForTest(base::OnceClosure closure);

 protected:
  class GlanceablesExpandButton : public CounterExpandButton {
    METADATA_HEADER(GlanceablesExpandButton, CounterExpandButton)
   public:
    GlanceablesExpandButton();
    ~GlanceablesExpandButton() override;

    void SetExpandedStateTooltipStringId(int tooltip_text_id);
    void SetCollapsedStateTooltipStringId(int tooltip_text_id);
    std::u16string GetExpandedStateTooltipText() const override;
    std::u16string GetCollapsedStateTooltipText() const override;

   private:
    // The tooltip string that tells that the button can expand the bubble.
    int expand_tooltip_string_id_ = 0;
    // The tooltip string that tells that the button can collapse the bubble.
    int collapse_tooltip_string_id_ = 0;
  };

  // Linear animation to track time management bubble resize animation - as the
  // animation progresses, the bubble view preferred size will change causing
  // bubble bounds updates. `ResizeAnimation` will provide the expected
  // preferred time management bubble height.
  class ResizeAnimation : public gfx::LinearAnimation {
   public:
    // The context of the animation that determines the type of tweens and
    // duration to use.
    enum class Type {
      kContainerExpandStateChanged,
      kChildResize,
    };

    ResizeAnimation(int start_height,
                    int end_height,
                    gfx::AnimationDelegate* delegate,
                    Type type);

    int GetCurrentHeight() const;

   private:
    const Type type_;

    const int start_height_;
    const int end_height_;
  };

  // Handles press on the header icon button in `header_view_`.
  virtual void OnHeaderIconPressed() = 0;

  // Handles press on the "See all" button in `GlanceablesListFooterView`.
  virtual void OnFooterButtonPressed() = 0;

  // Called when the selected list changed in `combobox_view_`.
  virtual void SelectedListChanged() = 0;

  // Triggers bubble view resize animation to new preferred size, if an
  // animation is required.
  virtual void AnimateResize(ResizeAnimation::Type resize_type) = 0;

  // Updates the interior margin according to the current view state.
  void UpdateInteriorMargin();

  // Creates and initializes `combobox_view_`.
  void CreateComboBoxView();

  // Returns the index that `combobox_view_` is currently selected.
  size_t GetComboboxSelectedIndex() const;

  // Updates the text on `combobox_replacement_label_`.
  void UpdateComboboxReplacementLabelText();

  void SetUpResizeThroughputTracker(const std::string& histogram_name);

  // Removes an active `error_message_` from the view, if any.
  void MaybeDismissErrorMessage();
  void ShowErrorMessage(const std::u16string& error_message,
                        views::Button::PressedCallback callback,
                        ErrorMessageToast::ButtonActionType type);

  Combobox* combobox_view() { return combobox_view_; }
  GlanceablesExpandButton* expand_button() { return expand_button_; }
  GlanceablesProgressBarView* progress_bar() { return progress_bar_; }
  GlanceablesContentsScrollView* content_scroll_view() {
    return content_scroll_view_;
  }
  views::View* items_container_view() { return items_container_view_; }
  GlanceablesListFooterView* list_footer_view() { return list_footer_view_; }

  ui::ComboboxModel* combobox_model() { return combobox_model_.get(); }

  // Linear animation that drive time management bubble resize animation - the
  // animation updates the time management bubble view preferred size, which
  // causes layout updates. Runs when the bubble preferred size changes.
  std::unique_ptr<ResizeAnimation> resize_animation_;

  base::ObserverList<Observer> observers_;

 private:
  // Toggles `is_expanded_` and updates the layout.
  void ToggleExpandState();

  const Context context_;

  // Whether the view is expanded and showing the contents in contents scroll
  // view.
  bool is_expanded_ = true;

  // Owned by views hierarchy.
  raw_ptr<views::FlexLayoutView> header_view_ = nullptr;
  raw_ptr<Combobox> combobox_view_ = nullptr;
  // This is a simple label that copies the label style on `combo_box_view_` so
  // that it can visually replace it when `combo_box_view_` is hidden.
  raw_ptr<views::Label> combobox_replacement_label_ = nullptr;
  raw_ptr<GlanceablesExpandButton> expand_button_ = nullptr;
  raw_ptr<GlanceablesProgressBarView> progress_bar_ = nullptr;
  raw_ptr<GlanceablesContentsScrollView> content_scroll_view_ = nullptr;
  raw_ptr<views::View> items_container_view_ = nullptr;
  raw_ptr<GlanceablesListFooterView> list_footer_view_ = nullptr;

  // The model for `combobox_view_`.
  const std::unique_ptr<ui::ComboboxModel> combobox_model_;

  // Measure animation smoothness metrics for `resize_animation_`.
  std::optional<ui::ThroughputTracker> resize_throughput_tracker_;

  // Called when `resize_animation_` ends or is canceled. This is currently only
  // used in test.
  base::OnceClosure resize_animation_ended_closure_;

  // Owned by views hierarchy.
  raw_ptr<ErrorMessageToast> error_message_ = nullptr;
};

}  // namespace ash

#endif  // ASH_GLANCEABLES_COMMON_GLANCEABLES_TIME_MANAGEMENT_BUBBLE_VIEW_H_