blob: d3342ff8f62e0cd4c04e9b52fbbbc6f5ead543fc [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/debug/stack_trace.h"
#include "cc/paint/paint_flags.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view.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/color/color_id.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/views/background.h"
#include "ui/views/controls/focus_ring.h"
namespace arc::input_overlay {
namespace {
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;
constexpr SkColor kOutsideStrokeColor =
SkColorSetA(SK_ColorWHITE, 0xCC /*80%*/);
constexpr SkColor kOutsideStrokeColorHover =
SkColorSetA(SK_ColorWHITE, 0xCC /*80%*/);
constexpr SkColor kOutsideStrokeColorDrag = gfx::kGoogleBlue200;
constexpr SkColor kInsideStrokeColor = SkColorSetA(SK_ColorBLACK, 0x33 /*20%*/);
constexpr SkColor kInsideStrokeColorHover =
SkColorSetA(SK_ColorBLACK, 0x33 /*20%*/);
constexpr SkColor kInsideStrokeColorDrag =
SkColorSetA(SK_ColorBLACK, 0x66 /*40%*/);
constexpr SkColor kCenterColor = SkColorSetRGB(0x12, 0x6D, 0xFF);
constexpr SkColor kCenterColorHover20White =
SkColorSetA(SK_ColorWHITE, 0x33 /*20%*/);
constexpr SkColor kCenterColorDrag30White =
SkColorSetA(SK_ColorWHITE, 0x4D /*30%*/);
// 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) {
SkPath path;
SkScalar short_length = (overall_length - mid_length) / 2;
path.moveTo(short_length + out_stroke_thickness,
short_length + out_stroke_thickness);
// #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(gfx::Canvas* canvas) {
return DrawCrossPath(
/*overall_length=*/SkIntToScalar(kCrossCenterLength),
/*mid_length=*/SkIntToScalar(kCrossCenterMiddleLength),
/*corner_radius=*/SkIntToScalar(kCrossCornerRadius),
/*out_stroke_thickness=*/SkIntToScalar(0));
}
SkPath DrawCrossInsideStroke(gfx::Canvas* canvas) {
return DrawCrossPath(
/*overall_length=*/SkIntToScalar(kCrossCenterLength),
/*mid_length=*/SkIntToScalar(kCrossCenterMiddleLength),
/*corner_radius=*/SkIntToScalar(kCrossCornerRadius),
/*out_stroke_thickness=*/SkIntToScalar(kCrossInsideStrokeThickness));
}
SkPath DrawCrossOutsideStroke(gfx::Canvas* canvas) {
return DrawCrossPath(
/*overall_length=*/SkIntToScalar(kCrossCenterLength +
2 * kCrossInsideStrokeThickness),
/*mid_length=*/
SkIntToScalar(kCrossCenterMiddleLength + 2 * kCrossInsideStrokeThickness),
/*corner_radius=*/SkIntToScalar(kCrossCornerRadius),
/*out_stroke_thickness=*/SkIntToScalar(kCrossOutsideStrokeThickness));
}
class Background : public views::Background {
public:
explicit Background(SkColor color) { SetNativeControlColor(color); }
~Background() override = default;
};
class CrossCenterBackground : public Background {
public:
explicit CrossCenterBackground(SkColor color) : Background(color) {}
~CrossCenterBackground() override = default;
void Paint(gfx::Canvas* canvas, views::View* view) const override {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(get_color());
canvas->DrawPath(DrawCrossCenter(canvas), flags);
}
};
class DotCenterBackground : public Background {
public:
explicit DotCenterBackground(SkColor color) : Background(color) {}
~DotCenterBackground() override = default;
void Paint(gfx::Canvas* canvas, views::View* view) const override {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(get_color());
int radius = kDotCenterDiameter / 2;
canvas->DrawCircle(gfx::Point(radius, radius), radius, flags);
}
};
class CrossInsideStrokeBackground : public Background {
public:
explicit CrossInsideStrokeBackground(SkColor color) : Background(color) {}
~CrossInsideStrokeBackground() override = default;
void Paint(gfx::Canvas* canvas, views::View* view) const override {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kCrossInsideStrokeThickness);
flags.setColor(get_color());
canvas->DrawPath(DrawCrossInsideStroke(canvas), flags);
}
};
class DotInsideStrokeBackground : public Background {
public:
explicit DotInsideStrokeBackground(SkColor color) : Background(color) {}
~DotInsideStrokeBackground() override = default;
void Paint(gfx::Canvas* canvas, views::View* view) const override {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kDotInsideStrokeThickness);
flags.setColor(get_color());
int radius = kDotCenterDiameter / 2;
int center = radius + kDotInsideStrokeThickness;
canvas->DrawCircle(gfx::Point(center, center), radius, flags);
}
};
class CrossOutsideStrokeBackground : public Background {
public:
explicit CrossOutsideStrokeBackground(SkColor color) : Background(color) {}
~CrossOutsideStrokeBackground() override = default;
void Paint(gfx::Canvas* canvas, views::View* view) const override {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kCrossOutsideStrokeThickness);
flags.setColor(kOutsideStrokeColor);
canvas->DrawPath(DrawCrossOutsideStroke(canvas), flags);
}
};
class DotOutsideStrokeBackground : public Background {
public:
explicit DotOutsideStrokeBackground(SkColor color) : Background(color) {}
~DotOutsideStrokeBackground() override = default;
void Paint(gfx::Canvas* canvas, views::View* view) const override {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setStrokeWidth(kDotOutsideStrokeThickness);
flags.setColor(get_color());
int radius = kDotCenterDiameter / 2 + kDotInsideStrokeThickness;
int center = radius + kDotOutsideStrokeThickness;
canvas->DrawCircle(gfx::Point(center, center), radius, flags);
}
};
class CrossCenter : public TouchPointElement {
public:
CrossCenter() {
SetToDefault();
int temp = kCrossInsideStrokeThickness + kCrossOutsideStrokeThickness;
SetPosition(gfx::Point(temp, temp));
SetSize(gfx::Size(kCrossCenterLength, kCrossCenterLength));
}
~CrossCenter() override = default;
// TouchPointElement:
void SetToDefault() override {
SetBackground(std::make_unique<CrossCenterBackground>(kCenterColor));
}
void SetToHover() override {
SetBackground(std::make_unique<CrossCenterBackground>(
color_utils::GetResultingPaintColor(kCenterColorHover20White,
kCenterColor)));
}
void SetToDrag() override {
SetBackground(std::make_unique<CrossCenterBackground>(
color_utils::GetResultingPaintColor(kCenterColorDrag30White,
kCenterColor)));
}
};
class DotCenter : public TouchPointElement {
public:
DotCenter() {
SetToDefault();
int temp = kDotOutsideStrokeThickness + kDotInsideStrokeThickness;
SetPosition(gfx::Point(temp, temp));
SetSize(gfx::Size(kDotCenterDiameter, kDotCenterDiameter));
}
~DotCenter() override = default;
// TouchPointElement:
void SetToDefault() override {
SetBackground(std::make_unique<DotCenterBackground>(kCenterColor));
}
void SetToHover() override {
SetBackground(std::make_unique<DotCenterBackground>(
color_utils::GetResultingPaintColor(kCenterColorHover20White,
kCenterColor)));
}
void SetToDrag() override {
SetBackground(std::make_unique<DotCenterBackground>(
color_utils::GetResultingPaintColor(kCenterColorDrag30White,
kCenterColor)));
}
};
// Because inside stroke is on top of the touch point. Use the inside stroke to
// forward the mouse and gesture events to its parent which is touch point.
class InsideStroke : public TouchPointElement {
public:
InsideStroke() = default;
~InsideStroke() override = default;
// views::View:
void OnMouseEntered(const ui::MouseEvent& event) override {
static_cast<TouchPoint*>(parent())->ApplyMouseEntered(event);
}
void OnMouseExited(const ui::MouseEvent& event) override {
static_cast<TouchPoint*>(parent())->ApplyMouseExited(event);
}
bool OnMousePressed(const ui::MouseEvent& event) override {
return static_cast<TouchPoint*>(parent())->ApplyMousePressed(event);
}
bool OnMouseDragged(const ui::MouseEvent& event) override {
return static_cast<TouchPoint*>(parent())->ApplyMouseDragged(event);
}
void OnMouseReleased(const ui::MouseEvent& event) override {
static_cast<TouchPoint*>(parent())->ApplyMouseReleased(event);
}
void OnGestureEvent(ui::GestureEvent* event) override {
return static_cast<TouchPoint*>(parent())->ApplyGestureEvent(event);
}
};
class CrossInsideStroke : public InsideStroke {
public:
CrossInsideStroke() {
SetToDefault();
int temp = kCrossOutsideStrokeThickness;
SetPosition(gfx::Point(temp, temp));
temp = kCrossCenterLength + 2 * kCrossInsideStrokeThickness;
SetSize(gfx::Size(temp, temp));
}
~CrossInsideStroke() override = default;
// TouchPointElement:
void SetToDefault() override {
SetBackground(
std::make_unique<CrossInsideStrokeBackground>(kInsideStrokeColor));
}
void SetToHover() override {
SetBackground(
std::make_unique<CrossInsideStrokeBackground>(kInsideStrokeColorHover));
}
void SetToDrag() override {
SetBackground(
std::make_unique<CrossInsideStrokeBackground>(kInsideStrokeColorDrag));
}
};
class DotInsideStroke : public InsideStroke {
public:
DotInsideStroke() {
SetToDefault();
int temp = kDotOutsideStrokeThickness;
SetPosition(gfx::Point(temp, temp));
temp = kDotCenterDiameter + 2 * kDotInsideStrokeThickness;
SetSize(gfx::Size(temp, temp));
}
~DotInsideStroke() override = default;
// TouchPointElement:
void SetToDefault() override {
SetBackground(
std::make_unique<DotInsideStrokeBackground>(kInsideStrokeColor));
}
void SetToHover() override {
SetBackground(
std::make_unique<DotInsideStrokeBackground>(kInsideStrokeColorHover));
}
void SetToDrag() override {
SetBackground(
std::make_unique<DotInsideStrokeBackground>(kInsideStrokeColorDrag));
}
};
class CrossOutsideStroke : public TouchPointElement {
public:
CrossOutsideStroke() {
SetToDefault();
SetPosition(gfx::Point(0, 0));
int temp = kCrossCenterLength + 2 * kCrossInsideStrokeThickness +
2 * kCrossOutsideStrokeThickness;
SetSize(gfx::Size(temp, temp));
}
~CrossOutsideStroke() override = default;
// TouchPointElement:
void SetToDefault() override {
SetBackground(
std::make_unique<CrossOutsideStrokeBackground>(kOutsideStrokeColor));
}
void SetToHover() override {
SetBackground(std::make_unique<CrossOutsideStrokeBackground>(
kOutsideStrokeColorHover));
}
void SetToDrag() override {
SetBackground(std::make_unique<CrossOutsideStrokeBackground>(
kOutsideStrokeColorDrag));
}
};
class DotOutsideStroke : public TouchPointElement {
public:
DotOutsideStroke() {
SetToDefault();
SetPosition(gfx::Point(0, 0));
int temp = kDotCenterDiameter + 2 * kDotInsideStrokeThickness +
2 * kDotOutsideStrokeThickness;
SetSize(gfx::Size(temp, temp));
}
~DotOutsideStroke() override = default;
// TouchPointElement:
void SetToDefault() override {
SetBackground(
std::make_unique<DotOutsideStrokeBackground>(kOutsideStrokeColor));
}
void SetToHover() override {
SetBackground(
std::make_unique<DotOutsideStrokeBackground>(kOutsideStrokeColorHover));
}
void SetToDrag() override {
SetBackground(
std::make_unique<DotOutsideStrokeBackground>(kOutsideStrokeColorDrag));
}
};
class CrossTouchPoint : public TouchPoint {
public:
explicit CrossTouchPoint(const gfx::Point& center_pos)
: TouchPoint(center_pos) {}
~CrossTouchPoint() override = default;
// TouchPoint:
void Init() override {
touch_outside_stroke_ =
AddChildView(std::make_unique<CrossOutsideStroke>());
touch_center_ = AddChildView(std::make_unique<CrossCenter>());
// Put the inside stroke on top purposely because the thickness is only 1
// and it doesn't show up obviously probably due to the round issue.
touch_inside_stroke_ = AddChildView(std::make_unique<CrossInsideStroke>());
SetAccessibilityProperties(
ax::mojom::Role::kGroup,
l10n_util::GetStringUTF16(
IDS_INPUT_OVERLAY_KEYMAPPING_TOUCH_POINT_CROSS));
TouchPoint::Init();
}
// views::View:
gfx::Size CalculatePreferredSize() const override {
int size = kCrossCenterLength + 2 * kCrossInsideStrokeThickness +
2 * kCrossOutsideStrokeThickness;
return gfx::Size(size, size);
}
};
class DotTouchPoint : public TouchPoint {
public:
explicit DotTouchPoint(const gfx::Point& center_pos)
: TouchPoint(center_pos) {}
~DotTouchPoint() override = default;
// TouchPoint:
void Init() override {
touch_outside_stroke_ = AddChildView(std::make_unique<DotOutsideStroke>());
touch_center_ = AddChildView(std::make_unique<DotCenter>());
// Put the inside stroke on top purposely because the thickness is only 1
// and it doesn't show up obviously probably due to the round issue.
touch_inside_stroke_ = AddChildView(std::make_unique<DotInsideStroke>());
SetAccessibilityProperties(
ax::mojom::Role::kGroup,
l10n_util::GetStringUTF16(
IDS_INPUT_OVERLAY_KEYMAPPING_TOUCH_POINT_DOT));
TouchPoint::Init();
}
// views::View:
gfx::Size CalculatePreferredSize() const override {
int size = kDotCenterDiameter + 2 * kDotInsideStrokeThickness +
2 * kDotOutsideStrokeThickness;
return gfx::Size(size, size);
}
};
} // namespace
TouchPointElement::TouchPointElement() = default;
TouchPointElement::~TouchPointElement() = default;
ui::Cursor TouchPointElement::GetCursor(const ui::MouseEvent& event) {
return ui::mojom::CursorType::kHand;
}
// 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;
}
gfx::Size TouchPoint::GetSize(ActionType action_type) {
int size = 0;
switch (action_type) {
case ActionType::TAP:
size = kDotCenterDiameter + kDotInsideStrokeThickness * 2 +
kDotOutsideStrokeThickness * 2;
break;
case ActionType::MOVE:
size = kCrossCenterLength + kCrossInsideStrokeThickness * 2 +
kCrossOutsideStrokeThickness * 2;
break;
default:
NOTREACHED();
}
return gfx::Size(size, size);
}
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)));
}
void TouchPoint::SetToDefault() {
touch_outside_stroke_->SetToDefault();
touch_inside_stroke_->SetToDefault();
touch_center_->SetToDefault();
}
void TouchPoint::SetToHover() {
touch_outside_stroke_->SetToHover();
touch_inside_stroke_->SetToHover();
touch_center_->SetToHover();
}
void TouchPoint::SetToDrag() {
touch_outside_stroke_->SetToDrag();
touch_inside_stroke_->SetToDrag();
touch_center_->SetToDrag();
}
void TouchPoint::ApplyMouseEntered(const ui::MouseEvent& event) {
SetToHover();
}
void TouchPoint::ApplyMouseExited(const ui::MouseEvent& event) {
SetToDefault();
}
bool TouchPoint::ApplyMousePressed(const ui::MouseEvent& event) {
return static_cast<ActionView*>(parent())->ApplyMousePressed(event);
}
bool TouchPoint::ApplyMouseDragged(const ui::MouseEvent& event) {
auto* widget = GetWidget();
// widget is null for test.
if (widget) {
widget->SetCursor(ui::mojom::CursorType::kGrabbing);
}
SetToDrag();
return static_cast<ActionView*>(parent())->ApplyMouseDragged(event);
}
void TouchPoint::ApplyMouseReleased(const ui::MouseEvent& event) {
auto* widget = GetWidget();
// widget is null for test.
if (widget) {
widget->SetCursor(ui::mojom::CursorType::kGrab);
}
SetToHover();
static_cast<ActionView*>(parent())->ApplyMouseReleased(event);
}
void TouchPoint::ApplyGestureEvent(ui::GestureEvent* event) {
switch (event->type()) {
case ui::ET_GESTURE_SCROLL_BEGIN:
SetToDrag();
event->SetHandled();
break;
case ui::ET_GESTURE_SCROLL_END:
case ui::ET_SCROLL_FLING_START:
SetToDefault();
event->SetHandled();
break;
default:
break;
}
static_cast<ActionView*>(parent())->ApplyGestureEvent(event);
}
bool TouchPoint::OnKeyPressed(const ui::KeyEvent& event) {
return static_cast<ActionView*>(parent())->ApplyKeyPressed(event);
}
bool TouchPoint::OnKeyReleased(const ui::KeyEvent& event) {
return static_cast<ActionView*>(parent())->ApplyKeyReleased(event);
}
void TouchPoint::OnFocus() {
static_cast<ActionView*>(parent())->ShowFocusInfoMsg(
l10n_util::GetStringUTF8(
IDS_INPUT_OVERLAY_EDIT_INSTRUCTIONS_TOUCH_POINT_FOCUS),
this);
}
void TouchPoint::OnBlur() {
static_cast<ActionView*>(parent())->RemoveMessage();
}
BEGIN_METADATA(TouchPoint, views::View)
END_METADATA
} // namespace arc::input_overlay