323 lines
13 KiB
C++
323 lines
13 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2022 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/ui/window_android.h"
|
|
|
|
#include <android/input.h>
|
|
#include <jni.h>
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/base/math.h"
|
|
#include "xenia/ui/surface_android.h"
|
|
#include "xenia/ui/windowed_app_context_android.h"
|
|
|
|
namespace xe {
|
|
namespace ui {
|
|
|
|
std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
|
|
const std::string_view title,
|
|
uint32_t desired_logical_width,
|
|
uint32_t desired_logical_height) {
|
|
return std::make_unique<AndroidWindow>(
|
|
app_context, title, desired_logical_width, desired_logical_height);
|
|
}
|
|
|
|
AndroidWindow::~AndroidWindow() {
|
|
EnterDestructor();
|
|
auto& android_app_context =
|
|
static_cast<AndroidWindowedAppContext&>(app_context());
|
|
if (android_app_context.GetActivityWindow() == this) {
|
|
android_app_context.SetActivityWindow(nullptr);
|
|
}
|
|
}
|
|
|
|
void AndroidWindow::OnActivitySurfaceLayoutChange() {
|
|
auto& android_app_context =
|
|
static_cast<const AndroidWindowedAppContext&>(app_context());
|
|
assert_true(android_app_context.GetActivityWindow() == this);
|
|
uint32_t physical_width =
|
|
uint32_t(android_app_context.window_surface_layout_right() -
|
|
android_app_context.window_surface_layout_left());
|
|
uint32_t physical_height =
|
|
uint32_t(android_app_context.window_surface_layout_bottom() -
|
|
android_app_context.window_surface_layout_top());
|
|
OnDesiredLogicalSizeUpdate(SizeToLogical(physical_width),
|
|
SizeToLogical(physical_height));
|
|
WindowDestructionReceiver destruction_receiver(this);
|
|
OnActualSizeUpdate(physical_width, physical_height, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool AndroidWindow::OnActivitySurfaceMotionEvent(jobject event) {
|
|
auto& android_app_context =
|
|
static_cast<const AndroidWindowedAppContext&>(app_context());
|
|
JNIEnv* jni_env = android_app_context.ui_thread_jni_env();
|
|
const AndroidWindowedAppContext::JniIDs& jni_ids =
|
|
android_app_context.jni_ids();
|
|
|
|
int32_t source =
|
|
jni_env->CallIntMethod(event, jni_ids.motion_event_get_source);
|
|
|
|
switch (source) {
|
|
case AINPUT_SOURCE_TOUCHSCREEN: {
|
|
// Returning true for all touch events regardless of whether they have
|
|
// been handled to keep receiving touch events for the pointers in the
|
|
// event (if returning false for a down event, no more events will be sent
|
|
// for the pointers in it), and also because there are multiple pointers
|
|
// in a single event, and different handler invocations may result in
|
|
// different is_handled.
|
|
WindowDestructionReceiver destruction_receiver(this);
|
|
int32_t action_and_pointer_index =
|
|
jni_env->CallIntMethod(event, jni_ids.motion_event_get_action);
|
|
int32_t action = action_and_pointer_index & AMOTION_EVENT_ACTION_MASK;
|
|
// For pointer ACTION_POINTER_DOWN and ACTION_POINTER_UP.
|
|
int32_t pointer_index = (action_and_pointer_index &
|
|
AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
|
|
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
|
|
if (action == AMOTION_EVENT_ACTION_POINTER_DOWN ||
|
|
action == AMOTION_EVENT_ACTION_POINTER_UP) {
|
|
int32_t touch_pointer_index =
|
|
(action_and_pointer_index &
|
|
AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >>
|
|
AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
|
|
TouchEvent e(
|
|
this,
|
|
jni_env->CallIntMethod(event, jni_ids.motion_event_get_pointer_id,
|
|
touch_pointer_index),
|
|
(action == AMOTION_EVENT_ACTION_POINTER_DOWN)
|
|
? TouchEvent::Action::kDown
|
|
: TouchEvent::Action::kUp,
|
|
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_x,
|
|
pointer_index),
|
|
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_y,
|
|
pointer_index));
|
|
OnTouchEvent(e, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return true;
|
|
}
|
|
} else {
|
|
TouchEvent::Action touch_event_action;
|
|
switch (action) {
|
|
case AMOTION_EVENT_ACTION_DOWN:
|
|
touch_event_action = TouchEvent::Action::kDown;
|
|
break;
|
|
case AMOTION_EVENT_ACTION_UP:
|
|
touch_event_action = TouchEvent::Action::kUp;
|
|
break;
|
|
case AMOTION_EVENT_ACTION_MOVE:
|
|
touch_event_action = TouchEvent::Action::kMove;
|
|
break;
|
|
case AMOTION_EVENT_ACTION_CANCEL:
|
|
touch_event_action = TouchEvent::Action::kCancel;
|
|
break;
|
|
default:
|
|
return true;
|
|
}
|
|
int32_t touch_pointer_count = jni_env->CallIntMethod(
|
|
event, jni_ids.motion_event_get_pointer_count);
|
|
for (int32_t i = 0; i < touch_pointer_count; ++i) {
|
|
TouchEvent e(
|
|
this,
|
|
jni_env->CallIntMethod(event, jni_ids.motion_event_get_pointer_id,
|
|
i),
|
|
touch_event_action,
|
|
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_x, i),
|
|
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_y, i));
|
|
OnTouchEvent(e, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
} break;
|
|
|
|
case AINPUT_SOURCE_MOUSE: {
|
|
WindowDestructionReceiver destruction_receiver(this);
|
|
// X and Y can be outside the View (have negative coordinates, or beyond
|
|
// the size of the element), and not only for ACTION_HOVER_EXIT (it's
|
|
// predeced by ACTION_HOVER_MOVE, at least on Android API level 30, also
|
|
// with out-of-bounds coordinates), when moving the mouse outside the
|
|
// View, or when starting moving the mouse when the pointer was previously
|
|
// outside the View in some cases.
|
|
int32_t mouse_x = int32_t(
|
|
std::min(float(GetActualPhysicalWidth()),
|
|
std::max(0.0f, jni_env->CallFloatMethod(
|
|
event, jni_ids.motion_event_get_x, 0))) +
|
|
0.5f);
|
|
int32_t mouse_y = int32_t(
|
|
std::min(float(GetActualPhysicalHeight()),
|
|
std::max(0.0f, jni_env->CallFloatMethod(
|
|
event, jni_ids.motion_event_get_y, 0))) +
|
|
0.5f);
|
|
static const MouseEvent::Button kMouseEventButtons[] = {
|
|
MouseEvent::Button::kLeft, MouseEvent::Button::kRight,
|
|
MouseEvent::Button::kMiddle, MouseEvent::Button::kX1,
|
|
MouseEvent::Button::kX2,
|
|
};
|
|
static constexpr uint32_t kUsedMouseButtonMask =
|
|
(UINT32_C(1) << xe::countof(kMouseEventButtons)) - 1;
|
|
uint32_t new_mouse_button_state = uint32_t(
|
|
jni_env->CallIntMethod(event, jni_ids.motion_event_get_button_state));
|
|
// OnMouseUp.
|
|
uint32_t mouse_buttons_remaining =
|
|
mouse_button_state_ & ~new_mouse_button_state & kUsedMouseButtonMask;
|
|
uint32_t mouse_button_index;
|
|
while (
|
|
xe::bit_scan_forward(mouse_buttons_remaining, &mouse_button_index)) {
|
|
mouse_buttons_remaining &= ~(UINT32_C(1) << mouse_button_index);
|
|
MouseEvent e(this, kMouseEventButtons[mouse_button_index], mouse_x,
|
|
mouse_y);
|
|
OnMouseUp(e, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return true;
|
|
}
|
|
}
|
|
// Generic OnMouseMove regardless of the action since any event can
|
|
// provide new coordinates.
|
|
{
|
|
MouseEvent e(this, MouseEvent::Button::kNone, mouse_x, mouse_y);
|
|
OnMouseMove(e, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return true;
|
|
}
|
|
}
|
|
// OnMouseWheel.
|
|
// The axis value may be outside -1...1 if multiple scrolls have occurred
|
|
// quickly.
|
|
int32_t scroll_x = int32_t(
|
|
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_axis_value,
|
|
AMOTION_EVENT_AXIS_HSCROLL, 0) *
|
|
float(MouseEvent::kScrollPerDetent));
|
|
int32_t scroll_y = int32_t(
|
|
jni_env->CallFloatMethod(event, jni_ids.motion_event_get_axis_value,
|
|
AMOTION_EVENT_AXIS_VSCROLL, 0) *
|
|
float(MouseEvent::kScrollPerDetent));
|
|
if (scroll_x || scroll_y) {
|
|
MouseEvent e(this, MouseEvent::Button::kNone, mouse_x, mouse_y,
|
|
scroll_x, scroll_y);
|
|
OnMouseWheel(e, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return true;
|
|
}
|
|
}
|
|
// OnMouseDown.
|
|
mouse_buttons_remaining =
|
|
new_mouse_button_state & ~mouse_button_state_ & kUsedMouseButtonMask;
|
|
while (
|
|
xe::bit_scan_forward(mouse_buttons_remaining, &mouse_button_index)) {
|
|
mouse_buttons_remaining &= ~(UINT32_C(1) << mouse_button_index);
|
|
MouseEvent e(this, kMouseEventButtons[mouse_button_index], mouse_x,
|
|
mouse_y);
|
|
OnMouseDown(e, destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return true;
|
|
}
|
|
}
|
|
// Update the button state for state differences.
|
|
mouse_button_state_ = new_mouse_button_state;
|
|
return true;
|
|
} break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint32_t AndroidWindow::GetLatestDpiImpl() const {
|
|
auto& android_app_context =
|
|
static_cast<const AndroidWindowedAppContext&>(app_context());
|
|
return android_app_context.GetPixelDensity();
|
|
}
|
|
|
|
bool AndroidWindow::OpenImpl() {
|
|
// Reset the input.
|
|
mouse_button_state_ = 0;
|
|
|
|
// The window is a proxy between the main activity and Xenia, so there can be
|
|
// only one open window for an activity.
|
|
auto& android_app_context =
|
|
static_cast<AndroidWindowedAppContext&>(app_context());
|
|
AndroidWindow* previous_activity_window =
|
|
android_app_context.GetActivityWindow();
|
|
assert_null(previous_activity_window);
|
|
if (previous_activity_window) {
|
|
// Don't detach the old window as it's assuming it's still attached while
|
|
// it's in an open phase.
|
|
return false;
|
|
}
|
|
android_app_context.SetActivityWindow(this);
|
|
|
|
// Report the initial layout.
|
|
OnActivitySurfaceLayoutChange();
|
|
|
|
return true;
|
|
}
|
|
|
|
void AndroidWindow::RequestCloseImpl() {
|
|
// Finishing the Activity would cause the entire WindowedAppContext to quit,
|
|
// which is not the same behavior as on other platforms - the
|
|
// WindowedAppContext quit is done explicitly by the listeners during
|
|
// OnClosing if needed (the main Window is potentially being changed to a
|
|
// different one, for instance). Therefore, only detach the window from the
|
|
// app context.
|
|
|
|
WindowDestructionReceiver destruction_receiver(this);
|
|
OnBeforeClose(destruction_receiver);
|
|
if (destruction_receiver.IsWindowDestroyed()) {
|
|
return;
|
|
}
|
|
OnAfterClose();
|
|
|
|
auto& android_app_context =
|
|
static_cast<AndroidWindowedAppContext&>(app_context());
|
|
if (android_app_context.GetActivityWindow() == this) {
|
|
android_app_context.SetActivityWindow(nullptr);
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<Surface> AndroidWindow::CreateSurfaceImpl(
|
|
Surface::TypeFlags allowed_types) {
|
|
if (allowed_types & Surface::kTypeFlag_AndroidNativeWindow) {
|
|
auto& android_app_context =
|
|
static_cast<const AndroidWindowedAppContext&>(app_context());
|
|
assert_true(android_app_context.GetActivityWindow() == this);
|
|
ANativeWindow* activity_window_surface =
|
|
android_app_context.GetWindowSurface();
|
|
if (activity_window_surface) {
|
|
return std::make_unique<AndroidNativeWindowSurface>(
|
|
activity_window_surface);
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void AndroidWindow::RequestPaintImpl() {
|
|
auto& android_app_context =
|
|
static_cast<AndroidWindowedAppContext&>(app_context());
|
|
assert_true(android_app_context.GetActivityWindow() == this);
|
|
android_app_context.PostInvalidateWindowSurface();
|
|
}
|
|
|
|
std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,
|
|
const std::string& text,
|
|
const std::string& hotkey,
|
|
std::function<void()> callback) {
|
|
return std::make_unique<AndroidMenuItem>(type, text, hotkey, callback);
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace xe
|