diff --git a/CMakeLists.txt b/CMakeLists.txt
index 26862ebc01..19d8e8ca45 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -735,6 +735,10 @@ add_library(Common STATIC
Common/UI/View.h
Common/UI/ViewGroup.cpp
Common/UI/ViewGroup.h
+ Common/UI/ScrollView.cpp
+ Common/UI/ScrollView.h
+ Common/UI/PopupScreens.cpp
+ Common/UI/PopupScreens.h
Common/BitScan.h
Common/BitSet.h
Common/Buffer.h
diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index d51b49d585..cf3502c526 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -554,8 +554,10 @@
+
+
@@ -996,8 +998,10 @@
+
+
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index 18cbbd9985..6dfa552f1b 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -458,6 +458,12 @@
GPU\Vulkan
+
+ UI
+
+
+ UI
+
@@ -866,6 +872,12 @@
GPU\Vulkan
+
+ UI
+
+
+ UI
+
diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp
new file mode 100644
index 0000000000..bcdc16606a
--- /dev/null
+++ b/Common/UI/PopupScreens.cpp
@@ -0,0 +1,584 @@
+#include
+
+#include "Common/UI/PopupScreens.h"
+#include "Common/UI/ViewGroup.h"
+#include "Common/UI/Context.h"
+#include "Common/UI/Root.h"
+#include "Common/StringUtils.h"
+#include "Common/Data/Text/I18n.h"
+
+namespace UI {
+
+void MessagePopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
+ using namespace UI;
+ UIContext &dc = *screenManager()->getUIContext();
+
+ std::vector messageLines;
+ SplitString(message_, '\n', messageLines);
+ for (const auto &lineOfText : messageLines)
+ parent->Add(new UI::TextView(lineOfText, ALIGN_LEFT | ALIGN_VCENTER, false))->SetTextColor(dc.theme->popupStyle.fgColor);
+}
+
+void MessagePopupScreen::OnCompleted(DialogResult result) {
+ if (result == DR_OK) {
+ if (callback_)
+ callback_(true);
+ } else {
+ if (callback_)
+ callback_(false);
+ }
+}
+
+void ListPopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
+ using namespace UI;
+
+ listView_ = parent->Add(new ListView(&adaptor_, hidden_)); //, new LinearLayoutParams(1.0)));
+ listView_->SetMaxHeight(screenManager()->getUIContext()->GetBounds().h - 140);
+ listView_->OnChoice.Handle(this, &ListPopupScreen::OnListChoice);
+}
+
+UI::EventReturn ListPopupScreen::OnListChoice(UI::EventParams &e) {
+ adaptor_.SetSelected(e.a);
+ if (callback_)
+ callback_(adaptor_.GetSelected());
+ TriggerFinish(DR_OK);
+ OnChoice.Dispatch(e);
+ return UI::EVENT_DONE;
+}
+
+PopupContextMenuScreen::PopupContextMenuScreen(const ContextMenuItem *items, size_t itemCount, I18NCategory *category, UI::View *sourceView)
+ : PopupScreen("", "", ""), items_(items), itemCount_(itemCount), category_(category), sourceView_(sourceView)
+{
+ enabled_.resize(itemCount, true);
+ SetPopupOrigin(sourceView);
+}
+
+void PopupContextMenuScreen::CreatePopupContents(UI::ViewGroup *parent) {
+ for (size_t i = 0; i < itemCount_; i++) {
+ if (items_[i].imageID) {
+ Choice *choice = new Choice(category_->T(items_[i].text), ImageID(items_[i].imageID));
+ parent->Add(choice);
+ if (enabled_[i]) {
+ choice->OnClick.Add([=](EventParams &p) {
+ TriggerFinish(DR_OK);
+ p.a = (uint32_t)i;
+ OnChoice.Dispatch(p);
+ return EVENT_DONE;
+ });
+ } else {
+ choice->SetEnabled(false);
+ }
+ }
+ }
+
+ // Hacky: Override the position to look like a popup menu.
+ AnchorLayoutParams *ap = (AnchorLayoutParams *)parent->GetLayoutParams();
+ ap->center = false;
+ ap->left = sourceView_->GetBounds().x;
+ ap->top = sourceView_->GetBounds().y2();
+}
+
+std::string ChopTitle(const std::string &title) {
+ size_t pos = title.find('\n');
+ if (pos != title.npos) {
+ return title.substr(0, pos);
+ }
+ return title;
+}
+
+UI::EventReturn PopupMultiChoice::HandleClick(UI::EventParams &e) {
+ restoreFocus_ = HasFocus();
+
+ auto category = category_ ? GetI18NCategory(category_) : nullptr;
+
+ std::vector choices;
+ for (int i = 0; i < numChoices_; i++) {
+ choices.push_back(category ? category->T(choices_[i]) : choices_[i]);
+ }
+
+ ListPopupScreen *popupScreen = new ListPopupScreen(ChopTitle(text_), choices, *value_ - minVal_,
+ std::bind(&PopupMultiChoice::ChoiceCallback, this, std::placeholders::_1));
+ popupScreen->SetHiddenChoices(hidden_);
+ if (e.v)
+ popupScreen->SetPopupOrigin(e.v);
+ screenManager_->push(popupScreen);
+ return UI::EVENT_DONE;
+}
+
+void PopupMultiChoice::Update() {
+ UpdateText();
+}
+
+void PopupMultiChoice::UpdateText() {
+ if (!choices_)
+ return;
+ auto category = GetI18NCategory(category_);
+ // Clamp the value to be safe.
+ if (*value_ < minVal_ || *value_ > minVal_ + numChoices_ - 1) {
+ valueText_ = "(invalid choice)"; // Shouldn't happen. Should be no need to translate this.
+ } else {
+ valueText_ = category ? category->T(choices_[*value_ - minVal_]) : choices_[*value_ - minVal_];
+ }
+}
+
+void PopupMultiChoice::ChoiceCallback(int num) {
+ if (num != -1) {
+ *value_ = num + minVal_;
+ UpdateText();
+
+ UI::EventParams e{};
+ e.v = this;
+ e.a = num;
+ OnChoice.Trigger(e);
+
+ if (restoreFocus_) {
+ SetFocusedView(this);
+ }
+ PostChoiceCallback(num);
+ }
+}
+
+std::string PopupMultiChoice::ValueText() const {
+ return valueText_;
+}
+
+PopupSliderChoice::PopupSliderChoice(int *value, int minValue, int maxValue, const std::string &text, ScreenManager *screenManager, const std::string &units, LayoutParams *layoutParams)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), minValue_(minValue), maxValue_(maxValue), step_(1), units_(units), screenManager_(screenManager) {
+ fmt_ = "%i";
+ OnClick.Handle(this, &PopupSliderChoice::HandleClick);
+}
+
+PopupSliderChoice::PopupSliderChoice(int *value, int minValue, int maxValue, const std::string &text, int step, ScreenManager *screenManager, const std::string &units, LayoutParams *layoutParams)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), minValue_(minValue), maxValue_(maxValue), step_(step), units_(units), screenManager_(screenManager) {
+ fmt_ = "%i";
+ OnClick.Handle(this, &PopupSliderChoice::HandleClick);
+}
+
+PopupSliderChoiceFloat::PopupSliderChoiceFloat(float *value, float minValue, float maxValue, const std::string &text, ScreenManager *screenManager, const std::string &units, LayoutParams *layoutParams)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), minValue_(minValue), maxValue_(maxValue), step_(1.0f), units_(units), screenManager_(screenManager) {
+ fmt_ = "%2.2f";
+ OnClick.Handle(this, &PopupSliderChoiceFloat::HandleClick);
+}
+
+PopupSliderChoiceFloat::PopupSliderChoiceFloat(float *value, float minValue, float maxValue, const std::string &text, float step, ScreenManager *screenManager, const std::string &units, LayoutParams *layoutParams)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), minValue_(minValue), maxValue_(maxValue), step_(step), units_(units), screenManager_(screenManager) {
+ fmt_ = "%2.2f";
+ OnClick.Handle(this, &PopupSliderChoiceFloat::HandleClick);
+}
+
+EventReturn PopupSliderChoice::HandleClick(EventParams &e) {
+ restoreFocus_ = HasFocus();
+
+ SliderPopupScreen *popupScreen = new SliderPopupScreen(value_, minValue_, maxValue_, ChopTitle(text_), step_, units_);
+ if (!negativeLabel_.empty())
+ popupScreen->SetNegativeDisable(negativeLabel_);
+ popupScreen->OnChange.Handle(this, &PopupSliderChoice::HandleChange);
+ if (e.v)
+ popupScreen->SetPopupOrigin(e.v);
+ screenManager_->push(popupScreen);
+ return EVENT_DONE;
+}
+
+EventReturn PopupSliderChoice::HandleChange(EventParams &e) {
+ e.v = this;
+ OnChange.Trigger(e);
+
+ if (restoreFocus_) {
+ SetFocusedView(this);
+ }
+ return EVENT_DONE;
+}
+
+std::string PopupSliderChoice::ValueText() const {
+ // Always good to have space for Unicode.
+ char temp[256];
+ if (zeroLabel_.size() && *value_ == 0) {
+ strcpy(temp, zeroLabel_.c_str());
+ } else if (negativeLabel_.size() && *value_ < 0) {
+ strcpy(temp, negativeLabel_.c_str());
+ } else {
+ sprintf(temp, fmt_, *value_);
+ }
+
+ return temp;
+}
+
+EventReturn PopupSliderChoiceFloat::HandleClick(EventParams &e) {
+ restoreFocus_ = HasFocus();
+
+ SliderFloatPopupScreen *popupScreen = new SliderFloatPopupScreen(value_, minValue_, maxValue_, ChopTitle(text_), step_, units_, liveUpdate_);
+ popupScreen->OnChange.Handle(this, &PopupSliderChoiceFloat::HandleChange);
+ popupScreen->SetHasDropShadow(hasDropShadow_);
+ if (e.v)
+ popupScreen->SetPopupOrigin(e.v);
+ screenManager_->push(popupScreen);
+ return EVENT_DONE;
+}
+
+EventReturn PopupSliderChoiceFloat::HandleChange(EventParams &e) {
+ e.v = this;
+ OnChange.Trigger(e);
+
+ if (restoreFocus_) {
+ SetFocusedView(this);
+ }
+ return EVENT_DONE;
+}
+
+std::string PopupSliderChoiceFloat::ValueText() const {
+ char temp[256];
+ if (zeroLabel_.size() && *value_ == 0.0f) {
+ strcpy(temp, zeroLabel_.c_str());
+ } else {
+ sprintf(temp, fmt_, *value_);
+ }
+
+ return temp;
+}
+
+EventReturn SliderPopupScreen::OnDecrease(EventParams ¶ms) {
+ if (sliderValue_ > minValue_ && sliderValue_ < maxValue_) {
+ sliderValue_ = step_ * floor((sliderValue_ / step_) + 0.5f);
+ }
+ sliderValue_ -= step_;
+ slider_->Clamp();
+ changing_ = true;
+ char temp[64];
+ sprintf(temp, "%d", sliderValue_);
+ edit_->SetText(temp);
+ changing_ = false;
+ disabled_ = false;
+ return EVENT_DONE;
+}
+
+EventReturn SliderPopupScreen::OnIncrease(EventParams ¶ms) {
+ if (sliderValue_ > minValue_ && sliderValue_ < maxValue_) {
+ sliderValue_ = step_ * floor((sliderValue_ / step_) + 0.5f);
+ }
+ sliderValue_ += step_;
+ slider_->Clamp();
+ changing_ = true;
+ char temp[64];
+ sprintf(temp, "%d", sliderValue_);
+ edit_->SetText(temp);
+ changing_ = false;
+ disabled_ = false;
+ return EVENT_DONE;
+}
+
+EventReturn SliderPopupScreen::OnSliderChange(EventParams ¶ms) {
+ changing_ = true;
+ char temp[64];
+ sprintf(temp, "%d", sliderValue_);
+ edit_->SetText(temp);
+ changing_ = false;
+ disabled_ = false;
+ return EVENT_DONE;
+}
+
+EventReturn SliderPopupScreen::OnTextChange(EventParams ¶ms) {
+ if (!changing_) {
+ sliderValue_ = atoi(edit_->GetText().c_str());
+ disabled_ = false;
+ slider_->Clamp();
+ }
+ return EVENT_DONE;
+}
+
+void SliderPopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
+ using namespace UI;
+ UIContext &dc = *screenManager()->getUIContext();
+
+ sliderValue_ = *value_;
+ if (disabled_ && sliderValue_ < 0)
+ sliderValue_ = 0;
+ LinearLayout *vert = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(UI::Margins(10, 10))));
+ slider_ = new Slider(&sliderValue_, minValue_, maxValue_, new LinearLayoutParams(UI::Margins(10, 10)));
+ slider_->OnChange.Handle(this, &SliderPopupScreen::OnSliderChange);
+ vert->Add(slider_);
+
+ LinearLayout *lin = vert->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(UI::Margins(10, 10))));
+ lin->Add(new Button(" - "))->OnClick.Handle(this, &SliderPopupScreen::OnDecrease);
+ lin->Add(new Button(" + "))->OnClick.Handle(this, &SliderPopupScreen::OnIncrease);
+
+ char temp[64];
+ sprintf(temp, "%d", sliderValue_);
+ edit_ = new TextEdit(temp, Title(), "", new LinearLayoutParams(10.0f));
+ edit_->SetMaxLen(16);
+ edit_->SetTextColor(dc.theme->itemStyle.fgColor);
+ edit_->SetTextAlign(FLAG_DYNAMIC_ASCII);
+ edit_->OnTextChange.Handle(this, &SliderPopupScreen::OnTextChange);
+ changing_ = false;
+ lin->Add(edit_);
+
+ if (!units_.empty())
+ lin->Add(new TextView(units_, new LinearLayoutParams(10.0f)))->SetTextColor(dc.theme->itemStyle.fgColor);
+
+ if (!negativeLabel_.empty())
+ vert->Add(new CheckBox(&disabled_, negativeLabel_));
+
+ if (IsFocusMovementEnabled())
+ UI::SetFocusedView(slider_);
+}
+
+void SliderFloatPopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
+ using namespace UI;
+ UIContext &dc = *screenManager()->getUIContext();
+
+ sliderValue_ = *value_;
+ LinearLayout *vert = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(UI::Margins(10, 10))));
+ slider_ = new SliderFloat(&sliderValue_, minValue_, maxValue_, new LinearLayoutParams(UI::Margins(10, 10)));
+ slider_->OnChange.Handle(this, &SliderFloatPopupScreen::OnSliderChange);
+ vert->Add(slider_);
+
+ LinearLayout *lin = vert->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(UI::Margins(10, 10))));
+ lin->Add(new Button(" - "))->OnClick.Handle(this, &SliderFloatPopupScreen::OnDecrease);
+ lin->Add(new Button(" + "))->OnClick.Handle(this, &SliderFloatPopupScreen::OnIncrease);
+
+ char temp[64];
+ sprintf(temp, "%0.3f", sliderValue_);
+ edit_ = new TextEdit(temp, Title(), "", new LinearLayoutParams(10.0f));
+ edit_->SetMaxLen(16);
+ edit_->SetTextColor(dc.theme->itemStyle.fgColor);
+ edit_->SetTextAlign(FLAG_DYNAMIC_ASCII);
+ edit_->OnTextChange.Handle(this, &SliderFloatPopupScreen::OnTextChange);
+ changing_ = false;
+ lin->Add(edit_);
+ if (!units_.empty())
+ lin->Add(new TextView(units_, new LinearLayoutParams(10.0f)))->SetTextColor(dc.theme->itemStyle.fgColor);
+
+ // slider_ = parent->Add(new SliderFloat(&sliderValue_, minValue_, maxValue_, new LinearLayoutParams(UI::Margins(10, 5))));
+ if (IsFocusMovementEnabled())
+ UI::SetFocusedView(slider_);
+}
+
+EventReturn SliderFloatPopupScreen::OnDecrease(EventParams ¶ms) {
+ if (sliderValue_ > minValue_ && sliderValue_ < maxValue_) {
+ sliderValue_ = step_ * floor((sliderValue_ / step_) + 0.5f);
+ }
+ sliderValue_ -= step_;
+ slider_->Clamp();
+ changing_ = true;
+ char temp[64];
+ sprintf(temp, "%0.3f", sliderValue_);
+ edit_->SetText(temp);
+ changing_ = false;
+ if (liveUpdate_) {
+ *value_ = sliderValue_;
+ }
+ return EVENT_DONE;
+}
+
+EventReturn SliderFloatPopupScreen::OnIncrease(EventParams ¶ms) {
+ if (sliderValue_ > minValue_ && sliderValue_ < maxValue_) {
+ sliderValue_ = step_ * floor((sliderValue_ / step_) + 0.5f);
+ }
+ sliderValue_ += step_;
+ slider_->Clamp();
+ changing_ = true;
+ char temp[64];
+ sprintf(temp, "%0.3f", sliderValue_);
+ edit_->SetText(temp);
+ changing_ = false;
+ if (liveUpdate_) {
+ *value_ = sliderValue_;
+ }
+ return EVENT_DONE;
+}
+
+EventReturn SliderFloatPopupScreen::OnSliderChange(EventParams ¶ms) {
+ changing_ = true;
+ char temp[64];
+ sprintf(temp, "%0.3f", sliderValue_);
+ edit_->SetText(temp);
+ changing_ = false;
+ if (liveUpdate_) {
+ *value_ = sliderValue_;
+ }
+ return EVENT_DONE;
+}
+
+EventReturn SliderFloatPopupScreen::OnTextChange(EventParams ¶ms) {
+ if (!changing_) {
+ sliderValue_ = atof(edit_->GetText().c_str());
+ slider_->Clamp();
+ if (liveUpdate_) {
+ *value_ = sliderValue_;
+ }
+ }
+ return EVENT_DONE;
+}
+
+void SliderPopupScreen::OnCompleted(DialogResult result) {
+ if (result == DR_OK) {
+ *value_ = disabled_ ? -1 : sliderValue_;
+ EventParams e{};
+ e.v = nullptr;
+ e.a = *value_;
+ OnChange.Trigger(e);
+ }
+}
+
+void SliderFloatPopupScreen::OnCompleted(DialogResult result) {
+ if (result == DR_OK) {
+ *value_ = sliderValue_;
+ EventParams e{};
+ e.v = nullptr;
+ e.a = (int)*value_;
+ e.f = *value_;
+ OnChange.Trigger(e);
+ } else {
+ *value_ = originalValue_;
+ }
+}
+
+PopupTextInputChoice::PopupTextInputChoice(std::string *value, const std::string &title, const std::string &placeholder, int maxLen, ScreenManager *screenManager, LayoutParams *layoutParams)
+ : AbstractChoiceWithValueDisplay(title, layoutParams), screenManager_(screenManager), value_(value), placeHolder_(placeholder), maxLen_(maxLen) {
+ OnClick.Handle(this, &PopupTextInputChoice::HandleClick);
+}
+
+EventReturn PopupTextInputChoice::HandleClick(EventParams &e) {
+ restoreFocus_ = HasFocus();
+
+ TextEditPopupScreen *popupScreen = new TextEditPopupScreen(value_, placeHolder_, ChopTitle(text_), maxLen_);
+ popupScreen->OnChange.Handle(this, &PopupTextInputChoice::HandleChange);
+ if (e.v)
+ popupScreen->SetPopupOrigin(e.v);
+ screenManager_->push(popupScreen);
+ return EVENT_DONE;
+}
+
+std::string PopupTextInputChoice::ValueText() const {
+ return *value_;
+}
+
+EventReturn PopupTextInputChoice::HandleChange(EventParams &e) {
+ e.v = this;
+ OnChange.Trigger(e);
+
+ if (restoreFocus_) {
+ SetFocusedView(this);
+ }
+ return EVENT_DONE;
+}
+
+void TextEditPopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
+ using namespace UI;
+ UIContext &dc = *screenManager()->getUIContext();
+
+ textEditValue_ = *value_;
+ LinearLayout *lin = parent->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams((UI::Size)300, WRAP_CONTENT)));
+ edit_ = new TextEdit(textEditValue_, Title(), placeholder_, new LinearLayoutParams(1.0f));
+ edit_->SetMaxLen(maxLen_);
+ edit_->SetTextColor(dc.theme->popupStyle.fgColor);
+ lin->Add(edit_);
+
+ UI::SetFocusedView(edit_);
+}
+
+void TextEditPopupScreen::OnCompleted(DialogResult result) {
+ if (result == DR_OK) {
+ *value_ = StripSpaces(edit_->GetText());
+ EventParams e{};
+ e.v = edit_;
+ OnChange.Trigger(e);
+ }
+}
+
+void AbstractChoiceWithValueDisplay::GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const {
+ const std::string valueText = ValueText();
+ int paddingX = 12;
+ // Assume we want at least 20% of the size for the label, at a minimum.
+ float availWidth = (horiz.size - paddingX * 2) * (text_.empty() ? 1.0f : 0.8f);
+ if (availWidth < 0) {
+ availWidth = 65535.0f;
+ }
+ float scale = CalculateValueScale(dc, valueText, availWidth);
+ Bounds availBounds(0, 0, availWidth, vert.size);
+
+ float valueW, valueH;
+ dc.MeasureTextRect(dc.theme->uiFont, scale, scale, valueText.c_str(), (int)valueText.size(), availBounds, &valueW, &valueH, ALIGN_RIGHT | ALIGN_VCENTER | FLAG_WRAP_TEXT);
+ valueW += paddingX;
+
+ // Give the choice itself less space to grow in, so it shrinks if needed.
+ // MeasureSpec horizLabel = horiz;
+ // horizLabel.size -= valueW;
+ Choice::GetContentDimensionsBySpec(dc, horiz, vert, w, h);
+
+ w += valueW;
+ // Fill out anyway if there's space.
+ if (horiz.type == AT_MOST && w < horiz.size) {
+ w = horiz.size;
+ }
+ h = std::max(h, valueH);
+}
+
+void AbstractChoiceWithValueDisplay::Draw(UIContext &dc) {
+ Style style = dc.theme->itemStyle;
+ if (!IsEnabled()) {
+ style = dc.theme->itemDisabledStyle;
+ }
+ if (HasFocus()) {
+ style = dc.theme->itemFocusedStyle;
+ }
+ if (down_) {
+ style = dc.theme->itemDownStyle;
+ }
+ int paddingX = 12;
+ dc.SetFontStyle(dc.theme->uiFont);
+
+ const std::string valueText = ValueText();
+
+ // If there is a label, assume we want at least 20% of the size for it, at a minimum.
+
+ if (!text_.empty()) {
+ float availWidth = (bounds_.w - paddingX * 2) * 0.8f;
+ float scale = CalculateValueScale(dc, valueText, availWidth);
+
+ float w, h;
+ Bounds availBounds(0, 0, availWidth, bounds_.h);
+ dc.MeasureTextRect(dc.theme->uiFont, scale, scale, valueText.c_str(), (int)valueText.size(), availBounds, &w, &h, ALIGN_RIGHT | ALIGN_VCENTER | FLAG_WRAP_TEXT);
+ textPadding_.right = w + paddingX;
+
+ Choice::Draw(dc);
+ dc.SetFontScale(scale, scale);
+ Bounds valueBounds(bounds_.x2() - textPadding_.right, bounds_.y, w, bounds_.h);
+ dc.DrawTextRect(valueText.c_str(), valueBounds, style.fgColor, ALIGN_RIGHT | ALIGN_VCENTER | FLAG_WRAP_TEXT);
+ dc.SetFontScale(1.0f, 1.0f);
+ } else {
+ Choice::Draw(dc);
+ float scale = CalculateValueScale(dc, valueText, bounds_.w);
+ dc.SetFontScale(scale, scale);
+ dc.DrawTextRect(valueText.c_str(), bounds_.Expand(-paddingX, 0.0f), style.fgColor, ALIGN_LEFT | ALIGN_VCENTER | FLAG_WRAP_TEXT);
+ dc.SetFontScale(1.0f, 1.0f);
+ }
+}
+
+float AbstractChoiceWithValueDisplay::CalculateValueScale(const UIContext &dc, const std::string &valueText, float availWidth) const {
+ float actualWidth, actualHeight;
+ Bounds availBounds(0, 0, availWidth, bounds_.h);
+ dc.MeasureTextRect(dc.theme->uiFont, 1.0f, 1.0f, valueText.c_str(), (int)valueText.size(), availBounds, &actualWidth, &actualHeight);
+ if (actualWidth > availWidth) {
+ return std::max(0.8f, availWidth / actualWidth);
+ }
+ return 1.0f;
+}
+
+std::string ChoiceWithValueDisplay::ValueText() const {
+ auto category = GetI18NCategory(category_);
+ std::ostringstream valueText;
+ if (translateCallback_ && sValue_) {
+ valueText << translateCallback_(sValue_->c_str());
+ } else if (sValue_ != nullptr) {
+ if (category)
+ valueText << category->T(*sValue_);
+ else
+ valueText << *sValue_;
+ } else if (iValue_ != nullptr) {
+ valueText << *iValue_;
+ }
+
+ return valueText.str();
+}
+
+} // namespace
diff --git a/Common/UI/PopupScreens.h b/Common/UI/PopupScreens.h
new file mode 100644
index 0000000000..403b3955bf
--- /dev/null
+++ b/Common/UI/PopupScreens.h
@@ -0,0 +1,388 @@
+#pragma once
+
+#include "Common/UI/UIScreen.h"
+#include "Common/UI/UI.h"
+#include "Common/UI/View.h"
+#include "Common/UI/ScrollView.h"
+
+namespace UI {
+
+class ListPopupScreen : public PopupScreen {
+public:
+ ListPopupScreen(std::string title) : PopupScreen(title) {}
+ ListPopupScreen(std::string title, const std::vector &items, int selected, std::function callback, bool showButtons = false)
+ : PopupScreen(title, "OK", "Cancel"), adaptor_(items, selected), callback_(callback), showButtons_(showButtons) {
+ }
+ ListPopupScreen(std::string title, const std::vector &items, int selected, bool showButtons = false)
+ : PopupScreen(title, "OK", "Cancel"), adaptor_(items, selected), showButtons_(showButtons) {
+ }
+
+ int GetChoice() const {
+ return listView_->GetSelected();
+ }
+ std::string GetChoiceString() const {
+ return adaptor_.GetTitle(listView_->GetSelected());
+ }
+ void SetHiddenChoices(std::set hidden) {
+ hidden_ = hidden;
+ }
+ const char *tag() const override { return "listpopup"; }
+
+ UI::Event OnChoice;
+
+protected:
+ bool FillVertical() const override { return false; }
+ bool ShowButtons() const override { return showButtons_; }
+ void CreatePopupContents(UI::ViewGroup *parent) override;
+ UI::StringVectorListAdaptor adaptor_;
+ UI::ListView *listView_ = nullptr;
+
+private:
+ UI::EventReturn OnListChoice(UI::EventParams &e);
+
+ std::function callback_;
+ bool showButtons_ = false;
+ std::set hidden_;
+};
+
+class MessagePopupScreen : public PopupScreen {
+public:
+ MessagePopupScreen(std::string title, std::string message, std::string button1, std::string button2, std::function callback)
+ : PopupScreen(title, button1, button2), message_(message), callback_(callback) {}
+ UI::Event OnChoice;
+
+protected:
+ bool FillVertical() const override { return false; }
+ bool ShowButtons() const override { return true; }
+ void CreatePopupContents(UI::ViewGroup *parent) override;
+
+private:
+ void OnCompleted(DialogResult result) override;
+ std::string message_;
+ std::function callback_;
+};
+
+class SliderPopupScreen : public PopupScreen {
+public:
+ SliderPopupScreen(int *value, int minValue, int maxValue, const std::string &title, int step = 1, const std::string &units = "")
+ : PopupScreen(title, "OK", "Cancel"), units_(units), value_(value), minValue_(minValue), maxValue_(maxValue), step_(step) {}
+ void CreatePopupContents(ViewGroup *parent) override;
+
+ void SetNegativeDisable(const std::string &str) {
+ negativeLabel_ = str;
+ disabled_ = *value_ < 0;
+ }
+
+ const char *tag() const override { return "SliderPopup"; }
+
+ Event OnChange;
+
+private:
+ EventReturn OnDecrease(EventParams ¶ms);
+ EventReturn OnIncrease(EventParams ¶ms);
+ EventReturn OnTextChange(EventParams ¶ms);
+ EventReturn OnSliderChange(EventParams ¶ms);
+ void OnCompleted(DialogResult result) override;
+ Slider *slider_ = nullptr;
+ UI::TextEdit *edit_ = nullptr;
+ std::string units_;
+ std::string negativeLabel_;
+ int *value_;
+ int sliderValue_ = 0;
+ int minValue_;
+ int maxValue_;
+ int step_;
+ bool changing_ = false;
+ bool disabled_ = false;
+};
+
+class SliderFloatPopupScreen : public PopupScreen {
+public:
+ SliderFloatPopupScreen(float *value, float minValue, float maxValue, const std::string &title, float step = 1.0f, const std::string &units = "", bool liveUpdate = false)
+ : PopupScreen(title, "OK", "Cancel"), units_(units), value_(value), originalValue_(*value), minValue_(minValue), maxValue_(maxValue), step_(step), changing_(false), liveUpdate_(liveUpdate) {}
+ void CreatePopupContents(UI::ViewGroup *parent) override;
+
+ const char *tag() const override { return "SliderFloatPopup"; }
+
+ Event OnChange;
+
+private:
+ EventReturn OnIncrease(EventParams ¶ms);
+ EventReturn OnDecrease(EventParams ¶ms);
+ EventReturn OnTextChange(EventParams ¶ms);
+ EventReturn OnSliderChange(EventParams ¶ms);
+ void OnCompleted(DialogResult result) override;
+ UI::SliderFloat *slider_ = nullptr;
+ UI::TextEdit *edit_ = nullptr;
+ std::string units_ = nullptr;
+ float sliderValue_;
+ float originalValue_;
+ float *value_;
+ float minValue_;
+ float maxValue_;
+ float step_;
+ bool changing_;
+ bool liveUpdate_;
+};
+
+class TextEditPopupScreen : public PopupScreen {
+public:
+ TextEditPopupScreen(std::string *value, const std::string &placeholder, const std::string &title, int maxLen)
+ : PopupScreen(title, "OK", "Cancel"), value_(value), placeholder_(placeholder), maxLen_(maxLen) {}
+ void CreatePopupContents(ViewGroup *parent) override;
+
+ const char *tag() const override { return "TextEditPopup"; }
+
+ Event OnChange;
+
+private:
+ void OnCompleted(DialogResult result) override;
+ TextEdit *edit_ = nullptr;
+ std::string *value_;
+ std::string textEditValue_;
+ std::string placeholder_;
+ int maxLen_;
+};
+
+struct ContextMenuItem {
+ const char *text;
+ const char *imageID;
+};
+
+// Once a selection has been made,
+class PopupContextMenuScreen : public PopupScreen {
+public:
+ PopupContextMenuScreen(const ContextMenuItem *items, size_t itemCount, I18NCategory *category, UI::View *sourceView);
+ void CreatePopupContents(ViewGroup *parent) override;
+
+ const char *tag() const override { return "ContextMenuPopup"; }
+
+ void SetEnabled(size_t index, bool enabled) {
+ enabled_[index] = enabled;
+ }
+
+ UI::Event OnChoice;
+
+protected:
+ bool HasTitleBar() const override { return false; }
+
+private:
+ const ContextMenuItem *items_;
+ size_t itemCount_;
+ I18NCategory *category_;
+ UI::View *sourceView_;
+ std::vector enabled_;
+};
+
+class AbstractChoiceWithValueDisplay : public UI::Choice {
+public:
+ AbstractChoiceWithValueDisplay(const std::string &text, LayoutParams *layoutParams = nullptr)
+ : Choice(text, layoutParams) {
+ }
+
+ void Draw(UIContext &dc) override;
+ void GetContentDimensionsBySpec(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert, float &w, float &h) const override;
+
+protected:
+ virtual std::string ValueText() const = 0;
+
+ float CalculateValueScale(const UIContext &dc, const std::string &valueText, float availWidth) const;
+};
+
+// Reads and writes value to determine the current selection.
+class PopupMultiChoice : public AbstractChoiceWithValueDisplay {
+public:
+ PopupMultiChoice(int *value, const std::string &text, const char **choices, int minVal, int numChoices,
+ const char *category, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), value_(value), choices_(choices), minVal_(minVal), numChoices_(numChoices),
+ category_(category), screenManager_(screenManager) {
+ if (*value >= numChoices + minVal)
+ *value = numChoices + minVal - 1;
+ if (*value < minVal)
+ *value = minVal;
+ OnClick.Handle(this, &PopupMultiChoice::HandleClick);
+ UpdateText();
+ }
+
+ void Update() override;
+
+ void HideChoice(int c) {
+ hidden_.insert(c);
+ }
+
+ UI::Event OnChoice;
+
+protected:
+ std::string ValueText() const override;
+
+ int *value_;
+ const char **choices_;
+ int minVal_;
+ int numChoices_;
+ void UpdateText();
+
+private:
+ UI::EventReturn HandleClick(UI::EventParams &e);
+
+ void ChoiceCallback(int num);
+ virtual void PostChoiceCallback(int num) {}
+
+ const char *category_;
+ ScreenManager *screenManager_;
+ std::string valueText_;
+ bool restoreFocus_ = false;
+ std::set hidden_;
+};
+
+// Allows passing in a dynamic vector of strings. Saves the string.
+class PopupMultiChoiceDynamic : public PopupMultiChoice {
+public:
+ PopupMultiChoiceDynamic(std::string *value, const std::string &text, std::vector choices,
+ const char *category, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr)
+ : UI::PopupMultiChoice(&valueInt_, text, nullptr, 0, (int)choices.size(), category, screenManager, layoutParams),
+ valueStr_(value) {
+ choices_ = new const char *[numChoices_];
+ valueInt_ = 0;
+ for (int i = 0; i < numChoices_; i++) {
+ choices_[i] = new char[choices[i].size() + 1];
+ memcpy((char *)choices_[i], choices[i].c_str(), choices[i].size() + 1);
+ if (*value == choices_[i])
+ valueInt_ = i;
+ }
+ value_ = &valueInt_;
+ UpdateText();
+ }
+ ~PopupMultiChoiceDynamic() {
+ for (int i = 0; i < numChoices_; i++) {
+ delete[] choices_[i];
+ }
+ delete[] choices_;
+ }
+
+protected:
+ void PostChoiceCallback(int num) override {
+ *valueStr_ = choices_[num];
+ }
+
+private:
+ int valueInt_;
+ std::string *valueStr_;
+};
+
+class PopupSliderChoice : public AbstractChoiceWithValueDisplay {
+public:
+ PopupSliderChoice(int *value, int minValue, int maxValue, const std::string &text, ScreenManager *screenManager, const std::string &units = "", LayoutParams *layoutParams = 0);
+ PopupSliderChoice(int *value, int minValue, int maxValue, const std::string &text, int step, ScreenManager *screenManager, const std::string &units = "", LayoutParams *layoutParams = 0);
+
+ void SetFormat(const char *fmt) {
+ fmt_ = fmt;
+ }
+ void SetZeroLabel(const std::string &str) {
+ zeroLabel_ = str;
+ }
+ void SetNegativeDisable(const std::string &str) {
+ negativeLabel_ = str;
+ }
+
+ Event OnChange;
+
+protected:
+ std::string ValueText() const override;
+
+private:
+ EventReturn HandleClick(EventParams &e);
+ EventReturn HandleChange(EventParams &e);
+
+ int *value_;
+ int minValue_;
+ int maxValue_;
+ int step_;
+ const char *fmt_;
+ std::string zeroLabel_;
+ std::string negativeLabel_;
+ std::string units_;
+ ScreenManager *screenManager_;
+ bool restoreFocus_ = false;
+};
+
+class PopupSliderChoiceFloat : public AbstractChoiceWithValueDisplay {
+public:
+ PopupSliderChoiceFloat(float *value, float minValue, float maxValue, const std::string &text, ScreenManager *screenManager, const std::string &units = "", LayoutParams *layoutParams = 0);
+ PopupSliderChoiceFloat(float *value, float minValue, float maxValue, const std::string &text, float step, ScreenManager *screenManager, const std::string &units = "", LayoutParams *layoutParams = 0);
+
+ void SetFormat(const char *fmt) {
+ fmt_ = fmt;
+ }
+ void SetZeroLabel(const std::string &str) {
+ zeroLabel_ = str;
+ }
+ void SetLiveUpdate(bool update) {
+ liveUpdate_ = update;
+ }
+ void SetHasDropShadow(bool has) {
+ hasDropShadow_ = has;
+ }
+
+ Event OnChange;
+
+protected:
+ std::string ValueText() const override;
+
+private:
+ EventReturn HandleClick(EventParams &e);
+ EventReturn HandleChange(EventParams &e);
+ float *value_;
+ float minValue_;
+ float maxValue_;
+ float step_;
+ const char *fmt_;
+ std::string zeroLabel_;
+ std::string units_;
+ ScreenManager *screenManager_;
+ bool restoreFocus_ = false;
+ bool liveUpdate_ = false;
+ bool hasDropShadow_ = true;
+};
+
+class PopupTextInputChoice : public AbstractChoiceWithValueDisplay {
+public:
+ PopupTextInputChoice(std::string *value, const std::string &title, const std::string &placeholder, int maxLen, ScreenManager *screenManager, LayoutParams *layoutParams = 0);
+
+ Event OnChange;
+
+protected:
+ std::string ValueText() const override;
+
+private:
+ EventReturn HandleClick(EventParams &e);
+ EventReturn HandleChange(EventParams &e);
+ ScreenManager *screenManager_;
+ std::string *value_;
+ std::string placeHolder_;
+ std::string defaultText_;
+ int maxLen_;
+ bool restoreFocus_;
+};
+
+class ChoiceWithValueDisplay : public AbstractChoiceWithValueDisplay {
+public:
+ ChoiceWithValueDisplay(int *value, const std::string &text, LayoutParams *layoutParams = 0)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), iValue_(value) {}
+
+ ChoiceWithValueDisplay(std::string *value, const std::string &text, const char *category, LayoutParams *layoutParams = 0)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), sValue_(value), category_(category) {}
+
+ ChoiceWithValueDisplay(std::string *value, const std::string &text, std::string(*translateCallback)(const char *value), LayoutParams *layoutParams = 0)
+ : AbstractChoiceWithValueDisplay(text, layoutParams), sValue_(value), translateCallback_(translateCallback) {
+ }
+
+private:
+ std::string ValueText() const override;
+
+ std::string *sValue_ = nullptr;
+ int *iValue_ = nullptr;
+ const char *category_ = nullptr;
+ std::string(*translateCallback_)(const char *value) = nullptr;
+};
+
+} // namespace UI
diff --git a/Common/UI/Screen.cpp b/Common/UI/Screen.cpp
index 866959ebe7..d06a26cd13 100644
--- a/Common/UI/Screen.cpp
+++ b/Common/UI/Screen.cpp
@@ -2,6 +2,7 @@
#include "Common/Input/InputState.h"
#include "Common/UI/Root.h"
#include "Common/UI/Screen.h"
+#include "Common/UI/ScrollView.h"
#include "Common/UI/UI.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
diff --git a/Common/UI/ScrollView.cpp b/Common/UI/ScrollView.cpp
new file mode 100644
index 0000000000..0158839e82
--- /dev/null
+++ b/Common/UI/ScrollView.cpp
@@ -0,0 +1,502 @@
+#include "Common/UI/Context.h"
+#include "Common/UI/ScrollView.h"
+#include "Common/Data/Text/I18n.h"
+
+namespace UI {
+
+float ScrollView::lastScrollPosX = 0;
+float ScrollView::lastScrollPosY = 0;
+
+ScrollView::~ScrollView() {
+ lastScrollPosX = 0;
+ lastScrollPosY = 0;
+}
+
+void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
+ // Respect margins
+ Margins margins;
+ if (views_.size()) {
+ const LinearLayoutParams *linLayoutParams = views_[0]->GetLayoutParams()->As();
+ if (linLayoutParams) {
+ margins = linLayoutParams->margins;
+ }
+ }
+
+ // The scroll view itself simply obeys its parent - but also tries to fit the child if possible.
+ MeasureBySpec(layoutParams_->width, horiz.size, horiz, &measuredWidth_);
+ MeasureBySpec(layoutParams_->height, vert.size, vert, &measuredHeight_);
+
+ if (views_.size()) {
+ if (orientation_ == ORIENT_HORIZONTAL) {
+ MeasureSpec v = MeasureSpec(AT_MOST, measuredHeight_ - margins.vert());
+ if (measuredHeight_ == 0.0f && (vert.type == UNSPECIFIED || layoutParams_->height == WRAP_CONTENT)) {
+ v.type = UNSPECIFIED;
+ }
+ views_[0]->Measure(dc, MeasureSpec(UNSPECIFIED, measuredWidth_), v);
+ MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_);
+ if (layoutParams_->width == WRAP_CONTENT)
+ MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth(), horiz, &measuredWidth_);
+ } else {
+ MeasureSpec h = MeasureSpec(AT_MOST, measuredWidth_ - margins.horiz());
+ if (measuredWidth_ == 0.0f && (horiz.type == UNSPECIFIED || layoutParams_->width == WRAP_CONTENT)) {
+ h.type = UNSPECIFIED;
+ }
+ views_[0]->Measure(dc, h, MeasureSpec(UNSPECIFIED, measuredHeight_));
+ MeasureBySpec(layoutParams_->width, views_[0]->GetMeasuredWidth(), horiz, &measuredWidth_);
+ if (layoutParams_->height == WRAP_CONTENT)
+ MeasureBySpec(layoutParams_->height, views_[0]->GetMeasuredHeight(), vert, &measuredHeight_);
+ }
+ if (orientation_ == ORIENT_VERTICAL && vert.type != EXACTLY) {
+ float bestHeight = std::max(views_[0]->GetMeasuredHeight(), views_[0]->GetBounds().h);
+ if (vert.type == AT_MOST)
+ bestHeight = std::min(bestHeight, vert.size);
+
+ if (measuredHeight_ < bestHeight && layoutParams_->height < 0.0f) {
+ measuredHeight_ = bestHeight;
+ }
+ }
+ }
+}
+
+void ScrollView::Layout() {
+ if (!views_.size())
+ return;
+ Bounds scrolled;
+
+ // Respect margins
+ Margins margins;
+ const LinearLayoutParams *linLayoutParams = views_[0]->GetLayoutParams()->As();
+ if (linLayoutParams) {
+ margins = linLayoutParams->margins;
+ }
+
+ scrolled.w = views_[0]->GetMeasuredWidth() - margins.horiz();
+ scrolled.h = views_[0]->GetMeasuredHeight() - margins.vert();
+
+ layoutScrollPos_ = ClampedScrollPos(scrollPos_);
+
+ switch (orientation_) {
+ case ORIENT_HORIZONTAL:
+ if (scrolled.w != lastViewSize_) {
+ if (rememberPos_)
+ scrollPos_ = *rememberPos_;
+ lastViewSize_ = scrolled.w;
+ }
+ scrolled.x = bounds_.x - layoutScrollPos_;
+ scrolled.y = bounds_.y + margins.top;
+ break;
+ case ORIENT_VERTICAL:
+ if (scrolled.h != lastViewSize_) {
+ if (rememberPos_)
+ scrollPos_ = *rememberPos_;
+ lastViewSize_ = scrolled.h;
+ }
+ scrolled.x = bounds_.x + margins.left;
+ scrolled.y = bounds_.y - layoutScrollPos_;
+ break;
+ }
+
+ views_[0]->SetBounds(scrolled);
+ views_[0]->Layout();
+}
+
+bool ScrollView::Key(const KeyInput &input) {
+ if (visibility_ != V_VISIBLE)
+ return ViewGroup::Key(input);
+
+ float scrollSpeed = 250;
+ switch (input.deviceId) {
+ case DEVICE_ID_XR_CONTROLLER_LEFT:
+ case DEVICE_ID_XR_CONTROLLER_RIGHT:
+ scrollSpeed = 50;
+ break;
+ }
+
+ if (input.flags & KEY_DOWN) {
+ if ((input.keyCode == NKCODE_EXT_MOUSEWHEEL_UP || input.keyCode == NKCODE_EXT_MOUSEWHEEL_DOWN) &&
+ (input.flags & KEY_HASWHEELDELTA)) {
+ scrollSpeed = (float)(short)(input.flags >> 16) * 1.25f; // Fudge factor
+ }
+
+ switch (input.keyCode) {
+ case NKCODE_EXT_MOUSEWHEEL_UP:
+ ScrollRelative(-scrollSpeed);
+ break;
+ case NKCODE_EXT_MOUSEWHEEL_DOWN:
+ ScrollRelative(scrollSpeed);
+ break;
+ }
+ }
+ return ViewGroup::Key(input);
+}
+
+const float friction = 0.92f;
+const float stop_threshold = 0.1f;
+
+bool ScrollView::Touch(const TouchInput &input) {
+ if ((input.flags & TOUCH_DOWN) && scrollTouchId_ == -1) {
+ scrollStart_ = scrollPos_;
+ inertia_ = 0.0f;
+ scrollTouchId_ = input.id;
+ }
+
+ Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
+
+ if ((input.flags & TOUCH_UP) && input.id == scrollTouchId_) {
+ float info[4];
+ if (gesture_.GetGestureInfo(gesture, input.id, info)) {
+ inertia_ = info[1];
+ }
+ scrollTouchId_ = -1;
+ }
+
+ TouchInput input2;
+ if (CanScroll()) {
+ input2 = gesture_.Update(input, bounds_);
+ float info[4];
+ if (input.id == scrollTouchId_ && gesture_.GetGestureInfo(gesture, input.id, info) && !(input.flags & TOUCH_DOWN)) {
+ float pos = scrollStart_ - info[0];
+ scrollPos_ = pos;
+ scrollTarget_ = pos;
+ scrollToTarget_ = false;
+ }
+ } else {
+ input2 = input;
+ scrollTarget_ = scrollPos_;
+ scrollToTarget_ = false;
+ }
+
+ if (!(input.flags & TOUCH_DOWN) || bounds_.Contains(input.x, input.y)) {
+ return ViewGroup::Touch(input2);
+ } else {
+ return false;
+ }
+}
+
+void ScrollView::Draw(UIContext &dc) {
+ if (!views_.size()) {
+ ViewGroup::Draw(dc);
+ return;
+ }
+
+ dc.PushScissor(bounds_);
+ dc.FillRect(bg_, bounds_);
+
+ // For debugging layout issues, this can be useful.
+ // dc.FillRect(Drawable(0x60FF00FF), bounds_);
+ views_[0]->Draw(dc);
+ dc.PopScissor();
+
+ float childHeight = views_[0]->GetBounds().h;
+ float scrollMax = std::max(0.0f, childHeight - bounds_.h);
+
+ float ratio = bounds_.h / std::max(0.01f, views_[0]->GetBounds().h);
+
+ float bobWidth = 5;
+ if (ratio < 1.0f && scrollMax > 0.0f) {
+ float bobHeight = ratio * bounds_.h;
+ float bobOffset = (ClampedScrollPos(scrollPos_) / scrollMax) * (bounds_.h - bobHeight);
+
+ Bounds bob(bounds_.x2() - bobWidth, bounds_.y + bobOffset, bobWidth, bobHeight);
+ dc.FillRect(Drawable(0x80FFFFFF), bob);
+ }
+}
+
+bool ScrollView::SubviewFocused(View *view) {
+ if (!ViewGroup::SubviewFocused(view))
+ return false;
+
+ const Bounds &vBounds = view->GetBounds();
+
+ // Scroll so that the focused view is visible, and a bit more so that headers etc gets visible too, in most cases.
+ const float overscroll = std::min(view->GetBounds().h / 1.5f, GetBounds().h / 4.0f);
+
+ float pos = ClampedScrollPos(scrollPos_);
+ float visibleSize = orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w;
+ float visibleEnd = scrollPos_ + visibleSize;
+
+ float viewStart = 0.0f, viewEnd = 0.0f;
+ switch (orientation_) {
+ case ORIENT_HORIZONTAL:
+ viewStart = layoutScrollPos_ + vBounds.x - bounds_.x;
+ viewEnd = layoutScrollPos_ + vBounds.x2() - bounds_.x;
+ break;
+ case ORIENT_VERTICAL:
+ viewStart = layoutScrollPos_ + vBounds.y - bounds_.y;
+ viewEnd = layoutScrollPos_ + vBounds.y2() - bounds_.y;
+ break;
+ }
+
+ if (viewEnd > visibleEnd) {
+ ScrollTo(viewEnd - visibleSize + overscroll);
+ } else if (viewStart < pos) {
+ ScrollTo(viewStart - overscroll);
+ }
+
+ return true;
+}
+
+NeighborResult ScrollView::FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) {
+ if (ContainsSubview(view) && views_[0]->IsViewGroup()) {
+ ViewGroup *vg = static_cast(views_[0]);
+ int found = -1;
+ for (int i = 0, n = vg->GetNumSubviews(); i < n; ++i) {
+ View *child = vg->GetViewByIndex(i);
+ if (child == view || child->ContainsSubview(view)) {
+ found = i;
+ break;
+ }
+ }
+
+ // Okay, the previously focused view is inside this.
+ if (found != -1) {
+ float mult = 0.0f;
+ switch (direction) {
+ case FOCUS_PREV_PAGE:
+ mult = -1.0f;
+ break;
+ case FOCUS_NEXT_PAGE:
+ mult = 1.0f;
+ break;
+ default:
+ break;
+ }
+
+ // Okay, now where is our ideal target?
+ Point targetPos = view->GetBounds().Center();
+ if (orientation_ == ORIENT_VERTICAL)
+ targetPos.y += mult * bounds_.h;
+ else
+ targetPos.x += mult * bounds_.x;
+
+ // Okay, which subview is closest to that?
+ best = vg->FindScrollNeighbor(view, targetPos, direction, best);
+ // Avoid reselecting the same view.
+ if (best.view == view)
+ best.view = nullptr;
+ return best;
+ }
+ }
+
+ return ViewGroup::FindScrollNeighbor(view, target, direction, best);
+}
+
+void ScrollView::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
+ ViewGroup::PersistData(status, anonId, storage);
+
+ std::string tag = Tag();
+ if (tag.empty()) {
+ tag = anonId;
+ }
+
+ PersistBuffer &buffer = storage["ScrollView::" + tag];
+ switch (status) {
+ case PERSIST_SAVE:
+ {
+ buffer.resize(1);
+ float pos = scrollToTarget_ ? scrollTarget_ : scrollPos_;
+ // Hmm, ugly... better buffer?
+ buffer[0] = *(int *)&pos;
+ }
+ break;
+
+ case PERSIST_RESTORE:
+ if (buffer.size() == 1) {
+ float pos = *(float *)&buffer[0];
+ scrollPos_ = pos;
+ scrollTarget_ = pos;
+ scrollToTarget_ = false;
+ }
+ break;
+ }
+}
+
+void ScrollView::SetVisibility(Visibility visibility) {
+ ViewGroup::SetVisibility(visibility);
+
+ if (visibility == V_GONE && !rememberPos_) {
+ // Since this is no longer shown, forget the scroll position.
+ // For example, this happens when switching tabs.
+ ScrollTo(0.0f);
+ }
+}
+
+void ScrollView::ScrollTo(float newScrollPos) {
+ scrollTarget_ = newScrollPos;
+ scrollToTarget_ = true;
+}
+
+void ScrollView::ScrollRelative(float distance) {
+ scrollTarget_ = scrollPos_ + distance;
+ scrollToTarget_ = true;
+}
+
+float ScrollView::ClampedScrollPos(float pos) {
+ if (!views_.size() || bounds_.h == 0.0f) {
+ return 0.0f;
+ }
+
+ float childSize = orientation_ == ORIENT_VERTICAL ? views_[0]->GetBounds().h : views_[0]->GetBounds().w;
+ float containerSize = (orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w);
+ float scrollMax = std::max(0.0f, childSize - containerSize);
+
+ Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
+
+ if (scrollTouchId_ >= 0 && gesture_.IsGestureActive(gesture, scrollTouchId_) && bounds_.h > 0.0f) {
+ float maxPull = bounds_.h * 0.1f;
+ if (pos < 0.0f) {
+ float dist = std::min(-pos * (1.0f / bounds_.h), 1.0f);
+ pull_ = -(sqrt(dist) * maxPull);
+ } else if (pos > scrollMax) {
+ float dist = std::min((pos - scrollMax) * (1.0f / bounds_.h), 1.0f);
+ pull_ = sqrt(dist) * maxPull;
+ } else {
+ pull_ = 0.0f;
+ }
+ }
+
+ if (pos < 0.0f && pos < pull_) {
+ pos = pull_;
+ }
+ if (pos > scrollMax && pos > scrollMax + pull_) {
+ pos = scrollMax + pull_;
+ }
+ if (childSize < containerSize &&alignOpposite_) {
+ pos = -(containerSize - childSize);
+ }
+ return pos;
+}
+
+void ScrollView::ScrollToBottom() {
+ float childHeight = views_[0]->GetBounds().h;
+ float scrollMax = std::max(0.0f, childHeight - bounds_.h);
+ scrollPos_ = scrollMax;
+ scrollTarget_ = scrollMax;
+}
+
+bool ScrollView::CanScroll() const {
+ if (!views_.size())
+ return false;
+ switch (orientation_) {
+ case ORIENT_VERTICAL:
+ return views_[0]->GetBounds().h > bounds_.h;
+ case ORIENT_HORIZONTAL:
+ return views_[0]->GetBounds().w > bounds_.w;
+ default:
+ return false;
+ }
+}
+
+void ScrollView::GetLastScrollPosition(float &x, float &y) {
+ x = lastScrollPosX;
+ y = lastScrollPosY;
+}
+
+void ScrollView::Update() {
+ if (visibility_ != V_VISIBLE) {
+ inertia_ = 0.0f;
+ }
+ ViewGroup::Update();
+ float oldPos = scrollPos_;
+
+ Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
+ gesture_.UpdateFrame();
+ if (scrollToTarget_) {
+ float target = ClampedScrollPos(scrollTarget_);
+
+ inertia_ = 0.0f;
+ if (fabsf(target - scrollPos_) < 0.5f) {
+ scrollPos_ = target;
+ scrollToTarget_ = false;
+ } else {
+ scrollPos_ += (target - scrollPos_) * 0.3f;
+ }
+ } else if (inertia_ != 0.0f && !gesture_.IsGestureActive(gesture, scrollTouchId_)) {
+ scrollPos_ -= inertia_;
+ inertia_ *= friction;
+ if (fabsf(inertia_) < stop_threshold)
+ inertia_ = 0.0f;
+ }
+
+ if (!gesture_.IsGestureActive(gesture, scrollTouchId_)) {
+ scrollPos_ = ClampedScrollPos(scrollPos_);
+
+ pull_ *= friction;
+ if (fabsf(pull_) < 0.01f) {
+ pull_ = 0.0f;
+ }
+ }
+
+ if (oldPos != scrollPos_)
+ orientation_ == ORIENT_HORIZONTAL ? lastScrollPosX = scrollPos_ : lastScrollPosY = scrollPos_;
+
+ // We load some lists asynchronously, so don't update the position until it's loaded.
+ if (rememberPos_ && ClampedScrollPos(scrollPos_) != ClampedScrollPos(*rememberPos_)) {
+ *rememberPos_ = scrollPos_;
+ }
+}
+
+ListView::ListView(ListAdaptor *a, std::set hidden, LayoutParams *layoutParams)
+ : ScrollView(ORIENT_VERTICAL, layoutParams), adaptor_(a), maxHeight_(0), hidden_(hidden) {
+
+ linLayout_ = new LinearLayout(ORIENT_VERTICAL);
+ linLayout_->SetSpacing(0.0f);
+ Add(linLayout_);
+ CreateAllItems();
+}
+
+void ListView::CreateAllItems() {
+ linLayout_->Clear();
+ // Let's not be clever yet, we'll just create them all up front and add them all in.
+ for (int i = 0; i < adaptor_->GetNumItems(); i++) {
+ if (hidden_.find(i) == hidden_.end()) {
+ View *v = linLayout_->Add(adaptor_->CreateItemView(i));
+ adaptor_->AddEventCallback(v, std::bind(&ListView::OnItemCallback, this, i, std::placeholders::_1));
+ }
+ }
+}
+
+void ListView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
+ ScrollView::Measure(dc, horiz, vert);
+ if (maxHeight_ > 0 && measuredHeight_ > maxHeight_) {
+ measuredHeight_ = maxHeight_;
+ }
+}
+
+std::string ListView::DescribeText() const {
+ auto u = GetI18NCategory("UI Elements");
+ return DescribeListOrdered(u->T("List:"));
+}
+
+EventReturn ListView::OnItemCallback(int num, EventParams &e) {
+ EventParams ev{};
+ ev.v = nullptr;
+ ev.a = num;
+ adaptor_->SetSelected(num);
+ OnChoice.Trigger(ev);
+ CreateAllItems();
+ return EVENT_DONE;
+}
+
+View *ChoiceListAdaptor::CreateItemView(int index) {
+ return new Choice(items_[index]);
+}
+
+bool ChoiceListAdaptor::AddEventCallback(View *view, std::function callback) {
+ Choice *choice = (Choice *)view;
+ choice->OnClick.Add(callback);
+ return EVENT_DONE;
+}
+
+
+View *StringVectorListAdaptor::CreateItemView(int index) {
+ return new Choice(items_[index], "", index == selected_);
+}
+
+bool StringVectorListAdaptor::AddEventCallback(View *view, std::function callback) {
+ Choice *choice = (Choice *)view;
+ choice->OnClick.Add(callback);
+ return EVENT_DONE;
+}
+
+}
diff --git a/Common/UI/ScrollView.h b/Common/UI/ScrollView.h
new file mode 100644
index 0000000000..d9092d3d34
--- /dev/null
+++ b/Common/UI/ScrollView.h
@@ -0,0 +1,133 @@
+#pragma once
+
+#include "Common/UI/View.h"
+#include "Common/UI/ViewGroup.h"
+
+namespace UI {
+
+// A scrollview usually contains just a single child - a linear layout or similar.
+class ScrollView : public ViewGroup {
+public:
+ ScrollView(Orientation orientation, LayoutParams *layoutParams = 0)
+ : ViewGroup(layoutParams), orientation_(orientation) {}
+ ~ScrollView();
+
+ void Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) override;
+ void Layout() override;
+
+ bool Key(const KeyInput &input) override;
+ bool Touch(const TouchInput &input) override;
+ void Draw(UIContext &dc) override;
+ std::string DescribeLog() const override { return "ScrollView: " + View::DescribeLog(); }
+
+ void ScrollTo(float newScrollPos);
+ void ScrollToBottom();
+ void ScrollRelative(float distance);
+ bool CanScroll() const;
+ void Update() override;
+
+ void RememberPosition(float *pos) {
+ rememberPos_ = pos;
+ ScrollTo(*pos);
+ }
+
+ // Get the last moved scroll view position
+ static void GetLastScrollPosition(float &x, float &y);
+
+ // Override so that we can scroll to the active one after moving the focus.
+ bool SubviewFocused(View *view) override;
+ void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override;
+ void SetVisibility(Visibility visibility) override;
+
+ // If the view is smaller than the scroll view, sets whether to align to the bottom/right instead of the left.
+ void SetAlignOpposite(bool alignOpposite) {
+ alignOpposite_ = alignOpposite;
+ }
+
+ NeighborResult FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) override;
+
+private:
+ float ClampedScrollPos(float pos);
+
+ GestureDetector gesture_;
+ Orientation orientation_;
+ float scrollPos_ = 0.0f;
+ float scrollStart_ = 0.0f;
+ float scrollTarget_ = 0.0f;
+ int scrollTouchId_ = -1;
+ bool scrollToTarget_ = false;
+ float layoutScrollPos_ = 0.0f;
+ float inertia_ = 0.0f;
+ float pull_ = 0.0f;
+ float lastViewSize_ = 0.0f;
+ float *rememberPos_ = nullptr;
+ bool alignOpposite_ = false;
+
+ static float lastScrollPosX;
+ static float lastScrollPosY;
+};
+
+// Yes, this feels a bit Java-ish...
+class ListAdaptor {
+public:
+ virtual ~ListAdaptor() {}
+ virtual View *CreateItemView(int index) = 0;
+ virtual int GetNumItems() = 0;
+ virtual bool AddEventCallback(View *view, std::function callback) { return false; }
+ virtual std::string GetTitle(int index) const { return ""; }
+ virtual void SetSelected(int sel) { }
+ virtual int GetSelected() { return -1; }
+};
+
+class ChoiceListAdaptor : public ListAdaptor {
+public:
+ ChoiceListAdaptor(const char *items[], int numItems) : items_(items), numItems_(numItems) {}
+ View *CreateItemView(int index) override;
+ int GetNumItems() override { return numItems_; }
+ bool AddEventCallback(View *view, std::function callback) override;
+
+private:
+ const char **items_;
+ int numItems_;
+};
+
+// The "selected" item is what was previously selected (optional). This items will be drawn differently.
+class StringVectorListAdaptor : public ListAdaptor {
+public:
+ StringVectorListAdaptor() : selected_(-1) {}
+ StringVectorListAdaptor(const std::vector &items, int selected = -1) : items_(items), selected_(selected) {}
+ View *CreateItemView(int index) override;
+ int GetNumItems() override { return (int)items_.size(); }
+ bool AddEventCallback(View *view, std::function callback) override;
+ void SetSelected(int sel) override { selected_ = sel; }
+ std::string GetTitle(int index) const override { return items_[index]; }
+ int GetSelected() override { return selected_; }
+
+private:
+ std::vector items_;
+ int selected_;
+};
+
+// A list view is a scroll view with autogenerated items.
+// In the future, it might be smart and load/unload items as they go, but currently not.
+class ListView : public ScrollView {
+public:
+ ListView(ListAdaptor *a, std::set hidden = std::set(), LayoutParams *layoutParams = 0);
+
+ int GetSelected() { return adaptor_->GetSelected(); }
+ void Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) override;
+ virtual void SetMaxHeight(float mh) { maxHeight_ = mh; }
+ Event OnChoice;
+ std::string DescribeLog() const override { return "ListView: " + View::DescribeLog(); }
+ std::string DescribeText() const override;
+
+private:
+ void CreateAllItems();
+ EventReturn OnItemCallback(int num, EventParams &e);
+ ListAdaptor *adaptor_;
+ LinearLayout *linLayout_;
+ float maxHeight_;
+ std::set hidden_;
+};
+
+} // namespace UI
diff --git a/Common/UI/UIScreen.cpp b/Common/UI/UIScreen.cpp
index ec586cdfe6..f8bf09602c 100644
--- a/Common/UI/UIScreen.cpp
+++ b/Common/UI/UIScreen.cpp
@@ -1,7 +1,3 @@
-#include
-#include
@@ -760,6 +766,12 @@
Render
+
+ UI
+
+
+ UI
+
diff --git a/android/jni/Android.mk b/android/jni/Android.mk
index 2ff923f755..b42ee5eff7 100644
--- a/android/jni/Android.mk
+++ b/android/jni/Android.mk
@@ -203,6 +203,8 @@ EXEC_AND_LIB_FILES := \
$(SRC)/Common/UI/Tween.cpp \
$(SRC)/Common/UI/View.cpp \
$(SRC)/Common/UI/ViewGroup.cpp \
+ $(SRC)/Common/UI/ScrollView.cpp \
+ $(SRC)/Common/UI/PopupScreens.cpp \
$(SRC)/Common/Serialize/Serializer.cpp \
$(SRC)/Common/ArmCPUDetect.cpp \
$(SRC)/Common/CPUDetect.cpp \
diff --git a/libretro/Makefile.common b/libretro/Makefile.common
index cded3e62c6..2d8a9c2ed3 100644
--- a/libretro/Makefile.common
+++ b/libretro/Makefile.common
@@ -299,6 +299,8 @@ SOURCES_CXX += \
$(COMMONDIR)/UI/Tween.cpp \
$(COMMONDIR)/UI/View.cpp \
$(COMMONDIR)/UI/ViewGroup.cpp \
+ $(COMMONDIR)/UI/ScrollView.cpp \
+ $(COMMONDIR)/UI/PopupScreens.cpp \
$(COMMONDIR)/System/Display.cpp \
$(COMMONDIR)/ArmCPUDetect.cpp \
$(COMMONDIR)/CPUDetect.cpp \