diff --git a/Common/Common.vcxproj b/Common/Common.vcxproj
index 2836fe43cd..49600b911a 100644
--- a/Common/Common.vcxproj
+++ b/Common/Common.vcxproj
@@ -538,6 +538,7 @@
+
@@ -983,6 +984,7 @@
+
diff --git a/Common/Common.vcxproj.filters b/Common/Common.vcxproj.filters
index 69df3d4c50..8825a91d6f 100644
--- a/Common/Common.vcxproj.filters
+++ b/Common/Common.vcxproj.filters
@@ -698,6 +698,9 @@
ext\nanosvg
+
+ UI
+
@@ -1302,6 +1305,9 @@
Render
+
+ UI
+
diff --git a/Common/UI/TabHolder.cpp b/Common/UI/TabHolder.cpp
new file mode 100644
index 0000000000..670c35debd
--- /dev/null
+++ b/Common/UI/TabHolder.cpp
@@ -0,0 +1,310 @@
+#include "Common/Math/curves.h"
+#include "Common/Data/Text/I18n.h"
+#include "Common/UI/TabHolder.h"
+#include "Common/UI/Root.h"
+#include "Common/UI/ScrollView.h"
+#include "Common/UI/ViewGroup.h"
+#include "Common/UI/UIScreen.h"
+
+namespace UI {
+
+TabHolder::TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams)
+ : LinearLayout(Opposite(orientation), layoutParams) {
+ SetSpacing(0.0f);
+ if (orientation == ORIENT_HORIZONTAL) {
+ tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
+ tabStrip_->SetTopTabs(true);
+ tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
+ tabScroll_->Add(tabStrip_);
+ Add(tabScroll_);
+ } else {
+ tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT));
+ tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(FILL_PARENT, FILL_PARENT));
+ tabStrip_->SetTopTabs(true);
+ tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(1.0f));
+ tabScroll_->Add(tabStrip_);
+ tabContainer_->Add(tabScroll_);
+ Add(tabContainer_);
+ }
+ tabStrip_->OnChoice.Handle(this, &TabHolder::OnTabClick);
+
+ Add(new Spacer(4.0f))->SetSeparator();
+
+ ViewGroup *contentHolder_ = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
+ if (bannerView) {
+ contentHolder_->Add(bannerView);
+ bannerView_ = bannerView;
+ }
+ contents_ = contentHolder_->Add(new AnchorLayout(new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)));
+ contents_->SetClip(true);
+ Add(contentHolder_);
+}
+
+void TabHolder::AddBack(UIScreen *parent) {
+ if (tabContainer_) {
+ auto di = GetI18NCategory(I18NCat::DIALOG);
+ tabContainer_->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle(parent, &UIScreen::OnBack);
+ }
+}
+
+void TabHolder::AddTabContents(std::string_view title, ViewGroup *tabContents) {
+ tabs_.push_back(tabContents);
+ tabStrip_->AddChoice(title);
+ contents_->Add(tabContents);
+ if (tabs_.size() > 1)
+ tabContents->SetVisibility(V_GONE);
+ tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
+
+ // Will be filled in later.
+ tabTweens_.push_back(nullptr);
+ // This entry doesn't need one.
+ createFuncs_.push_back(nullptr);
+}
+
+void TabHolder::AddTabDeferred(std::string_view title, std::function createCb) {
+ tabs_.push_back(nullptr); // marker
+ tabStrip_->AddChoice(title);
+ tabTweens_.push_back(nullptr);
+ createFuncs_.push_back(createCb);
+
+ if (tabs_.size() == 1) {
+ EnsureTab(0);
+ }
+}
+
+void TabHolder::EnsureAllCreated() {
+ for (int i = 0; i < createFuncs_.size(); i++) {
+ if (createFuncs_[i]) {
+ EnsureTab(i);
+ tabs_[i]->SetVisibility(i == currentTab_ ? V_VISIBLE : V_GONE);
+ }
+ }
+}
+
+bool TabHolder::EnsureTab(int index) {
+ _dbg_assert_(index >= 0 && index < createFuncs_.size());
+
+ if (!tabs_[index]) {
+ _dbg_assert_(index < createFuncs_.size());
+ _dbg_assert_(createFuncs_[index]);
+ std::function func;
+ createFuncs_[index].swap(func);
+
+ ViewGroup *tabContents = func();
+ tabs_[index] = tabContents;
+ contents_->Add(tabContents);
+
+ tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool TabHolder::SetCurrentTab(int tab, bool skipTween) {
+ if (tab >= (int)tabs_.size()) {
+ // Ignore
+ return false;
+ }
+
+ bool created = false;
+
+ if (tab != currentTab_) {
+ _dbg_assert_(tabs_[currentTab_]); // we should always have a tab to switch *from*.
+ created = EnsureTab(tab);
+ }
+
+ auto setupTween = [&](View *view, AnchorTranslateTween *&tween) {
+ _dbg_assert_(view != nullptr);
+ if (tween)
+ return;
+
+ tween = new AnchorTranslateTween(0.15f, bezierEaseInOut);
+ tween->Finish.Add([&](EventParams &e) {
+ e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE);
+ });
+ view->AddTween(tween)->Persist();
+ };
+
+ if (tab != currentTab_) {
+ Orientation orient = Opposite(orientation_);
+ // Direction from which the new tab will come.
+ float dir = tab < currentTab_ ? -1.0f : 1.0f;
+
+ // First, setup any missing tweens.
+ setupTween(tabs_[currentTab_], tabTweens_[currentTab_]);
+ setupTween(tabs_[tab], tabTweens_[tab]);
+
+ // Currently displayed, so let's reset it.
+ if (skipTween) {
+ tabs_[currentTab_]->SetVisibility(V_GONE);
+ tabTweens_[tab]->Reset(Point2D(0.0f, 0.0f));
+ tabTweens_[tab]->Apply(tabs_[tab]);
+ } else {
+ tabTweens_[currentTab_]->Reset(Point2D(0.0f, 0.0f));
+
+ if (orient == ORIENT_HORIZONTAL) {
+ tabTweens_[tab]->Reset(Point2D(bounds_.w * dir, 0.0f));
+ tabTweens_[currentTab_]->Divert(Point2D(bounds_.w * -dir, 0.0f));
+ } else {
+ tabTweens_[tab]->Reset(Point2D(0.0f, bounds_.h * dir));
+ tabTweens_[currentTab_]->Divert(Point2D(0.0f, bounds_.h * -dir));
+ }
+ // Actually move it to the initial position now, just to avoid any flicker.
+ tabTweens_[tab]->Apply(tabs_[tab]);
+ tabTweens_[tab]->Divert(Point2D(0.0f, 0.0f));
+ }
+ tabs_[tab]->SetVisibility(V_VISIBLE);
+
+ currentTab_ = tab;
+ }
+ tabStrip_->SetSelection(tab, false);
+
+ return created;
+}
+
+void TabHolder::OnTabClick(EventParams &e) {
+ // We have e.b set when it was an explicit click action.
+ // In that case, we make the view gone and then visible - this scrolls scrollviews to the top.
+ if (e.b != 0) {
+ EnsureTab(e.a);
+ SetCurrentTab((int)e.a);
+ }
+}
+
+void TabHolder::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["TabHolder::" + tag];
+ switch (status) {
+ case PERSIST_SAVE:
+ buffer.resize(1);
+ buffer[0] = currentTab_;
+ break;
+
+ case PERSIST_RESTORE:
+ if (buffer.size() == 1) {
+ if (SetCurrentTab(buffer[0], true)) {
+ // Re-run PersistData. TODO: Only need to do it for the new tab.
+ ViewGroup::PersistData(status, anonId, storage);
+ }
+ }
+ break;
+ }
+}
+
+void TabHolder::EnableTab(int tab, bool enabled) {
+ tabStrip_->EnableChoice(tab, enabled);
+}
+
+
+ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams)
+ : LinearLayout(orientation, layoutParams) {
+ SetSpacing(0.0f);
+}
+
+void ChoiceStrip::AddChoice(std::string_view title) {
+ StickyChoice *c = new StickyChoice(title, "",
+ orientation_ == ORIENT_HORIZONTAL ?
+ nullptr :
+ new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
+ c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
+ Add(c);
+ if (selected_ == (int)views_.size() - 1)
+ c->Press();
+}
+
+void ChoiceStrip::AddChoice(ImageID buttonImage) {
+ StickyChoice *c = new StickyChoice(buttonImage,
+ orientation_ == ORIENT_HORIZONTAL ?
+ nullptr :
+ new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
+ c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
+ Add(c);
+ if (selected_ == (int)views_.size() - 1)
+ c->Press();
+}
+
+void ChoiceStrip::OnChoiceClick(EventParams &e) {
+ // Unstick the other choices that weren't clicked.
+ for (int i = 0; i < (int)views_.size(); i++) {
+ if (views_[i] != e.v) {
+ Choice(i)->Release();
+ } else {
+ selected_ = i;
+ }
+ }
+
+ EventParams e2{};
+ e2.v = views_[selected_];
+ e2.a = selected_;
+ // Set to 1 to indicate an explicit click.
+ e2.b = 1;
+ // Dispatch immediately (we're already on the UI thread as we're in an event handler).
+ OnChoice.Dispatch(e2);
+}
+
+void ChoiceStrip::SetSelection(int sel, bool triggerClick) {
+ int prevSelected = selected_;
+ StickyChoice *prevChoice = Choice(selected_);
+ if (prevChoice)
+ prevChoice->Release();
+ selected_ = sel;
+ StickyChoice *newChoice = Choice(selected_);
+ if (newChoice) {
+ newChoice->Press();
+
+ if (topTabs_ && prevSelected != selected_) {
+ EventParams e{};
+ e.v = views_[selected_];
+ e.a = selected_;
+ // Set to 0 to indicate a selection change (not a click.)
+ e.b = triggerClick ? 1 : 0;
+ OnChoice.Trigger(e);
+ }
+ }
+}
+
+void ChoiceStrip::EnableChoice(int choice, bool enabled) {
+ if (choice < (int)views_.size()) {
+ Choice(choice)->SetEnabled(enabled);
+ }
+}
+
+bool ChoiceStrip::Key(const KeyInput &input) {
+ bool ret = false;
+ if (topTabs_ && (input.flags & KEY_DOWN)) {
+ if (IsTabLeftKey(input)) {
+ if (selected_ > 0) {
+ SetSelection(selected_ - 1, true);
+ UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point?
+ }
+ ret = true;
+ } else if (IsTabRightKey(input)) {
+ if (selected_ < (int)views_.size() - 1) {
+ SetSelection(selected_ + 1, true);
+ UI::PlayUISound(UI::UISound::TOGGLE_ON);
+ }
+ ret = true;
+ }
+ }
+ return ret || ViewGroup::Key(input);
+}
+
+std::string ChoiceStrip::DescribeText() const {
+ auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
+ return DescribeListUnordered(u->T("Choices:"));
+}
+
+StickyChoice *ChoiceStrip::Choice(int index) {
+ if ((size_t)index < views_.size())
+ return static_cast(views_[index]);
+ return nullptr;
+}
+
+} // namespace UI
diff --git a/Common/UI/TabHolder.h b/Common/UI/TabHolder.h
new file mode 100644
index 0000000000..9f098c60ba
--- /dev/null
+++ b/Common/UI/TabHolder.h
@@ -0,0 +1,90 @@
+#pragma once
+
+#include
+
+#include "Common/UI/View.h"
+#include "Common/UI/ViewGroup.h"
+#include "Common/UI/Tween.h"
+
+namespace UI {
+
+class ChoiceStrip;
+class ScrollView;
+
+class TabHolder : public LinearLayout {
+public:
+ TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams = 0);
+
+ template
+ T *AddTab(std::string_view title, T *tabContents) {
+ AddTabContents(title, tabContents);
+ return tabContents;
+ }
+ void AddTabDeferred(std::string_view title, std::function createCb);
+ void EnableTab(int tab, bool enabled);
+
+ void AddBack(UIScreen *parent);
+
+ // Returns true if the tab wasn't created before (but is now).
+ bool SetCurrentTab(int tab, bool skipTween = false);
+
+ int GetCurrentTab() const { return currentTab_; }
+ std::string DescribeLog() const override { return "TabHolder: " + View::DescribeLog(); }
+
+ void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override;
+
+ void EnsureAllCreated();
+
+ LinearLayout *Container() { return tabContainer_; }
+
+ const std::vector &GetTabContentViews() const {
+ return tabs_;
+ }
+
+private:
+ void AddTabContents(std::string_view title, ViewGroup *tabContents);
+ void OnTabClick(EventParams &e);
+ bool EnsureTab(int index); // return true if it actually created a tab.
+
+ View *bannerView_ = nullptr;
+ LinearLayout *tabContainer_ = nullptr;
+ ChoiceStrip *tabStrip_ = nullptr;
+ ScrollView *tabScroll_ = nullptr;
+ ViewGroup *contents_ = nullptr;
+
+ int currentTab_ = 0;
+ std::vector tabs_;
+ std::vector tabTweens_;
+ std::vector> createFuncs_;
+};
+
+class ChoiceStrip : public LinearLayout {
+public:
+ ChoiceStrip(Orientation orientation, LayoutParams *layoutParams = 0);
+
+ void AddChoice(std::string_view title);
+ void AddChoice(ImageID buttonImage);
+
+ int GetSelection() const { return selected_; }
+ void SetSelection(int sel, bool triggerClick);
+
+ void EnableChoice(int choice, bool enabled);
+
+ bool Key(const KeyInput &input) override;
+
+ void SetTopTabs(bool tabs) { topTabs_ = tabs; }
+
+ std::string DescribeLog() const override { return "ChoiceStrip: " + View::DescribeLog(); }
+ std::string DescribeText() const override;
+
+ Event OnChoice;
+
+private:
+ StickyChoice *Choice(int index);
+ void OnChoiceClick(EventParams &e);
+
+ int selected_ = 0; // Can be controlled with L/R.
+ bool topTabs_ = false;
+};
+
+} // namespace
diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp
index 7873615367..fd4da98669 100644
--- a/Common/UI/View.cpp
+++ b/Common/UI/View.cpp
@@ -21,7 +21,6 @@
namespace UI {
-static constexpr Size ITEM_HEIGHT = 64.f;
static constexpr float MIN_TEXT_SCALE = 0.8f;
static constexpr float MAX_ITEM_SIZE = 65535.0f;
diff --git a/Common/UI/View.h b/Common/UI/View.h
index f617773491..439a28b384 100644
--- a/Common/UI/View.h
+++ b/Common/UI/View.h
@@ -139,6 +139,9 @@ typedef float Size; // can also be WRAP_CONTENT or FILL_PARENT.
static constexpr Size WRAP_CONTENT = -1.0f;
static constexpr Size FILL_PARENT = -2.0f;
+// TODO: This needs to move to the theme.
+static constexpr Size ITEM_HEIGHT = 64.f;
+
// Gravity
enum Gravity {
G_LEFT = 0,
diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp
index 099e590d3e..2708b5cbc6 100644
--- a/Common/UI/ViewGroup.cpp
+++ b/Common/UI/ViewGroup.cpp
@@ -24,8 +24,6 @@
namespace UI {
-static constexpr Size ITEM_HEIGHT = 64.f;
-
void ApplyGravity(const Bounds &outer, const Margins &margins, float w, float h, int gravity, Bounds &inner) {
inner.w = w;
inner.h = h;
@@ -957,300 +955,6 @@ std::string GridLayoutList::DescribeText() const {
return DescribeListOrdered(u->T("List:"));
}
-TabHolder::TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams)
- : LinearLayout(Opposite(orientation), layoutParams) {
- SetSpacing(0.0f);
- if (orientation == ORIENT_HORIZONTAL) {
- tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
- tabStrip_->SetTopTabs(true);
- tabScroll_ = new ScrollView(orientation, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
- tabScroll_->Add(tabStrip_);
- Add(tabScroll_);
- } else {
- tabContainer_ = new LinearLayout(ORIENT_VERTICAL, new LayoutParams(stripSize, FILL_PARENT));
- tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(FILL_PARENT, FILL_PARENT));
- tabStrip_->SetTopTabs(true);
- tabScroll_ = new ScrollView(orientation, new LinearLayoutParams(1.0f));
- tabScroll_->Add(tabStrip_);
- tabContainer_->Add(tabScroll_);
- Add(tabContainer_);
- }
- tabStrip_->OnChoice.Handle(this, &TabHolder::OnTabClick);
-
- Add(new Spacer(4.0f))->SetSeparator();
-
- ViewGroup *contentHolder_ = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
- if (bannerView) {
- contentHolder_->Add(bannerView);
- bannerView_ = bannerView;
- }
- contents_ = contentHolder_->Add(new AnchorLayout(new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)));
- contents_->SetClip(true);
- Add(contentHolder_);
-}
-
-void TabHolder::AddBack(UIScreen *parent) {
- if (tabContainer_) {
- auto di = GetI18NCategory(I18NCat::DIALOG);
- tabContainer_->Add(new Choice(di->T("Back"), "", false, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, 10, 10))))->OnClick.Handle(parent, &UIScreen::OnBack);
- }
-}
-
-void TabHolder::AddTabContents(std::string_view title, ViewGroup *tabContents) {
- tabs_.push_back(tabContents);
- tabStrip_->AddChoice(title);
- contents_->Add(tabContents);
- if (tabs_.size() > 1)
- tabContents->SetVisibility(V_GONE);
- tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
-
- // Will be filled in later.
- tabTweens_.push_back(nullptr);
- // This entry doesn't need one.
- createFuncs_.push_back(nullptr);
-}
-
-void TabHolder::AddTabDeferred(std::string_view title, std::function createCb) {
- tabs_.push_back(nullptr); // marker
- tabStrip_->AddChoice(title);
- tabTweens_.push_back(nullptr);
- createFuncs_.push_back(createCb);
-
- if (tabs_.size() == 1) {
- EnsureTab(0);
- }
-}
-
-void TabHolder::EnsureAllCreated() {
- for (int i = 0; i < createFuncs_.size(); i++) {
- if (createFuncs_[i]) {
- EnsureTab(i);
- tabs_[i]->SetVisibility(i == currentTab_ ? V_VISIBLE : V_GONE);
- }
- }
-}
-
-bool TabHolder::EnsureTab(int index) {
- _dbg_assert_(index >= 0 && index < createFuncs_.size());
-
- if (!tabs_[index]) {
- _dbg_assert_(index < createFuncs_.size());
- _dbg_assert_(createFuncs_[index]);
- std::function func;
- createFuncs_[index].swap(func);
-
- ViewGroup *tabContents = func();
- tabs_[index] = tabContents;
- contents_->Add(tabContents);
-
- tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
- return true;
- } else {
- return false;
- }
-}
-
-bool TabHolder::SetCurrentTab(int tab, bool skipTween) {
- if (tab >= (int)tabs_.size()) {
- // Ignore
- return false;
- }
-
- bool created = false;
-
- if (tab != currentTab_) {
- _dbg_assert_(tabs_[currentTab_]); // we should always have a tab to switch *from*.
- created = EnsureTab(tab);
- }
-
- auto setupTween = [&](View *view, AnchorTranslateTween *&tween) {
- _dbg_assert_(view != nullptr);
- if (tween)
- return;
-
- tween = new AnchorTranslateTween(0.15f, bezierEaseInOut);
- tween->Finish.Add([&](EventParams &e) {
- e.v->SetVisibility(tabs_[currentTab_] == e.v ? V_VISIBLE : V_GONE);
- });
- view->AddTween(tween)->Persist();
- };
-
- if (tab != currentTab_) {
- Orientation orient = Opposite(orientation_);
- // Direction from which the new tab will come.
- float dir = tab < currentTab_ ? -1.0f : 1.0f;
-
- // First, setup any missing tweens.
- setupTween(tabs_[currentTab_], tabTweens_[currentTab_]);
- setupTween(tabs_[tab], tabTweens_[tab]);
-
- // Currently displayed, so let's reset it.
- if (skipTween) {
- tabs_[currentTab_]->SetVisibility(V_GONE);
- tabTweens_[tab]->Reset(Point2D(0.0f, 0.0f));
- tabTweens_[tab]->Apply(tabs_[tab]);
- } else {
- tabTweens_[currentTab_]->Reset(Point2D(0.0f, 0.0f));
-
- if (orient == ORIENT_HORIZONTAL) {
- tabTweens_[tab]->Reset(Point2D(bounds_.w * dir, 0.0f));
- tabTweens_[currentTab_]->Divert(Point2D(bounds_.w * -dir, 0.0f));
- } else {
- tabTweens_[tab]->Reset(Point2D(0.0f, bounds_.h * dir));
- tabTweens_[currentTab_]->Divert(Point2D(0.0f, bounds_.h * -dir));
- }
- // Actually move it to the initial position now, just to avoid any flicker.
- tabTweens_[tab]->Apply(tabs_[tab]);
- tabTweens_[tab]->Divert(Point2D(0.0f, 0.0f));
- }
- tabs_[tab]->SetVisibility(V_VISIBLE);
-
- currentTab_ = tab;
- }
- tabStrip_->SetSelection(tab, false);
-
- return created;
-}
-
-void TabHolder::OnTabClick(EventParams &e) {
- // We have e.b set when it was an explicit click action.
- // In that case, we make the view gone and then visible - this scrolls scrollviews to the top.
- if (e.b != 0) {
- EnsureTab(e.a);
- SetCurrentTab((int)e.a);
- }
-}
-
-void TabHolder::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["TabHolder::" + tag];
- switch (status) {
- case PERSIST_SAVE:
- buffer.resize(1);
- buffer[0] = currentTab_;
- break;
-
- case PERSIST_RESTORE:
- if (buffer.size() == 1) {
- if (SetCurrentTab(buffer[0], true)) {
- // Re-run PersistData. TODO: Only need to do it for the new tab.
- ViewGroup::PersistData(status, anonId, storage);
- }
- }
- break;
- }
-}
-
-ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams)
- : LinearLayout(orientation, layoutParams) {
- SetSpacing(0.0f);
-}
-
-void ChoiceStrip::AddChoice(std::string_view title) {
- StickyChoice *c = new StickyChoice(title, "",
- orientation_ == ORIENT_HORIZONTAL ?
- nullptr :
- new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
- c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
- Add(c);
- if (selected_ == (int)views_.size() - 1)
- c->Press();
-}
-
-void ChoiceStrip::AddChoice(ImageID buttonImage) {
- StickyChoice *c = new StickyChoice(buttonImage,
- orientation_ == ORIENT_HORIZONTAL ?
- nullptr :
- new LinearLayoutParams(FILL_PARENT, ITEM_HEIGHT));
- c->OnClick.Handle(this, &ChoiceStrip::OnChoiceClick);
- Add(c);
- if (selected_ == (int)views_.size() - 1)
- c->Press();
-}
-
-void ChoiceStrip::OnChoiceClick(EventParams &e) {
- // Unstick the other choices that weren't clicked.
- for (int i = 0; i < (int)views_.size(); i++) {
- if (views_[i] != e.v) {
- Choice(i)->Release();
- } else {
- selected_ = i;
- }
- }
-
- EventParams e2{};
- e2.v = views_[selected_];
- e2.a = selected_;
- // Set to 1 to indicate an explicit click.
- e2.b = 1;
- // Dispatch immediately (we're already on the UI thread as we're in an event handler).
- OnChoice.Dispatch(e2);
-}
-
-void ChoiceStrip::SetSelection(int sel, bool triggerClick) {
- int prevSelected = selected_;
- StickyChoice *prevChoice = Choice(selected_);
- if (prevChoice)
- prevChoice->Release();
- selected_ = sel;
- StickyChoice *newChoice = Choice(selected_);
- if (newChoice) {
- newChoice->Press();
-
- if (topTabs_ && prevSelected != selected_) {
- EventParams e{};
- e.v = views_[selected_];
- e.a = selected_;
- // Set to 0 to indicate a selection change (not a click.)
- e.b = triggerClick ? 1 : 0;
- OnChoice.Trigger(e);
- }
- }
-}
-
-void ChoiceStrip::EnableChoice(int choice, bool enabled) {
- if (choice < (int)views_.size()) {
- Choice(choice)->SetEnabled(enabled);
- }
-}
-
-bool ChoiceStrip::Key(const KeyInput &input) {
- bool ret = false;
- if (topTabs_ && (input.flags & KEY_DOWN)) {
- if (IsTabLeftKey(input)) {
- if (selected_ > 0) {
- SetSelection(selected_ - 1, true);
- UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point?
- }
- ret = true;
- } else if (IsTabRightKey(input)) {
- if (selected_ < (int)views_.size() - 1) {
- SetSelection(selected_ + 1, true);
- UI::PlayUISound(UI::UISound::TOGGLE_ON);
- }
- ret = true;
- }
- }
- return ret || ViewGroup::Key(input);
-}
-
-std::string ChoiceStrip::DescribeText() const {
- auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
- return DescribeListUnordered(u->T("Choices:"));
-}
-
-StickyChoice *ChoiceStrip::Choice(int index) {
- if ((size_t)index < views_.size())
- return static_cast(views_[index]);
- return nullptr;
-}
-
CollapsibleSection::CollapsibleSection(std::string_view title, LayoutParams *layoutParams) : LinearLayout(ORIENT_VERTICAL, layoutParams) {
open_ = &localOpen_;
SetSpacing(0.0f);
diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h
index 55fab65fd7..ac1ad27955 100644
--- a/Common/UI/ViewGroup.h
+++ b/Common/UI/ViewGroup.h
@@ -260,84 +260,6 @@ public:
std::string DescribeText() const override;
};
-class ChoiceStrip : public LinearLayout {
-public:
- ChoiceStrip(Orientation orientation, LayoutParams *layoutParams = 0);
-
- void AddChoice(std::string_view title);
- void AddChoice(ImageID buttonImage);
-
- int GetSelection() const { return selected_; }
- void SetSelection(int sel, bool triggerClick);
-
- void EnableChoice(int choice, bool enabled);
-
- bool Key(const KeyInput &input) override;
-
- void SetTopTabs(bool tabs) { topTabs_ = tabs; }
-
- std::string DescribeLog() const override { return "ChoiceStrip: " + View::DescribeLog(); }
- std::string DescribeText() const override;
-
- Event OnChoice;
-
-private:
- StickyChoice *Choice(int index);
- void OnChoiceClick(EventParams &e);
-
- int selected_ = 0; // Can be controlled with L/R.
- bool topTabs_ = false;
-};
-
-class TabHolder : public LinearLayout {
-public:
- TabHolder(Orientation orientation, float stripSize, View *bannerView, LayoutParams *layoutParams = 0);
-
- template
- T *AddTab(std::string_view title, T *tabContents) {
- AddTabContents(title, tabContents);
- return tabContents;
- }
- void AddTabDeferred(std::string_view title, std::function createCb);
- void EnableTab(int tab, bool enabled) {
- tabStrip_->EnableChoice(tab, enabled);
- }
-
- void AddBack(UIScreen *parent);
-
- // Returns true if the tab wasn't created before (but is now).
- bool SetCurrentTab(int tab, bool skipTween = false);
-
- int GetCurrentTab() const { return currentTab_; }
- std::string DescribeLog() const override { return "TabHolder: " + View::DescribeLog(); }
-
- void PersistData(PersistStatus status, std::string anonId, PersistMap &storage) override;
-
- void EnsureAllCreated();
-
- LinearLayout *Container() { return tabContainer_; }
-
- const std::vector &GetTabContentViews() const {
- return tabs_;
- }
-
-private:
- void AddTabContents(std::string_view title, ViewGroup *tabContents);
- void OnTabClick(EventParams &e);
- bool EnsureTab(int index); // return true if it actually created a tab.
-
- View *bannerView_ = nullptr;
- LinearLayout *tabContainer_ = nullptr;
- ChoiceStrip *tabStrip_ = nullptr;
- ScrollView *tabScroll_ = nullptr;
- ViewGroup *contents_ = nullptr;
-
- int currentTab_ = 0;
- std::vector tabs_;
- std::vector tabTweens_;
- std::vector> createFuncs_;
-};
-
class CollapsibleHeader;
class CollapsibleSection : public LinearLayout {
diff --git a/UI/DisplayLayoutScreen.cpp b/UI/DisplayLayoutScreen.cpp
index 07241bcb26..89cda23fc7 100644
--- a/UI/DisplayLayoutScreen.cpp
+++ b/UI/DisplayLayoutScreen.cpp
@@ -24,8 +24,8 @@
#include "Common/UI/Context.h"
#include "Common/UI/View.h"
#include "Common/UI/UIScreen.h"
+#include "Common/UI/TabHolder.h"
#include "Common/Math/math_util.h"
-#include "Common/System/Display.h"
#include "Common/System/NativeApp.h"
#include "Common/VR/PPSSPPVR.h"
#include "Common/StringUtils.h"
diff --git a/UI/DisplayLayoutScreen.h b/UI/DisplayLayoutScreen.h
index 09a6be40f9..be6e875770 100644
--- a/UI/DisplayLayoutScreen.h
+++ b/UI/DisplayLayoutScreen.h
@@ -25,6 +25,10 @@
#include "MiscScreens.h"
+namespace UI {
+class ChoiceStrip;
+}
+
class DisplayLayoutScreen : public UIDialogScreenWithGameBackground {
public:
DisplayLayoutScreen(const Path &filename);
diff --git a/UI/DriverManagerScreen.cpp b/UI/DriverManagerScreen.cpp
index 65be97bd9e..3454139aeb 100644
--- a/UI/DriverManagerScreen.cpp
+++ b/UI/DriverManagerScreen.cpp
@@ -7,7 +7,7 @@
#include "Core/Config.h"
#include "Core/System.h"
-#include "UI/View.h"
+#include "Common/UI/View.h"
#include "UI/DriverManagerScreen.h"
#include "UI/GameSettingsScreen.h" // for triggerrestart
#include "UI/OnScreenDisplay.h"
@@ -79,8 +79,6 @@ public:
std::string name_;
};
-static constexpr UI::Size ITEM_HEIGHT = 64.f;
-
DriverChoice::DriverChoice(const std::string &driverName, bool current, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_VERTICAL, layoutParams), name_(driverName) {
using namespace UI;
SetSpacing(2.0f);
diff --git a/UI/GPUDriverTestScreen.h b/UI/GPUDriverTestScreen.h
index 6f27f855be..7421603de8 100644
--- a/UI/GPUDriverTestScreen.h
+++ b/UI/GPUDriverTestScreen.h
@@ -4,6 +4,7 @@
#include "Common/UI/Context.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "Common/Log.h"
#include "UI/MiscScreens.h"
diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp
index 08dea7a9ac..1a683133d6 100644
--- a/UI/GameSettingsScreen.cpp
+++ b/UI/GameSettingsScreen.cpp
@@ -936,8 +936,6 @@ public:
MacAddressChooser(RequesterToken token, Path gamePath, std::string *value, std::string_view title, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr);
};
-static constexpr UI::Size ITEM_HEIGHT = 64.f;
-
MacAddressChooser::MacAddressChooser(RequesterToken token, Path gamePath_, std::string *value, std::string_view title, ScreenManager *screenManager, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams) {
using namespace UI;
SetSpacing(5.0f);
diff --git a/UI/MainScreen.h b/UI/MainScreen.h
index a812bc48aa..9c31fbcbee 100644
--- a/UI/MainScreen.h
+++ b/UI/MainScreen.h
@@ -23,6 +23,7 @@
#include "Common/File/Path.h"
#include "Common/UI/UIScreen.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "UI/MiscScreens.h"
#include "Common/File/PathBrowser.h"
@@ -134,6 +135,7 @@ protected:
void CreateViews() override;
void CreateRecentTab();
GameBrowser *CreateBrowserTab(const Path &path, std::string_view title, std::string_view howToTitle, std::string_view howToUri, BrowseFlags browseFlags, bool *bGridView, float *scrollPos);
+ UI::ViewGroup *CreateButtons(bool vertical);
void DrawBackground(UIContext &dc) override;
void update() override;
diff --git a/UI/RetroAchievementScreens.cpp b/UI/RetroAchievementScreens.cpp
index eaefaa8204..40f2bf7560 100644
--- a/UI/RetroAchievementScreens.cpp
+++ b/UI/RetroAchievementScreens.cpp
@@ -2,6 +2,7 @@
#include "Common/System/Request.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "Common/UI/Context.h"
#include "Common/Data/Text/I18n.h"
#include "Common/UI/IconCache.h"
@@ -26,8 +27,6 @@ public:
UI::UISound sound_;
};
-static constexpr UI::Size ITEM_HEIGHT = 64.f;
-
AudioFileChooser::AudioFileChooser(RequesterToken token, std::string *value, std::string_view title, UI::UISound sound, UI::LayoutParams *layoutParams) : UI::LinearLayout(UI::ORIENT_HORIZONTAL, layoutParams), sound_(sound) {
using namespace UI;
SetSpacing(2.0f);
diff --git a/UI/TabbedDialogScreen.cpp b/UI/TabbedDialogScreen.cpp
index e34c06f444..07346a2c78 100644
--- a/UI/TabbedDialogScreen.cpp
+++ b/UI/TabbedDialogScreen.cpp
@@ -4,6 +4,7 @@
#include "Common/System/NativeApp.h"
#include "Common/System/Request.h"
#include "Common/System/Display.h"
+#include "Common/UI/TabHolder.h"
#include "UI/TabbedDialogScreen.h"
void TabbedUIDialogScreenWithGameBackground::AddTab(const char *tag, std::string_view title, std::function createCallback, bool isSearch) {
diff --git a/UI/TabbedDialogScreen.h b/UI/TabbedDialogScreen.h
index 436bb45ffa..a856acbf38 100644
--- a/UI/TabbedDialogScreen.h
+++ b/UI/TabbedDialogScreen.h
@@ -8,6 +8,10 @@
#include "Core/ConfigValues.h"
#include "UI/MiscScreens.h"
+namespace UI {
+class TabHolder;
+}
+
class TabbedUIDialogScreenWithGameBackground : public UIDialogScreenWithGameBackground {
public:
TabbedUIDialogScreenWithGameBackground(const Path &gamePath) : UIDialogScreenWithGameBackground(gamePath) {}
diff --git a/UI/TouchControlLayoutScreen.h b/UI/TouchControlLayoutScreen.h
index eb33a0799e..105463a69f 100644
--- a/UI/TouchControlLayoutScreen.h
+++ b/UI/TouchControlLayoutScreen.h
@@ -19,6 +19,7 @@
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
+#include "Common/UI/TabHolder.h"
#include "MiscScreens.h"
class ControlLayoutView;
diff --git a/UI/TouchControlVisibilityScreen.cpp b/UI/TouchControlVisibilityScreen.cpp
index b6b3ed71ac..4ad7c9a67a 100644
--- a/UI/TouchControlVisibilityScreen.cpp
+++ b/UI/TouchControlVisibilityScreen.cpp
@@ -15,6 +15,7 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
+#include "Common/UI/TabHolder.h"
#include "Common/Render/TextureAtlas.h"
#include "Common/Data/Text/I18n.h"
#include "Common/StringUtils.h"
diff --git a/UI/UIAtlas.cpp b/UI/UIAtlas.cpp
index f468b66137..854acafce5 100644
--- a/UI/UIAtlas.cpp
+++ b/UI/UIAtlas.cpp
@@ -129,6 +129,11 @@ static const ImageMeta imageIDs[] = {
{"I_PIN", false},
{"I_UNPIN", false},
{"I_FOLDER_PINNED", false},
+ {"I_FILLED_CIRCLE_1", false},
+ {"I_FILLED_CIRCLE_2", false},
+ {"I_FILLED_CIRCLE_3", false},
+ {"I_FILLED_CIRCLE_4", false},
+ {"I_FILLED_CIRCLE_5", false},
};
static std::string PNGNameFromID(std::string_view id) {
diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg
index 7a9db667e4..00ccb9713a 100644
--- a/assets/ui_images/images.svg
+++ b/assets/ui_images/images.svg
@@ -7,14 +7,14 @@
viewBox="0 0 158.75 158.75"
version="1.1"
id="svg1"
- inkscape:version="1.4 (86a8ad7, 2024-10-11)"
+ inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="images.svg"
+ xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
- xmlns:svg="http://www.w3.org/2000/svg">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ sodipodi:nodetypes="sssccccccaccccccssssccccszsccc"
+ id="path109" />
+
+
+
+Layer 1
diff --git a/assets/ui_images/svg_sources.txt b/assets/ui_images/svg_sources.txt
new file mode 100644
index 0000000000..6a34956285
--- /dev/null
+++ b/assets/ui_images/svg_sources.txt
@@ -0,0 +1,5 @@
+Most of the vectors in images.svg are custom drawn by hrydgard and NABN00B.
+
+Some are derived from public domain SVGs from the following sites:
+
+www.svgrepo.com