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"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + +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