ppsspp/Common/UI/UIScreen.cpp
2024-11-05 11:27:16 +01:00

461 lines
12 KiB
C++

#include <algorithm>
#include "Common/Log.h"
#include "Common/System/Display.h"
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/Input/InputState.h"
#include "Common/Input/KeyCodes.h"
#include "Common/Math/curves.h"
#include "Common/UI/UIScreen.h"
#include "Common/UI/Context.h"
#include "Common/UI/Screen.h"
#include "Common/UI/Root.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Render/DrawBuffer.h"
static const bool ClickDebug = false;
UIScreen::UIScreen()
: Screen() {
lastVertical_ = UseVerticalLayout();
}
UIScreen::~UIScreen() {
delete root_;
}
bool UIScreen::UseVerticalLayout() const {
return g_display.dp_yres > g_display.dp_xres * 1.1f;
}
void UIScreen::DoRecreateViews() {
if (!recreateViews_) {
return;
}
std::lock_guard<std::recursive_mutex> guard(screenManager()->inputLock_);
UI::PersistMap persisted;
bool persisting = root_ != nullptr;
if (persisting) {
root_->PersistData(UI::PERSIST_SAVE, "root", persisted);
}
delete root_;
root_ = nullptr;
CreateViews();
UI::View *defaultView = root_ ? root_->GetDefaultFocusView() : nullptr;
if (defaultView && defaultView->GetVisibility() == UI::V_VISIBLE) {
defaultView->SetFocus();
}
recreateViews_ = false;
if (persisting && root_ != nullptr) {
root_->PersistData(UI::PERSIST_RESTORE, "root", persisted);
// Update layout and refocus so things scroll into view.
// This is for resizing down, when focused on something now offscreen.
UI::LayoutViewHierarchy(*screenManager()->getUIContext(), root_, ignoreInsets_);
UI::View *focused = UI::GetFocusedView();
if (focused) {
root_->SubviewFocused(focused);
}
}
}
void UIScreen::touch(const TouchInput &touch) {
if (!ignoreInput_ && root_) {
UI::TouchEvent(touch, root_);
}
}
void UIScreen::axis(const AxisInput &axis) {
if (!ignoreInput_ && root_) {
UI::AxisEvent(axis, root_);
}
}
bool UIScreen::key(const KeyInput &key) {
if (!ignoreInput_ && root_) {
return UI::KeyEvent(key, root_);
} else {
return false;
}
}
bool UIScreen::UnsyncTouch(const TouchInput &touch) {
if (ClickDebug && root_ && (touch.flags & TOUCH_DOWN)) {
INFO_LOG(Log::System, "Touch down!");
std::vector<UI::View *> views;
root_->Query(touch.x, touch.y, views);
for (auto view : views) {
INFO_LOG(Log::System, "%s", view->DescribeLog().c_str());
}
}
QueuedEvent ev{};
ev.type = QueuedEventType::TOUCH;
ev.touch = touch;
std::lock_guard<std::mutex> guard(eventQueueLock_);
eventQueue_.push_back(ev);
return false;
}
void UIScreen::UnsyncAxis(const AxisInput *axes, size_t count) {
QueuedEvent ev{};
ev.type = QueuedEventType::AXIS;
std::lock_guard<std::mutex> guard(eventQueueLock_);
for (size_t i = 0; i < count; i++) {
ev.axis = axes[i];
eventQueue_.push_back(ev);
}
}
bool UIScreen::UnsyncKey(const KeyInput &key) {
bool retval = false;
if (root_) {
// TODO: Make key events async too. The return value is troublesome, though.
switch (UI::UnsyncKeyEvent(key, root_)) {
case UI::KeyEventResult::ACCEPT:
retval = true;
break;
case UI::KeyEventResult::PASS_THROUGH:
retval = false;
break;
case UI::KeyEventResult::IGNORE_KEY:
return false;
}
}
QueuedEvent ev{};
ev.type = QueuedEventType::KEY;
ev.key = key;
std::lock_guard<std::mutex> guard(eventQueueLock_);
eventQueue_.push_back(ev);
return retval;
}
void UIScreen::update() {
bool vertical = UseVerticalLayout();
if (vertical != lastVertical_) {
RecreateViews();
lastVertical_ = vertical;
}
DoRecreateViews();
if (root_) {
UpdateViewHierarchy(root_);
}
while (true) {
QueuedEvent ev{};
{
std::lock_guard<std::mutex> guard(eventQueueLock_);
if (!eventQueue_.empty()) {
ev = eventQueue_.front();
eventQueue_.pop_front();
} else {
break;
}
}
if (ignoreInput_) {
continue;
}
switch (ev.type) {
case QueuedEventType::KEY:
key(ev.key);
break;
case QueuedEventType::TOUCH:
if (ClickDebug && (ev.touch.flags & TOUCH_DOWN)) {
INFO_LOG(Log::System, "Touch down!");
std::vector<UI::View *> views;
root_->Query(ev.touch.x, ev.touch.y, views);
for (auto view : views) {
INFO_LOG(Log::System, "%s", view->DescribeLog().c_str());
}
}
touch(ev.touch);
break;
case QueuedEventType::AXIS:
axis(ev.axis);
break;
}
}
}
void UIScreen::deviceLost() {
if (root_)
root_->DeviceLost();
}
void UIScreen::deviceRestored(Draw::DrawContext *draw) {
if (root_)
root_->DeviceRestored(draw);
}
void UIScreen::SetupViewport() {
using namespace Draw;
Draw::DrawContext *draw = screenManager()->getDrawContext();
_dbg_assert_(draw != nullptr);
// Bind and clear the back buffer
draw->BindFramebufferAsRenderTarget(nullptr, { RPAction::CLEAR, RPAction::CLEAR, RPAction::CLEAR, 0xFF000000 }, "UI");
screenManager()->getUIContext()->BeginFrame();
Draw::Viewport viewport;
viewport.TopLeftX = 0;
viewport.TopLeftY = 0;
viewport.Width = g_display.pixel_xres;
viewport.Height = g_display.pixel_yres;
viewport.MaxDepth = 1.0;
viewport.MinDepth = 0.0;
draw->SetViewport(viewport);
draw->SetTargetSize(g_display.pixel_xres, g_display.pixel_yres);
}
ScreenRenderFlags UIScreen::render(ScreenRenderMode mode) {
if (mode & ScreenRenderMode::FIRST) {
SetupViewport();
}
DoRecreateViews();
UIContext &uiContext = *screenManager()->getUIContext();
if (root_) {
UI::LayoutViewHierarchy(uiContext, root_, ignoreInsets_);
}
uiContext.PushTransform({translation_, scale_, alpha_});
uiContext.Begin();
DrawBackground(uiContext);
if (root_) {
root_->Draw(uiContext);
}
uiContext.Flush();
DrawForeground(uiContext);
uiContext.Flush();
uiContext.PopTransform();
return ScreenRenderFlags::NONE;
}
TouchInput UIScreen::transformTouch(const TouchInput &touch) {
TouchInput updated = touch;
float x = touch.x - translation_.x;
float y = touch.y - translation_.y;
// Scale around the center as the origin.
updated.x = (x - g_display.dp_xres * 0.5f) / scale_.x + g_display.dp_xres * 0.5f;
updated.y = (y - g_display.dp_yres * 0.5f) / scale_.y + g_display.dp_yres * 0.5f;
return updated;
}
void UIScreen::TriggerFinish(DialogResult result) {
// From here on, this dialog cannot receive input.
ignoreInput_ = true;
screenManager()->finishDialog(this, result);
}
bool UIDialogScreen::key(const KeyInput &key) {
bool retval = UIScreen::key(key);
if (!retval && (key.flags & KEY_DOWN) && UI::IsEscapeKey(key)) {
if (finished_) {
ERROR_LOG(Log::System, "Screen already finished");
} else {
finished_ = true;
TriggerFinish(DR_BACK);
UI::PlayUISound(UI::UISound::BACK);
}
return true;
}
return retval;
}
void UIDialogScreen::sendMessage(UIMessage message, const char *value) {
Screen *screen = screenManager()->dialogParent(this);
if (screen) {
screen->sendMessage(message, value);
}
}
UI::EventReturn UIScreen::OnBack(UI::EventParams &e) {
TriggerFinish(DR_BACK);
return UI::EVENT_DONE;
}
UI::EventReturn UIScreen::OnOK(UI::EventParams &e) {
TriggerFinish(DR_OK);
return UI::EVENT_DONE;
}
UI::EventReturn UIScreen::OnCancel(UI::EventParams &e) {
TriggerFinish(DR_CANCEL);
return UI::EVENT_DONE;
}
PopupScreen::PopupScreen(std::string_view title, std::string_view button1, std::string_view button2)
: title_(title) {
auto di = GetI18NCategory(I18NCat::DIALOG);
if (!button1.empty())
button1_ = di->T(button1);
if (!button2.empty())
button2_ = di->T(button2);
alpha_ = 0.0f; // inherited
}
void PopupScreen::touch(const TouchInput &touch) {
if (!box_ || (touch.flags & TOUCH_DOWN) == 0) {
// Handle down-presses here.
UIDialogScreen::touch(touch);
return;
}
// Extra bounds to avoid closing the dialog while trying to aim for something
// near the edge. Now that we only close on actual down-events, we can shrink
// this border a bit.
if (!box_->GetBounds().Expand(30.0f, 30.0f).Contains(touch.x, touch.y)) {
TriggerFinish(DR_CANCEL);
}
UIDialogScreen::touch(touch);
}
bool PopupScreen::key(const KeyInput &key) {
if (key.flags & KEY_DOWN) {
if ((key.keyCode == NKCODE_ENTER || key.keyCode == NKCODE_NUMPAD_ENTER) && defaultButton_) {
UI::EventParams e{};
defaultButton_->OnClick.Trigger(e);
return true;
}
}
return UIDialogScreen::key(key);
}
void PopupScreen::update() {
UIDialogScreen::update();
if (defaultButton_)
defaultButton_->SetEnabled(CanComplete(DR_OK));
float animatePos = 1.0f;
++frames_;
if (finishFrame_ >= 0) {
float leadOut = bezierEaseInOut((frames_ - finishFrame_) * (1.0f / (float)FRAMES_LEAD_OUT));
animatePos = 1.0f - leadOut;
if (frames_ >= finishFrame_ + FRAMES_LEAD_OUT) {
// Actual finish happens here.
screenManager()->finishDialog(this, finishResult_);
}
} else if (frames_ < FRAMES_LEAD_IN) {
float leadIn = bezierEaseInOut(frames_ * (1.0f / (float)FRAMES_LEAD_IN));
animatePos = leadIn;
}
if (animatePos < 1.0f) {
alpha_ = animatePos;
scale_.x = 0.9f + animatePos * 0.1f;
scale_.y = 0.9f + animatePos * 0.1f;
if (hasPopupOrigin_) {
float xoff = popupOrigin_.x - g_display.dp_xres / 2;
float yoff = popupOrigin_.y - g_display.dp_yres / 2;
// Pull toward the origin a bit.
translation_.x = xoff * (1.0f - animatePos) * 0.2f;
translation_.y = yoff * (1.0f - animatePos) * 0.2f;
} else {
translation_.y = -g_display.dp_yres * (1.0f - animatePos) * 0.2f;
}
} else {
alpha_ = 1.0f;
scale_.x = 1.0f;
scale_.y = 1.0f;
translation_.x = 0.0f;
translation_.y = 0.0f;
}
}
void PopupScreen::SetPopupOrigin(const UI::View *view) {
hasPopupOrigin_ = true;
popupOrigin_ = view->GetBounds().Center();
}
void PopupScreen::TriggerFinish(DialogResult result) {
if (CanComplete(result)) {
ignoreInput_ = true;
finishFrame_ = frames_;
finishResult_ = result;
OnCompleted(result);
}
// Inform UI that popup close to hide OSK (if visible)
System_NotifyUIEvent(UIEventNotification::POPUP_CLOSED);
}
void PopupScreen::CreateViews() {
using namespace UI;
UIContext &dc = *screenManager()->getUIContext();
AnchorLayout *anchor = new AnchorLayout(new LayoutParams(FILL_PARENT, FILL_PARENT));
anchor->Overflow(false);
root_ = anchor;
float yres = screenManager()->getUIContext()->GetBounds().h;
AnchorLayoutParams *anchorParams;
if (!alignTop_) {
// Standard centering etc.
anchorParams = new AnchorLayoutParams(PopupWidth(), FillVertical() ? yres - 30 : WRAP_CONTENT,
dc.GetBounds().centerX(), dc.GetBounds().centerY() + offsetY_, NONE, NONE, true);
} else {
// Top-aligned, for dialogs where we need to pop a keyboard below.
anchorParams = new AnchorLayoutParams(PopupWidth(), FillVertical() ? yres - 30 : WRAP_CONTENT,
NONE, 0, NONE, NONE, false);
}
box_ = new LinearLayout(ORIENT_VERTICAL, anchorParams);
root_->Add(box_);
box_->SetBG(dc.theme->popupStyle.background);
box_->SetHasDropShadow(hasDropShadow_);
// Since we scale a bit, make the dropshadow bleed past the edges.
box_->SetDropShadowExpand(std::max(g_display.dp_xres, g_display.dp_yres));
box_->SetSpacing(0.0f);
if (HasTitleBar()) {
View* title = new PopupHeader(title_);
box_->Add(title);
}
CreatePopupContents(box_);
root_->SetDefaultFocusView(box_);
if (ShowButtons() && !button1_.empty()) {
// And the two buttons at the bottom.
LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(200, WRAP_CONTENT));
buttonRow->SetSpacing(0);
Margins buttonMargins(5, 5);
// Adjust button order to the platform default.
if (System_GetPropertyBool(SYSPROP_OK_BUTTON_LEFT)) {
defaultButton_ = buttonRow->Add(new Button(button1_, new LinearLayoutParams(1.0f, buttonMargins)));
defaultButton_->OnClick.Handle<UIScreen>(this, &UIScreen::OnOK);
if (!button2_.empty())
buttonRow->Add(new Button(button2_, new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Handle<UIScreen>(this, &UIScreen::OnCancel);
} else {
if (!button2_.empty())
buttonRow->Add(new Button(button2_, new LinearLayoutParams(1.0f)))->OnClick.Handle<UIScreen>(this, &UIScreen::OnCancel);
defaultButton_ = buttonRow->Add(new Button(button1_, new LinearLayoutParams(1.0f)));
defaultButton_->OnClick.Handle<UIScreen>(this, &UIScreen::OnOK);
}
box_->Add(buttonRow);
}
}