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