blob: 01e603b0548852a26bdea95d7c11f93304a9db0d [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 "ash/virtual_trackpad/virtual_trackpad_view.h"
#include <memory>
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/shell.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/blurred_background_shield.h"
#include "ash/wm/window_util.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "ui/aura/window.h"
#include "ui/aura/window_targeter.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/devices/device_data_manager.h"
#include "ui/gfx/canvas.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
namespace ash {
namespace {
// Amount to multiply each scroll event by. Makes the feature easier to use.
constexpr int kScrollMultiplier = 3;
// The number of fingers for the fake scroll events.
constexpr int kDefaultFingers = 3;
constexpr gfx::Size kDefaultSize(400, 400);
constexpr int kRoundedCorner = 30;
constexpr SkColor kTrackpadColor = SkColorSetARGB(0xFF, 0x66, 0x66, 0x66);
constexpr int kTrackpadBetweenChildSpacing = 10;
constexpr SkColor kTrackpadBorderColor = SK_ColorBLUE;
constexpr int kTrackpadBorderThickness = 6;
constexpr float kTrackpadAspectRatio = 1.4f;
constexpr float kAffordanceCircleRadius = 5.f;
constexpr float kTrackpadContainerOpacity = 0.6f;
constexpr int kTrackpadContainerPadding = 15;
constexpr SkColor kTrackpadContainerBackgroundColor = SK_ColorWHITE;
constexpr gfx::Size kButtonSize(48, 48);
constexpr float kButtonRounding = 32.f;
constexpr SkColor kSelectedTextColor = SK_ColorBLUE;
constexpr SkColor kUnselectedTextColor = SK_ColorWHITE;
views::Widget* g_fake_trackpad_widget = nullptr;
using FingerButtonOnClick = views::Button::PressedCallback;
// Creates a button to switch the number of fingers to perform gestures with.
// The button is housed inside `finger_buttons_panel_` and is tracked by
std::unique_ptr<views::LabelButton> CreateFingerButton(
int num_finger,
FingerButtonOnClick callback) {
std::unique_ptr<views::LabelButton> finger_button =
views::Builder<views::LabelButton>()
.SetCallback(std::move(callback))
.SetText(base::NumberToString16(num_finger))
.SetPreferredSize(kButtonSize)
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER)
.SetBackground(
views::CreateRoundedRectBackground(SK_ColorGRAY, kButtonRounding))
.Build();
auto* finger_button_ink_drop_host = views::InkDrop::Get(finger_button.get());
finger_button_ink_drop_host->SetMode(
views::InkDropHost::InkDropMode::ON_NO_GESTURE_HANDLER);
views::InkDrop::UseInkDropForFloodFillRipple(finger_button_ink_drop_host);
views::InstallCircleHighlightPathGenerator(finger_button.get());
return finger_button;
}
} // namespace
// -----------------------------------------------------------------------------
// TrackpadInternalSurfaceView:
// Captures mouse events and formats them before sending them to the event sink.
class TrackpadInternalSurfaceView : public views::View {
METADATA_HEADER(TrackpadInternalSurfaceView, views::View)
public:
TrackpadInternalSurfaceView() = default;
TrackpadInternalSurfaceView(const TrackpadInternalSurfaceView&) = delete;
TrackpadInternalSurfaceView& operator=(const TrackpadInternalSurfaceView&) =
delete;
~TrackpadInternalSurfaceView() override = default;
void set_fingers(int fingers) { fingers_ = fingers; }
int fingers() const { return fingers_; }
// views::View:
void OnPaint(gfx::Canvas* canvas) override {
views::View::OnPaint(canvas);
cc::PaintFlags trackpad_flags;
trackpad_flags.setStyle(cc::PaintFlags::kFill_Style);
trackpad_flags.setColor(kTrackpadColor);
trackpad_flags.setAntiAlias(true);
gfx::Rect border_bounds = GetLocalBounds();
canvas->DrawRoundRect(border_bounds, kRoundedCorner, trackpad_flags);
border_bounds.Inset(kTrackpadBorderThickness / 2);
cc::PaintFlags border_flags;
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
border_flags.setColor(SK_ColorBLUE);
border_flags.setAntiAlias(true);
border_flags.setStrokeWidth(kTrackpadBorderThickness);
canvas->DrawRoundRect(border_bounds, kRoundedCorner, border_flags);
if (!scroll_data_) {
return;
}
// Draw an affordance.
cc::PaintFlags circle_flags;
circle_flags.setStyle(cc::PaintFlags::kFill_Style);
circle_flags.setColor(kTrackpadBorderColor);
canvas->DrawCircle(scroll_data_->current_location, kAffordanceCircleRadius,
circle_flags);
}
bool OnMousePressed(const ui::MouseEvent& event) override {
scroll_data_ = ScrollData{event.location_f(), event.location_f()};
SchedulePaint();
GenerateScrollEvent(ui::ET_SCROLL_FLING_CANCEL, event);
return true;
}
bool OnMouseDragged(const ui::MouseEvent& event) override {
if (!scroll_data_) {
return true;
}
// We generate the scroll event before we update `current_location` because
// calculating the current scroll event's offset requires us to know where
// it began, which is the `current_location` from the previous scroll.
GenerateScrollEvent(ui::ET_SCROLL, event);
CHECK(scroll_data_);
scroll_data_->current_location = event.location_f();
SchedulePaint();
return true;
}
void OnMouseReleased(const ui::MouseEvent& event) override {
if (!scroll_data_) {
return;
}
GenerateScrollEvent(ui::ET_SCROLL_FLING_START, event);
scroll_data_.reset();
SchedulePaint();
}
private:
// A struct containing the relevant data during a scroll session.
struct ScrollData {
gfx::PointF initial_location;
gfx::PointF current_location;
};
// Creates a scroll event based on the mouse event on the trackpad and sends
// it to the root window host.
void GenerateScrollEvent(ui::EventType type, const ui::MouseEvent& event) {
CHECK(scroll_data_);
// `event.location_f()` is the position of the current mouse event while
// `scroll_data_->current_location` is the position of the last mouse event.
const gfx::Vector2dF distance =
event.location_f() - scroll_data_->current_location;
if (type == ui::ET_SCROLL_FLING_CANCEL) {
CHECK_EQ(gfx::Vector2dF(), distance);
}
// Mimic the true behavior of scroll events by initially flipping the value
// of vertical scroll offsets before they get sent further up the chain.
const int y_multiplier =
kScrollMultiplier * (window_util::IsNaturalScrollOn() ? 1 : -1);
const gfx::PointF location = event.location_f();
const gfx::PointF root_location = event.root_location_f();
ui::ScrollEvent scroll_event(
type, location, root_location, ui::EventTimeForNow(), /*flags=*/0,
distance.x() * kScrollMultiplier, distance.y() * y_multiplier,
/*x_offset_ordinal=*/0,
/*y_offset_ordinal=*/0, fingers_);
auto* host = GetWidget()->GetNativeWindow()->GetRootWindow()->GetHost();
CHECK(host);
std::ignore = host->SendEventToSink(&scroll_event);
}
// Contains the data during a scroll session. Empty when no scroll is
// underway.
std::optional<ScrollData> scroll_data_;
int fingers_ = kDefaultFingers;
};
BEGIN_METADATA(TrackpadInternalSurfaceView)
END_METADATA
// -----------------------------------------------------------------------------
// VirtualTrackpadView:
VirtualTrackpadView::VirtualTrackpadView() {
// Create the two children of this container.
views::Label* finger_panel_label;
finger_buttons_panel_ = AddChildView(
views::Builder<views::BoxLayoutView>()
.SetOrientation(views::BoxLayout::Orientation::kHorizontal)
.SetBetweenChildSpacing(kTrackpadBetweenChildSpacing)
.AddChild(
views::Builder<views::Label>()
.CopyAddressTo(&finger_panel_label)
.SetText(u"Fingers")
.SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT))
.Build());
finger_buttons_panel_->SetFlexForView(finger_panel_label, 1);
trackpad_view_ =
AddChildView(std::make_unique<TrackpadInternalSurfaceView>());
// Add the buttons that allow us to choose how many fingers our generated
// scroll events should have.
for (int num_finger : {3, 4}) {
finger_buttons_[num_finger] =
finger_buttons_panel_->AddChildView(CreateFingerButton(
num_finger,
base::BindRepeating(&VirtualTrackpadView::OnFingerButtonPressed,
base::Unretained(this), num_finger)));
}
UpdateFingerButtonsColors();
SetPaintToLayer();
layer()->SetOpacity(kTrackpadContainerOpacity);
SetBorder(views::CreateEmptyBorder(kTrackpadContainerPadding));
SetBackground(
views::CreateSolidBackground(kTrackpadContainerBackgroundColor));
blurred_background_ = std::make_unique<BlurredBackgroundShield>(
this, SK_ColorTRANSPARENT, ColorProvider::kBackgroundBlurSigma,
gfx::RoundedCornersF(
static_cast<float>(chromeos::kTopCornerRadiusWhenRestored)));
}
VirtualTrackpadView::~VirtualTrackpadView() = default;
// static
void VirtualTrackpadView::Toggle() {
// If we have a real trackpad, we should use that instead.
if (!ui::DeviceDataManager::GetInstance()->GetTouchpadDevices().empty()) {
return;
}
if (g_fake_trackpad_widget) {
g_fake_trackpad_widget->Close();
g_fake_trackpad_widget = nullptr;
return;
}
auto delegate = std::make_unique<views::WidgetDelegate>();
delegate->RegisterWindowClosingCallback(
base::BindOnce([]() { g_fake_trackpad_widget = nullptr; }));
delegate->SetOwnedByWidget(true);
delegate->SetCanResize(true);
delegate->SetTitle(u"Virtual Trackpad Simulator");
// `SetContentsView()` must be done through the delegate. If we wanted to call
// it directly, through `g_fake_trackpad_widget`, it would cause a CHECK to
// fail because it is a widget with non-client views due to it being a
// `TYPE_WINDOW`.
delegate->SetContentsView(std::make_unique<VirtualTrackpadView>());
views::Widget::InitParams params(views::Widget::InitParams::TYPE_WINDOW);
params.delegate = delegate.release();
// TODO(b/252556382): The bounds and root should be where the user last
// closed the window if any.
params.parent = Shell::GetContainer(Shell::GetPrimaryRootWindow(),
kShellWindowId_OverlayContainer);
params.bounds = gfx::Rect(kDefaultSize);
params.ownership = views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET;
params.name = "VirtualTrackpadWidget";
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.accept_events = true;
// The widget is owned by its native widget.
g_fake_trackpad_widget = new views::Widget(std::move(params));
g_fake_trackpad_widget->Show();
// Used to extend bounds on the virtual trackpad window for resizing. Note
// that we cannot use
// `window_util::InstallResizeHandleWindowTargeterForWindow` since the virtual
// trackpad window is not a toplevel window.
auto targeter = std::make_unique<aura::WindowTargeter>();
targeter->SetInsets(gfx::Insets(-chromeos::kResizeOutsideBoundsSize));
g_fake_trackpad_widget->GetNativeWindow()->SetEventTargeter(
std::move(targeter));
}
void VirtualTrackpadView::Layout(PassKey) {
LayoutSuperclass<views::View>(this);
// The height of the finger buttons container stays the same while the width
// matches the parent width.
gfx::Rect finger_buttons_bounds(GetContentsBounds());
finger_buttons_bounds.set_height(
finger_buttons_panel_->GetPreferredSize().height());
finger_buttons_panel_->SetBoundsRect(finger_buttons_bounds);
// Bounds after `finger_buttons_panel_` has been positioned. The trackpad
// container keeps a certain aspect ratio so it's always in a rectangular
// shape (even while resizing) that looks similar to an actual trackpad.
gfx::Rect remaining_bounds(GetContentsBounds());
remaining_bounds.Inset(
gfx::Insets::TLBR(finger_buttons_bounds.height(), 0, 0, 0));
auto trackpad_size = remaining_bounds.size();
const float trackpad_preferred_width =
remaining_bounds.height() * kTrackpadAspectRatio;
if (trackpad_preferred_width < remaining_bounds.width()) {
trackpad_size.set_width(trackpad_preferred_width);
} else {
trackpad_size.set_height(remaining_bounds.width() / kTrackpadAspectRatio);
}
remaining_bounds.ClampToCenteredSize(trackpad_size);
trackpad_view_->SetBoundsRect(remaining_bounds);
}
// static
views::Widget* VirtualTrackpadView::GetWidgetForTesting() {
return g_fake_trackpad_widget;
}
void VirtualTrackpadView::OnFingerButtonPressed(int num_fingers) {
if (num_fingers != trackpad_view_->fingers()) {
trackpad_view_->set_fingers(num_fingers);
UpdateFingerButtonsColors();
}
}
void VirtualTrackpadView::UpdateFingerButtonsColors() {
constexpr views::Button::ButtonState kStates[] = {
views::Button::STATE_NORMAL, views::Button::STATE_HOVERED,
views::Button::STATE_PRESSED, views::Button::STATE_DISABLED};
const int selected_finger_count = trackpad_view_->fingers();
for (const auto& [finger, button] : finger_buttons_) {
const bool active = finger == selected_finger_count;
for (auto state : kStates) {
button->SetTextColor(state,
active ? kSelectedTextColor : kUnselectedTextColor);
}
button->SetHighlighted(active);
}
}
views::View* VirtualTrackpadView::GetTrackpadViewForTesting() {
return trackpad_view_;
}
BEGIN_METADATA(VirtualTrackpadView, views::View)
END_METADATA
} // namespace ash