chromium/ash/system/time/calendar_event_list_item_view.cc

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

#include "ash/system/time/calendar_event_list_item_view.h"

#include <string>
#include <string_view>

#include "ash/bubble/bubble_utils.h"
#include "ash/public/cpp/system_tray_client.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/pill_button.h"
#include "ash/style/style_util.h"
#include "ash/style/typography.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/time/calendar_utils.h"
#include "ash/system/time/calendar_view_controller.h"
#include "ash/system/time/event_date_formatter_util.h"
#include "base/containers/fixed_flat_map.h"
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "google_apis/calendar/calendar_api_response_types.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"

namespace ash {
namespace {

// The paddings for `CalendarEventListItemView`.
constexpr auto kEventListItemInsets =
    gfx::Insets::VH(8, calendar_utils::kEventListItemViewStartEndMargin);
constexpr auto kEventListItemHorizontalChildSpacing = 8;
constexpr int kEventListItemCornerRadius = 16;
constexpr int kEventListItemCornerDefaultRadius = 5;
constexpr int kUpNextViewEventListItemCornerRadius = 20;
constexpr float kEventListItemFocusCornerRadius = 3.0f;

// Radius of the event color dot.
constexpr int kColorDotRadius = 4;

// Dimension of the event color dot view.
constexpr int kColorDotViewSize = kColorDotRadius * 2;

// Default Calendar API color ID to use when no event color is specifified.
constexpr char kDefaultColorId[] = "7";

// The color ID for past event items.
constexpr char kPastEventsColorId[] = "0";

// Map of Calendar API color ids and their respective hex color code. For color
// id "0", it's not part of the Calendar API color, but it's used for past
// events for a gray out effect.
constexpr auto kEventHexColorCodes =
    base::MakeFixedFlatMap<std::string_view, std::string_view>(
        {{"0", "1B1B1F"},
         {"1", "6994FF"},
         {"2", "3CBD8E"},
         {"3", "BB74F2"},
         {"4", "FA827A"},
         {"5", "C7C419"},
         {"6", "F0A85F"},
         {"7", "60BDBD"},
         {"8", "BD9C9C"},
         {"9", "7077DC"},
         {"10", "5B9157"},
         {"11", "D45D5D"}});

// In Multi-Calendar, events without a custom color are injected with the color
// ID of the calendar they belong to in order to maintain a visible distinction
// between calendars when viewing events.
// Modified events have been prepended with a marker (`kInjectedColorIdPrefix`)
// to indicate that the calendar color map should be used to decode the color.
constexpr auto kCalendarHexColorCodes =
    base::MakeFixedFlatMap<std::string_view, std::string_view>(
        {{"c1", "ac725e"},  {"c2", "d06b64"},  {"c3", "f83a22"},
         {"c4", "fa573c"},  {"c5", "ff7537"},  {"c6", "ffad46"},
         {"c7", "42d692"},  {"c8", "16a765"},  {"c9", "7bd148"},
         {"c10", "b3dc6c"}, {"c11", "fbe983"}, {"c12", "fad165"},
         {"c13", "92e1c0"}, {"c14", "9fe1e7"}, {"c15", "9fc6e7"},
         {"c16", "4986e7"}, {"c17", "9a9cff"}, {"c18", "b99aff"},
         {"c19", "c2c2c2"}, {"c20", "cabdbf"}, {"c21", "cca6ac"},
         {"c22", "f691b2"}, {"c23", "cd74e6"}, {"c24", "a47ae2"}});

constexpr SkAlpha SK_Alpha38Opacity = 0x61;
constexpr SkAlpha SK_Alpha50Opacity = 0x80;

// Renders an Event color dot.
class CalendarEventListItemDot : public views::View {
  METADATA_HEADER(CalendarEventListItemDot, views::View)

 public:
  explicit CalendarEventListItemDot(std::string color_id)
      : alpha_(color_id == kPastEventsColorId ? SK_Alpha38Opacity
                                              : SK_AlphaOPAQUE) {
    CHECK(color_id.empty() || kEventHexColorCodes.count(color_id) ||
          kCalendarHexColorCodes.count(color_id));

    std::string_view hex_code = LookupColorId(color_id);
    base::HexStringToInt(hex_code, &color_);
    SetPreferredSize(gfx::Size(
        kColorDotViewSize,
        kColorDotViewSize + calendar_utils::kEventListItemViewStartEndMargin));
  }
  CalendarEventListItemDot(const CalendarEventListItemDot& other) = delete;
  CalendarEventListItemDot& operator=(const CalendarEventListItemDot& other) =
      delete;
  ~CalendarEventListItemDot() override = default;

  // Draws the circle for the event color dot.
  void OnPaint(gfx::Canvas* canvas) override {
    cc::PaintFlags color_dot;
    color_dot.setColor(SkColorSetA(color_, alpha_));
    color_dot.setStyle(cc::PaintFlags::kFill_Style);
    color_dot.setAntiAlias(true);
    canvas->DrawCircle(GetContentsBounds().CenterPoint(), kColorDotRadius,
                       color_dot);
  }

 private:
  std::string_view LookupColorId(std::string color_id) {
    const auto event_color_iter = kEventHexColorCodes.find(color_id);
    if (event_color_iter != kEventHexColorCodes.end()) {
      return event_color_iter->second;
    }
    if (calendar_utils::IsMultiCalendarEnabled()) {
      const auto cal_color_iter = kCalendarHexColorCodes.find(color_id);
      if (cal_color_iter != kCalendarHexColorCodes.end()) {
        return cal_color_iter->second;
      }
    }
    return kEventHexColorCodes.at(kDefaultColorId);
  }

  // The color value and the opacity of the dot.
  int color_;
  const SkAlpha alpha_;
};

BEGIN_METADATA(CalendarEventListItemDot)
END_METADATA

// Creates and returns a label containing the event summary.
views::Builder<views::Label> CreateSummaryLabel(
    const std::string& event_summary,
    const std::u16string& tooltip_text,
    const int& fixed_width,
    const bool is_past_event) {
  return views::Builder<views::Label>(
             bubble_utils::CreateLabel(
                 TypographyToken::kCrosButton2,
                 event_summary.empty()
                     ? l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_NO_TITLE)
                     : base::UTF8ToUTF16(event_summary),
                 is_past_event ? cros_tokens::kCrosSysDisabled
                               : cros_tokens::kCrosSysOnSurface))
      .SetID(kSummaryLabelID)
      .SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
      .SetAutoColorReadabilityEnabled(false)
      .SetMultiLine(true)
      .SetMaxLines(1)
      .SizeToFit(fixed_width)
      .SetElideBehavior(gfx::ElideBehavior::ELIDE_TAIL)
      .SetSubpixelRenderingEnabled(false)
      .SetTooltipText(tooltip_text);
}

// Creates and returns a label containing the event time.
views::Builder<views::Label> CreateTimeLabel(const std::u16string& title,
                                             const std::u16string& tooltip_text,
                                             const bool is_past_event) {
  return views::Builder<views::Label>(
             bubble_utils::CreateLabel(
                 TypographyToken::kCrosAnnotation1, title,
                 is_past_event ? cros_tokens::kCrosSysDisabled
                               : cros_tokens::kCrosSysOnSurfaceVariant))
      .SetID(kTimeLabelID)
      .SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT)
      .SetAutoColorReadabilityEnabled(false)
      .SetElideBehavior(gfx::ElideBehavior::NO_ELIDE)
      .SetSubpixelRenderingEnabled(false)
      .SetTooltipText(tooltip_text);
}

// A `HighlightPathGenerator` that uses caller-supplied rounded rect corners.
class VIEWS_EXPORT RoundedCornerHighlightPathGenerator
    : public views::HighlightPathGenerator {
 public:
  explicit RoundedCornerHighlightPathGenerator(
      const gfx::RoundedCornersF& corners)
      : corners_(corners) {}

  RoundedCornerHighlightPathGenerator(
      const RoundedCornerHighlightPathGenerator&) = delete;
  RoundedCornerHighlightPathGenerator& operator=(
      const RoundedCornerHighlightPathGenerator&) = delete;

  // views::HighlightPathGenerator:
  std::optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override {
    return gfx::RRectF(rect, corners_);
  }

 private:
  // The user-supplied rounded rect corners.
  const gfx::RoundedCornersF corners_;
};

}  // namespace

CalendarEventListItemView::CalendarEventListItemView(
    CalendarViewController* calendar_view_controller,
    SelectedDateParams selected_date_params,
    google_apis::calendar::CalendarEvent event,
    UIParams ui_params,
    EventListItemIndex event_list_item_index)
    : calendar_view_controller_(calendar_view_controller),
      selected_date_params_(selected_date_params),
      event_url_(event.html_link()),
      video_conference_url_(event.conference_data_uri()) {
  SetCallback(base::BindRepeating(&CalendarEventListItemView::PerformAction,
                                  base::Unretained(this)));

  SetLayoutManager(std::make_unique<views::FillLayout>());

  const auto [start_time, end_time] = calendar_utils::GetStartAndEndTime(
      &event, selected_date_params_.selected_date,
      selected_date_params_.selected_date_midnight,
      selected_date_params_.selected_date_midnight_utc);
  const auto [start_time_accessible_name, end_time_accessible_name] =
      event_date_formatter_util::GetStartAndEndTimeAccessibleNames(start_time,
                                                                   end_time);
  GetViewAccessibility().SetRole(ax::mojom::Role::kButton);
  const std::u16string event_item_index_in_list_string =
      l10n_util::GetStringFUTF16(
          IDS_ASH_CALENDAR_EVENT_POSITION_ACCESSIBLE_DESCRIPTION,
          base::NumberToString16(event_list_item_index.item_index),
          base::NumberToString16(event_list_item_index.total_count_of_events));
  GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
      IDS_ASH_CALENDAR_EVENT_ENTRY_ACCESSIBLE_DESCRIPTION,
      event_item_index_in_list_string, base::UTF8ToUTF16(event.summary()),
      start_time_accessible_name, end_time_accessible_name,
      calendar_utils::GetTimeZone(start_time)));
  SetFocusBehavior(FocusBehavior::ALWAYS);

  // Conditionally round the items corners depending upon where it sits in the
  // list and whether it is in the up next event list.
  const int round_corner_radius = ui_params.is_up_next_event_list_item
                                      ? kUpNextViewEventListItemCornerRadius
                                      : kEventListItemCornerRadius;
  const int top_radius = ui_params.round_top_corners
                             ? round_corner_radius
                             : kEventListItemCornerDefaultRadius;
  const int bottom_radius = ui_params.round_bottom_corners
                                ? round_corner_radius
                                : kEventListItemCornerDefaultRadius;
  const gfx::RoundedCornersF item_corner_radius = gfx::RoundedCornersF(
      top_radius, top_radius, bottom_radius, bottom_radius);
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  layer()->SetRoundedCornerRadius(item_corner_radius);
  SetUpFocusHighlight(item_corner_radius);

  std::u16string formatted_time_text;

  is_past_event_ = end_time < base::Time::NowFromSystemTime();
  if (calendar_utils::IsMultiDayEvent(&event) || event.all_day_event()) {
    formatted_time_text = event_date_formatter_util::GetMultiDayText(
        &event, selected_date_params_.selected_date_midnight,
        selected_date_params_.selected_date_midnight_utc);
  } else {
    formatted_time_text =
        event_date_formatter_util::GetFormattedInterval(start_time, end_time);
    is_current_or_next_single_day_event_ = !is_past_event_;
  }

  const auto tooltip_text = l10n_util::GetStringFUTF16(
      IDS_ASH_CALENDAR_EVENT_ENTRY_TOOL_TIP, base::UTF8ToUTF16(event.summary()),
      formatted_time_text);

  auto horizontal_layout_manager = std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, kEventListItemInsets,
      kEventListItemHorizontalChildSpacing);
  horizontal_layout_manager->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);

  views::View* horizontal_container =
      AddChildView(std::make_unique<views::View>());
  auto* horizontal_container_layout_manager =
      horizontal_container->SetLayoutManager(
          std::move(horizontal_layout_manager));

  // Event list dot.
  if (ui_params.show_event_list_dot) {
    views::View* event_list_dot_container =
        horizontal_container->AddChildView(std::make_unique<views::View>());
    auto* layout_vertical_start = event_list_dot_container->SetLayoutManager(
        std::make_unique<views::BoxLayout>(
            views::BoxLayout::Orientation::kVertical));
    layout_vertical_start->set_main_axis_alignment(
        views::BoxLayout::MainAxisAlignment::kStart);

    // TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace.
    event_list_dot_container->SetLayoutManagerUseConstrainedSpace(false);

    event_list_dot_container
        ->AddChildView(std::make_unique<CalendarEventListItemDot>(
            is_past_event_ ? kPastEventsColorId : event.color_id()))
        ->SetID(kEventListItemDotID);
  }

  // Labels.
  views::View* vertical_container =
      horizontal_container->AddChildView(std::make_unique<views::View>());
  vertical_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical));
  vertical_container->AddChildView(
      CreateSummaryLabel(event.summary(), tooltip_text, ui_params.fixed_width,
                         is_past_event_)
          .Build());
  vertical_container->AddChildView(
      CreateTimeLabel(formatted_time_text, tooltip_text, is_past_event_)
          .Build());
  horizontal_container_layout_manager->SetFlexForView(vertical_container, 1);
  // TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace.
  // `vertical_container` has 1 flex. This causes the passed constraint space to
  // be fully occupied. Thus causing the view to become larger.
  horizontal_container->SetLayoutManagerUseConstrainedSpace(false);

  // Join button. Only shows it if the event is not the past event.
  if (!video_conference_url_.is_empty() && !is_past_event_) {
    auto join_button = std::make_unique<PillButton>(
        base::BindRepeating(
            &CalendarEventListItemView::OnJoinMeetingButtonPressed,
            weak_ptr_factory_.GetWeakPtr()),
        l10n_util::GetStringUTF16(IDS_ASH_CALENDAR_JOIN_BUTTON),
        PillButton::Type::kDefaultWithoutIcon);
    join_button->GetViewAccessibility().SetName(
        l10n_util::GetStringFUTF16(IDS_ASH_CALENDAR_JOIN_BUTTON_ACCESSIBLE_NAME,
                                   base::UTF8ToUTF16(event.summary())));
    join_button->SetID(kJoinButtonID);
    horizontal_container->AddChildView(std::move(join_button));
  }
}

CalendarEventListItemView::~CalendarEventListItemView() = default;

void CalendarEventListItemView::OnThemeChanged() {
  views::View::OnThemeChanged();
  SetBackground(views::CreateSolidBackground(
      SkColorSetA(GetColorProvider()->GetColor(cros_tokens::kCrosSysSurface5),
                  is_past_event_ ? SK_Alpha50Opacity : SK_AlphaOPAQUE)));
}

void CalendarEventListItemView::PerformAction(const ui::Event& event) {
  DCHECK(event_url_.is_empty() || event_url_.is_valid());

  calendar_view_controller_->RecordEventListItemActivated(event);
  calendar_view_controller_->OnCalendarEventWillLaunch();

  GURL finalized_url;
  bool opened_pwa = false;
  Shell::Get()->system_tray_model()->client()->ShowCalendarEvent(
      event_url_, selected_date_params_.selected_date_midnight, opened_pwa,
      finalized_url);
}

void CalendarEventListItemView::SetUpFocusHighlight(
    const gfx::RoundedCornersF& item_corner_radius) {
  StyleUtil::SetUpInkDropForButton(this, gfx::Insets(),
                                   /*highlight_on_hover=*/false,
                                   /*highlight_on_focus=*/false,
                                   /*background_color=*/
                                   gfx::kPlaceholderColor);
  views::FocusRing::Get(this)->SetColorId(cros_tokens::kCrosSysFocusRing);
  views::FocusRing::Get(this)->SetHaloThickness(
      kEventListItemFocusCornerRadius);
  views::HighlightPathGenerator::Install(
      this, std::make_unique<RoundedCornerHighlightPathGenerator>(
                item_corner_radius));
  // Unset the focus painter set by `HoverHighlightView`.
  SetFocusPainter(nullptr);
}

void CalendarEventListItemView::OnJoinMeetingButtonPressed(
    const ui::Event& event) {
  calendar_view_controller_->RecordJoinMeetingButtonPressed(event);

  // The join button won't be shown if `video_conference_url_` doesn't have a
  // value.
  DCHECK(!video_conference_url_.is_empty());
  Shell::Get()->system_tray_model()->client()->ShowVideoConference(
      video_conference_url_);
}

BEGIN_METADATA(CalendarEventListItemView);
END_METADATA

}  // namespace ash