blob: 1bcc21d3d3f3ed04ee6e36c3328c818529ffee42 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/arc/input_overlay/ui/touch_point.h"
#include <cmath>
#include "base/notreached.h"
#include "cc/paint/paint_flags.h"
#include "chrome/browser/ash/arc/input_overlay/actions/action.h"
#include "chrome/browser/ash/arc/input_overlay/constants.h"
#include "chrome/browser/ash/arc/input_overlay/db/proto/app_data.pb.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view.h"
#include "chrome/browser/ash/arc/input_overlay/ui/ui_utils.h"
#include "chrome/browser/ash/arc/input_overlay/util.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/cursor/cursor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/point.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
namespace arc::input_overlay {
namespace {
// `TouchPoint` consists of outside stroke, inside stroke and center.
constexpr size_t kTouchPointComponentSize = 3u;
constexpr int kDotCenterDiameter = 14;
constexpr int kDotInsideStrokeThickness = 1;
constexpr int kDotOutsideStrokeThickness = 3;
constexpr int kCrossCenterLength = 78;
constexpr int kCrossCenterMiddleLength = 30;
constexpr int kCrossInsideStrokeThickness = 1;
constexpr int kCrossOutsideStrokeThickness = 4;
constexpr int kCrossCornerRadius = 6;
// Gap between focus ring outer edge to label.
constexpr float kHaloInset = -6;
// Thickness of focus ring.
constexpr float kHaloThickness = 3;
// Draw the cross shape path with round corner. It starts from bottom to up on
// line #0 and draws clock-wisely.
// `overall_length` is the total length of one side excluding the stroke
// thickness. `mid_length` is the length of the middle part which is close to
// the one third of `overall_length`.
// __
// _0^ |__
// |__ __|
// |__|
//
SkPath DrawCrossPath(SkScalar overall_length,
SkScalar mid_length,
SkScalar corner_radius,
SkScalar out_stroke_thickness,
SkPoint center) {
SkPath path;
SkScalar short_length = (overall_length - mid_length) / 2;
path.moveTo(center.x() - mid_length / 2, center.y() - mid_length / 2);
// #0
path.rLineTo(0, -(short_length - corner_radius));
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, corner_radius, -corner_radius);
// #1
path.rLineTo(mid_length - 2 * corner_radius, 0);
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, corner_radius, corner_radius);
// #2
path.rLineTo(0, short_length - corner_radius);
// #3
path.rLineTo(short_length - corner_radius, 0);
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, corner_radius, corner_radius);
// #4
path.rLineTo(0, mid_length - 2 * corner_radius);
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, -corner_radius, +corner_radius);
// #5
path.rLineTo(-(short_length - corner_radius), 0);
// #6
path.rLineTo(0, short_length - corner_radius);
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, -corner_radius, corner_radius);
// #7
path.rLineTo(-(mid_length - 2 * corner_radius), 0);
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, -corner_radius, -corner_radius);
// #8
path.rLineTo(0, -(short_length - corner_radius));
// #9
path.rLineTo(-(short_length - corner_radius), 0);
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, -corner_radius, -corner_radius);
// #10
path.rLineTo(0, -(mid_length - 2 * corner_radius));
path.rArcTo(corner_radius, corner_radius, 0, SkPath::kSmall_ArcSize,
SkPathDirection::kCW, corner_radius, -corner_radius);
// #11
path.close();
return path;
}
SkPath DrawCrossCenter(const gfx::Point& center) {
return DrawCrossPath(
/*overall_length=*/SkIntToScalar(kCrossCenterLength),
/*mid_length=*/SkIntToScalar(kCrossCenterMiddleLength),
/*corner_radius=*/SkIntToScalar(kCrossCornerRadius),
/*out_stroke_thickness=*/
SkIntToScalar(kCrossOutsideStrokeThickness + kCrossInsideStrokeThickness),
/*center=*/SkPoint::Make(center.x(), center.y()));
}
SkPath DrawCrossOutsideStroke(const gfx::Point& center) {
return DrawCrossPath(
/*overall_length=*/SkIntToScalar(kCrossCenterLength +
2 * kCrossInsideStrokeThickness),
/*mid_length=*/
SkIntToScalar(kCrossCenterMiddleLength + 2 * kCrossInsideStrokeThickness),
/*corner_radius=*/SkIntToScalar(kCrossCornerRadius),
/*out_stroke_thickness=*/SkIntToScalar(kCrossOutsideStrokeThickness),
/*center=*/SkPoint::Make(center.x(), center.y()));
}
SkColor GetOutsideStrokeColor(const ui::ColorProvider* color_provider,
UIState ui_state) {
switch (ui_state) {
case UIState::kDefault:
case UIState::kHover:
return SkColorSetA(SK_ColorWHITE, GetAlpha(/*percent=*/0.8f));
case UIState::kDrag:
return color_provider->GetColor(
cros_tokens::kCrosSysGamingControlButtonBorderHover);
default:
NOTREACHED();
}
}
SkColor GetInsideStrokeColor(const ui::ColorProvider* color_provider,
UIState ui_state) {
switch (ui_state) {
case UIState::kDefault:
return SkColorSetA(SK_ColorBLACK, GetAlpha(/*percent=*/0.2f));
case UIState::kHover:
return SkColorSetA(SK_ColorBLACK, GetAlpha(/*percent=*/0.2f));
case UIState::kDrag:
return SkColorSetA(SK_ColorBLACK, GetAlpha(/*percent=*/0.4f));
default:
NOTREACHED();
}
}
SkColor GetCenterColor(const ui::ColorProvider* color_provider,
UIState ui_state) {
switch (ui_state) {
case UIState::kDefault:
return color_provider->GetColor(
cros_tokens::kCrosSysGamingControlButtonDefault);
case UIState::kHover:
case UIState::kDrag:
return color_provider->GetColor(
cros_tokens::kCrosSysGamingControlButtonHover);
default:
NOTREACHED();
}
}
std::array<SkColor, kTouchPointComponentSize> GetColors(
const ui::ColorProvider* color_provider,
UIState ui_state) {
return {GetOutsideStrokeColor(color_provider, ui_state),
GetInsideStrokeColor(color_provider, ui_state),
GetCenterColor(color_provider, ui_state)};
}
class CrossTouchPoint : public TouchPoint {
public:
explicit CrossTouchPoint(const gfx::Point& center_pos)
: TouchPoint(center_pos) {}
~CrossTouchPoint() override = default;
// TouchPoint:
void Init() override {
GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
GetViewAccessibility().SetName(l10n_util::GetStringUTF16(
IDS_INPUT_OVERLAY_KEYMAPPING_TOUCH_POINT_CROSS));
TouchPoint::Init();
}
// views::View:
gfx::Size CalculatePreferredSize(
const views::SizeBounds& available_size) const override {
return GetSize(ActionType::MOVE);
}
void OnPaintBackground(gfx::Canvas* canvas) override {
PaintBackground(canvas, ActionType::MOVE);
}
};
class DotTouchPoint : public TouchPoint {
public:
explicit DotTouchPoint(const gfx::Point& center_pos)
: TouchPoint(center_pos) {}
~DotTouchPoint() override = default;
// TouchPoint:
void Init() override {
GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
GetViewAccessibility().SetName(l10n_util::GetStringUTF16(
IDS_INPUT_OVERLAY_KEYMAPPING_TOUCH_POINT_DOT));
TouchPoint::Init();
}
// views::View:
gfx::Size CalculatePreferredSize(
const views::SizeBounds& available_size) const override {
return GetSize(ActionType::TAP);
}
void OnPaintBackground(gfx::Canvas* canvas) override {
PaintBackground(canvas, ActionType::TAP);
}
};
} // namespace
// static
TouchPoint* TouchPoint::Show(views::View* parent,
ActionType action_type,
const gfx::Point& center_pos) {
std::unique_ptr<TouchPoint> touch_point;
switch (action_type) {
case ActionType::TAP:
touch_point = std::make_unique<DotTouchPoint>(center_pos);
break;
case ActionType::MOVE:
touch_point = std::make_unique<CrossTouchPoint>(center_pos);
break;
default:
NOTREACHED();
}
auto* touch_point_ptr =
parent->AddChildViewAt(std::move(touch_point), /*index=*/0);
touch_point_ptr->Init();
return touch_point_ptr;
}
// static
int TouchPoint::GetEdgeLength(ActionType action_type) {
int length = 0;
switch (action_type) {
case ActionType::TAP:
length = kDotCenterDiameter + kDotInsideStrokeThickness * 2 +
kDotOutsideStrokeThickness * 2;
break;
case ActionType::MOVE:
length = kCrossCenterLength + kCrossInsideStrokeThickness * 2 +
kCrossOutsideStrokeThickness * 2;
break;
default:
NOTREACHED();
}
return length;
}
// static
gfx::Size TouchPoint::GetSize(ActionType action_type) {
const int edge_length = GetEdgeLength(action_type);
return gfx::Size(edge_length, edge_length);
}
// static
void TouchPoint::DrawTouchPoint(gfx::Canvas* canvas,
const ui::ColorProvider* color_provider,
ActionType action_type,
UIState ui_state,
const gfx::Point& center) {
DCHECK(canvas);
DCHECK(color_provider);
cc::PaintFlags flags;
flags.setAntiAlias(true);
const auto colors = GetColors(color_provider, ui_state);
DCHECK_EQ(colors.size(), kTouchPointComponentSize);
switch (action_type) {
case ActionType::TAP: {
const int radius = kDotCenterDiameter / 2;
const int center_x = center.x();
const int center_y = center.y();
// Draw outside stroke.
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kDotOutsideStrokeThickness);
flags.setColor(colors[0]);
canvas->DrawCircle(gfx::Point(center_x, center_y),
radius + kDotInsideStrokeThickness, flags);
// Draw center.
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(colors[2]);
canvas->DrawCircle(gfx::Point(center_x, center_y), radius, flags);
// Draw inside stroke.
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kDotInsideStrokeThickness);
flags.setColor(colors[1]);
canvas->DrawCircle(gfx::Point(center_x, center_y), radius, flags);
} break;
case ActionType::MOVE:
// Draw outside stroke.
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kCrossOutsideStrokeThickness);
flags.setColor(colors[0]);
canvas->DrawPath(DrawCrossOutsideStroke(center), flags);
// Draw center.
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(colors[2]);
canvas->DrawPath(DrawCrossCenter(center), flags);
// Draw inside stroke.
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kCrossInsideStrokeThickness);
flags.setColor(colors[1]);
canvas->DrawPath(DrawCrossCenter(center), flags);
break;
default:
NOTREACHED();
}
}
TouchPoint::TouchPoint(const gfx::Point& center_pos)
: center_pos_(center_pos) {}
TouchPoint::~TouchPoint() = default;
void TouchPoint::Init() {
SizeToPreferredSize();
SetPosition(gfx::Point(std::max(0, center_pos_.x() - size().width() / 2),
std::max(0, center_pos_.y() - size().height() / 2)));
SetFocusBehavior(FocusBehavior::ALWAYS);
views::FocusRing::Install(this);
auto* focus_ring = views::FocusRing::Get(this);
focus_ring->SetColorId(ui::kColorAshInputOverlayFocusRing);
focus_ring->SetHaloInset(kHaloInset);
focus_ring->SetHaloThickness(kHaloThickness);
}
void TouchPoint::OnCenterPositionChanged(const gfx::Point& point) {
center_pos_ = point;
SetPosition(gfx::Point(std::max(0, center_pos_.x() - size().width() / 2),
std::max(0, center_pos_.y() - size().height() / 2)));
}
ui::Cursor TouchPoint::GetCursor(const ui::MouseEvent& event) {
return ui::mojom::CursorType::kHand;
}
void TouchPoint::OnMouseEntered(const ui::MouseEvent& event) {
SetToHover();
}
void TouchPoint::OnMouseExited(const ui::MouseEvent& event) {
SetToDefault();
}
bool TouchPoint::OnMousePressed(const ui::MouseEvent& event) {
if (auto* parent_view = views::AsViewClass<ActionView>(parent())) {
parent_view->ApplyMousePressed(event);
}
return true;
}
bool TouchPoint::OnMouseDragged(const ui::MouseEvent& event) {
// widget is null for test.
if (auto* widget = GetWidget()) {
widget->SetCursor(ui::mojom::CursorType::kGrabbing);
}
SetToDrag();
views::AsViewClass<ActionView>(parent())->ApplyMouseDragged(event);
return true;
}
void TouchPoint::OnMouseReleased(const ui::MouseEvent& event) {
// widget is null for test.
if (auto* widget = GetWidget()) {
widget->SetCursor(ui::mojom::CursorType::kGrab);
}
SetToHover();
if (auto* parent_view = views::AsViewClass<ActionView>(parent())) {
parent_view->ApplyMouseReleased(event);
}
}
void TouchPoint::OnGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::EventType::kGestureScrollBegin:
SetToDrag();
event->SetHandled();
break;
case ui::EventType::kGestureScrollEnd:
case ui::EventType::kScrollFlingStart:
SetToDefault();
event->SetHandled();
break;
default:
break;
}
if (auto* parent_view = views::AsViewClass<ActionView>(parent())) {
parent_view->ApplyGestureEvent(event);
}
}
bool TouchPoint::OnKeyPressed(const ui::KeyEvent& event) {
if (auto* parent_view = views::AsViewClass<ActionView>(parent())) {
return parent_view->ApplyKeyPressed(event);
}
return false;
}
bool TouchPoint::OnKeyReleased(const ui::KeyEvent& event) {
if (auto* parent_view = views::AsViewClass<ActionView>(parent())) {
return parent_view->ApplyKeyReleased(event);
}
return false;
}
void TouchPoint::OnFocus() {
if (auto* parent_view = views::AsViewClass<ActionView>(parent())) {
parent_view->ShowFocusInfoMsg(
l10n_util::GetStringUTF8(
IDS_INPUT_OVERLAY_EDIT_INSTRUCTIONS_TOUCH_POINT_FOCUS),
this);
}
}
void TouchPoint::PaintBackground(gfx::Canvas* canvas, ActionType action_type) {
const auto size = GetSize(action_type);
DrawTouchPoint(canvas, GetColorProvider(), action_type, ui_state_,
/*center=*/gfx::Point(size.width() / 2, size.height() / 2));
}
void TouchPoint::SetToDefault() {
if (ui_state_ == UIState::kDefault) {
return;
}
ui_state_ = UIState::kDefault;
SchedulePaint();
}
void TouchPoint::SetToHover() {
if (ui_state_ == UIState::kHover) {
return;
}
ui_state_ = UIState::kHover;
SchedulePaint();
}
void TouchPoint::SetToDrag() {
if (ui_state_ == UIState::kDrag) {
return;
}
ui_state_ = UIState::kDrag;
SchedulePaint();
}
BEGIN_METADATA(TouchPoint)
END_METADATA
} // namespace arc::input_overlay