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

#include "ui/views/controls/scrollbar/overlay_scroll_bar.h"

#include "base/macros.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/canvas.h"
#include "ui/native_theme/overlay_scrollbar_constants_aura.h"
#include "ui/views/background.h"
#include "ui/views/border.h"

namespace views {
namespace {

// Total thickness of the thumb (matches visuals when hovered).
constexpr int kThumbThickness =
    ui::kOverlayScrollbarThumbWidthPressed + ui::kOverlayScrollbarStrokeWidth;
// When hovered, the thumb takes up the full width. Otherwise, it's a bit
// slimmer.
constexpr int kThumbHoverOffset = 4;
// The layout size of the thumb stroke, in DIP.
constexpr int kThumbStroke = ui::kOverlayScrollbarStrokeWidth;
// The visual size of the thumb stroke, in px.
constexpr int kThumbStrokeVisualSize = ui::kOverlayScrollbarStrokeWidth;

}  // namespace

OverlayScrollBar::Thumb::Thumb(OverlayScrollBar* scroll_bar)
    : BaseScrollBarThumb(scroll_bar), scroll_bar_(scroll_bar) {
  // |scroll_bar| isn't done being constructed; it's not safe to do anything
  // that might reference it yet.
}

OverlayScrollBar::Thumb::~Thumb() {}

void OverlayScrollBar::Thumb::Init() {
  EnableCanvasFlippingForRTLUI(true);
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);
  // Animate all changes to the layer except the first one.
  OnStateChanged();
  layer()->SetAnimator(ui::LayerAnimator::CreateImplicitAnimator());
}

gfx::Size OverlayScrollBar::Thumb::CalculatePreferredSize() const {
  // The visual size of the thumb is kThumbThickness, but it slides back and
  // forth by kThumbHoverOffset. To make event targetting work well, expand the
  // width of the thumb such that it's always taking up the full width of the
  // track regardless of the offset.
  return gfx::Size(kThumbThickness + kThumbHoverOffset,
                   kThumbThickness + kThumbHoverOffset);
}

void OverlayScrollBar::Thumb::OnPaint(gfx::Canvas* canvas) {
  cc::PaintFlags fill_flags;
  fill_flags.setStyle(cc::PaintFlags::kFill_Style);
  fill_flags.setColor(SK_ColorBLACK);
  gfx::RectF fill_bounds(GetLocalBounds());
  fill_bounds.Inset(gfx::InsetsF(IsHorizontal() ? kThumbHoverOffset : 0,
                                 IsHorizontal() ? 0 : kThumbHoverOffset, 0, 0));
  fill_bounds.Inset(gfx::InsetsF(kThumbStroke, kThumbStroke,
                                 IsHorizontal() ? 0 : kThumbStroke,
                                 IsHorizontal() ? kThumbStroke : 0));
  canvas->DrawRect(fill_bounds, fill_flags);

  cc::PaintFlags stroke_flags;
  stroke_flags.setStyle(cc::PaintFlags::kStroke_Style);
  stroke_flags.setColor(
      SkColorSetA(SK_ColorWHITE, (ui::kOverlayScrollbarStrokeNormalAlpha /
                                  ui::kOverlayScrollbarThumbNormalAlpha) *
                                     SK_AlphaOPAQUE));
  stroke_flags.setStrokeWidth(kThumbStrokeVisualSize);
  stroke_flags.setStrokeCap(cc::PaintFlags::kSquare_Cap);

  // The stroke is a single pixel, so we must deal with the unscaled canvas.
  const float dsf = canvas->UndoDeviceScaleFactor();
  gfx::RectF stroke_bounds(fill_bounds);
  stroke_bounds.Scale(dsf);
  // The stroke should be aligned to the pixel center that is nearest the fill,
  // so outset by a half pixel.
  stroke_bounds.Inset(gfx::InsetsF(-kThumbStrokeVisualSize / 2.0f));
  // The stroke doesn't apply to the far edge of the thumb.
  SkPath path;
  path.moveTo(gfx::PointFToSkPoint(stroke_bounds.top_right()));
  path.lineTo(gfx::PointFToSkPoint(stroke_bounds.origin()));
  path.lineTo(gfx::PointFToSkPoint(stroke_bounds.bottom_left()));
  if (IsHorizontal()) {
    path.moveTo(gfx::PointFToSkPoint(stroke_bounds.bottom_right()));
    path.close();
  } else {
    path.lineTo(gfx::PointFToSkPoint(stroke_bounds.bottom_right()));
  }
  canvas->DrawPath(path, stroke_flags);
}

void OverlayScrollBar::Thumb::OnBoundsChanged(
    const gfx::Rect& previous_bounds) {
  scroll_bar_->Show();
  // Don't start the hide countdown if the thumb is still hovered or pressed.
  if (GetState() == Button::STATE_NORMAL)
    scroll_bar_->StartHideCountdown();
}

void OverlayScrollBar::Thumb::OnStateChanged() {
  if (GetState() == Button::STATE_NORMAL) {
    gfx::Transform translation;
    const int direction = base::i18n::IsRTL() ? -1 : 1;
    translation.Translate(
        gfx::Vector2d(IsHorizontal() ? 0 : direction * kThumbHoverOffset,
                      IsHorizontal() ? kThumbHoverOffset : 0));
    layer()->SetTransform(translation);
    layer()->SetOpacity(ui::kOverlayScrollbarThumbNormalAlpha);

    if (GetWidget())
      scroll_bar_->StartHideCountdown();
  } else {
    layer()->SetTransform(gfx::Transform());
    layer()->SetOpacity(ui::kOverlayScrollbarThumbHoverAlpha);
  }
}

OverlayScrollBar::OverlayScrollBar(bool horizontal)
    : BaseScrollBar(horizontal), hide_timer_(false, false) {
  auto* thumb = new Thumb(this);
  SetThumb(thumb);
  thumb->Init();
  set_notify_enter_exit_on_child(true);
  SetPaintToLayer();
  layer()->SetMasksToBounds(true);
  layer()->SetFillsBoundsOpaquely(false);
}

OverlayScrollBar::~OverlayScrollBar() {}

gfx::Rect OverlayScrollBar::GetTrackBounds() const {
  gfx::Rect local = GetLocalBounds();
  // The track has to be wide enough for the thumb.
  local.Inset(gfx::Insets(IsHorizontal() ? -kThumbHoverOffset : 0,
                          IsHorizontal() ? 0 : -kThumbHoverOffset, 0, 0));
  return local;
}

int OverlayScrollBar::GetThickness() const {
  return kThumbThickness;
}

bool OverlayScrollBar::OverlapsContent() const {
  return true;
}

void OverlayScrollBar::Layout() {
  gfx::Rect thumb_bounds = GetTrackBounds();
  BaseScrollBarThumb* thumb = GetThumb();
  if (IsHorizontal()) {
    thumb_bounds.set_x(thumb->x());
    thumb_bounds.set_width(thumb->width());
  } else {
    thumb_bounds.set_y(thumb->y());
    thumb_bounds.set_height(thumb->height());
  }
  thumb->SetBoundsRect(thumb_bounds);
}

void OverlayScrollBar::OnMouseEntered(const ui::MouseEvent& event) {
  Show();
}

void OverlayScrollBar::OnMouseExited(const ui::MouseEvent& event) {
  StartHideCountdown();
}

void OverlayScrollBar::Show() {
  layer()->SetOpacity(1.0f);
  hide_timer_.Stop();
}

void OverlayScrollBar::Hide() {
  ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
  settings.SetTransitionDuration(ui::kOverlayScrollbarFadeDuration);
  layer()->SetOpacity(0.0f);
}

void OverlayScrollBar::StartHideCountdown() {
  if (IsMouseHovered())
    return;
  hide_timer_.Start(
      FROM_HERE, ui::kOverlayScrollbarFadeDelay,
      base::Bind(&OverlayScrollBar::Hide, base::Unretained(this)));
}

}  // namespace views
