chromium/ash/hud_display/tab_strip.cc

// Copyright 2020 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/hud_display/tab_strip.h"

#include <cmath>

#include "ash/hud_display/hud_display.h"
#include "ash/hud_display/hud_properties.h"
#include "base/functional/bind.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/border.h"

namespace ash {
namespace hud_display {
namespace {

// The width in pixels of overlaying adjacent tabs. Must be an even number.
constexpr int kHUDTabOverlayWidth = 2 * kHUDTabOverlayCornerRadius / 3;

// Border around tab text (the tab overlay width will be added to this).
constexpr int kHUDTabTitleBorder = 3;

}  // namespace

BEGIN_METADATA(HUDTabButton)
END_METADATA

HUDTabButton::HUDTabButton(Style style,
                           const HUDDisplayMode display_mode,
                           const std::u16string& text)
    : views::LabelButton(views::Button::PressedCallback(), text),
      style_(style),
      display_mode_(display_mode) {
  SetHorizontalAlignment(gfx::ALIGN_CENTER);
  SetEnabledTextColors(kHUDDefaultColor);
  SetProperty(kHUDClickHandler, HTCLIENT);
  SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(
      kHUDSettingsIconBorder, kHUDTabOverlayWidth + kHUDTabTitleBorder)));

  SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
}

void HUDTabButton::SetStyle(Style style) {
  if (style_ == style)
    return;

  style_ = style;
  SchedulePaint();
}

void HUDTabButton::PaintButtonContents(gfx::Canvas* canvas) {
  // Horizontal offset from tab {0,0} where two tab arcs cross.
  constexpr int kTabArcCrossedX = kHUDTabOverlayWidth / 2;

  // Reduce kTabArcCrossedX by one pixel when calculating partial arc so that
  // the pixels along kTabArcCrossedX vertical line are drawn by full arc only.
  static const float kTabPartialArcAngle =
      90 - 180 *
               asinf((kHUDTabOverlayCornerRadius - kTabArcCrossedX + 1) /
                     (float)kHUDTabOverlayCornerRadius) /
               M_PI;

  constexpr SkScalar kCircleSize = kHUDTabOverlayCornerRadius * 2;
  const SkScalar right_edge = width();
  const SkScalar bottom_edge = height();

  SkPath path;

  // Draw left vertical line and arc
  if (style_ == Style::RIGHT) {
    /* |true| - move to the start of the arc */
    path.arcTo({0, 0, kCircleSize, kCircleSize}, -90 - kTabPartialArcAngle,
               kTabPartialArcAngle, true);
  } else {
    if (style_ == Style::LEFT) {
      // Draw bottom line from the right edge. Adjust for 2 pixels crossing the
      // right vertical line.
      path.moveTo(right_edge - kHUDTabOverlayWidth / 2 - 2, bottom_edge);
      path.lineTo(0, bottom_edge);
    } else {
      // No bottom line. Just move to the start of the vertical line.
      path.moveTo(0, bottom_edge);
    }
    /* |false| will draw straight line to the start of the arc */
    path.arcTo({0, 0, kCircleSize - 1, kCircleSize}, -180, 90, false);
  }
  // Draw top line, right arc and right vertical line
  if (style_ == Style::LEFT) {
    /* |false| will draw straight line to the start of the arc */
    path.arcTo({right_edge - kCircleSize, 0, right_edge, kCircleSize}, -90,
               kTabPartialArcAngle, false);
  } else {
    /* |false| will draw straight line to the start of the arc */
    path.arcTo({right_edge - kCircleSize, 0, right_edge, kCircleSize}, -90, 90,
               false);
    path.lineTo(right_edge, bottom_edge);
    if (style_ == Style::RIGHT) {
      // Draw bottom line to the left edge. Adjust for 2 pixels crossing the
      // left vertical line.
      path.lineTo(kHUDTabOverlayWidth / 2 + 2, bottom_edge);
    }
  }

  cc::PaintFlags flags;
  flags.setAntiAlias(true);
  flags.setBlendMode(SkBlendMode::kSrc);
  flags.setStyle(cc::PaintFlags::kStroke_Style);
  flags.setStrokeWidth(1);
  flags.setColor(kHUDDefaultColor);
  canvas->DrawPath(path, flags);
}

BEGIN_METADATA(HUDTabStrip)
END_METADATA

HUDTabStrip::HUDTabStrip(HUDDisplayView* hud) : hud_(hud) {
  SetBetweenChildSpacing(-kHUDTabOverlayWidth);
  SetInsideBorderInsets(gfx::Insets::TLBR(0, 0, 0, kHUDSettingsIconBorder));
}

HUDTabStrip::~HUDTabStrip() = default;

HUDTabButton* HUDTabStrip::AddTabButton(const HUDDisplayMode display_mode,
                                        const std::u16string& label) {
  CHECK_NE(static_cast<int>(display_mode), 0);
  // Make first tab active by default.
  HUDTabButton* tab_button = AddChildView(std::make_unique<HUDTabButton>(
      tabs_.size() ? HUDTabButton::Style::RIGHT : HUDTabButton::Style::ACTIVE,
      display_mode, label));
  tab_button->SetCallback(base::BindRepeating(
      [](HUDTabButton* sender, HUDTabStrip* tab_strip) {
        for (const ash::hud_display::HUDTabButton* tab : tab_strip->tabs_) {
          if (tab == sender) {
            tab_strip->hud_->SetDisplayMode(tab->display_mode());
            return;
          }
        }
        NOTREACHED();
      },
      base::Unretained(tab_button), base::Unretained(this)));
  tabs_.push_back(tab_button);
  return tab_button;
}

void HUDTabStrip::ActivateTab(const HUDDisplayMode mode) {
  // True if we find given active tab.
  bool found = false;

  for (HUDTabButton* tab : tabs_) {
    if (found) {
      tab->SetStyle(HUDTabButton::Style::RIGHT);
      continue;
    }
    if (tab->display_mode() == mode) {
      found = true;
      tab->SetStyle(HUDTabButton::Style::ACTIVE);
      continue;
    }
    tab->SetStyle(HUDTabButton::Style::LEFT);
  }
  DCHECK(found);
}

}  // namespace hud_display
}  // namespace ash