ppsspp/Common/UI/ViewGroup.cpp
Unknown W. Brackets 7a29a8bd48 UI: Handle WRAP_CONTENT in flexible scroll views.
If the scroll view had weight, it'd grow to the max size always.  Now it
still fits to the container, if that's what it's set to.
2021-08-29 16:39:14 -07:00

1595 lines
45 KiB
C++

#include <algorithm>
#include <cmath>
#include <functional>
#include <iomanip>
#include <mutex>
#include <set>
#include <sstream>
#include "Common/Data/Text/I18n.h"
#include "Common/Input/KeyCodes.h"
#include "Common/Math/curves.h"
#include "Common/UI/Context.h"
#include "Common/UI/Tween.h"
#include "Common/UI/Root.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Common/Render/DrawBuffer.h"
#include "Common/Log.h"
#include "Common/TimeUtil.h"
#include "Common/StringUtils.h"
namespace UI {
const float 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;
switch (gravity & G_HORIZMASK) {
case G_LEFT: inner.x = outer.x + margins.left; break;
case G_RIGHT: inner.x = outer.x + outer.w - w - margins.right; break;
case G_HCENTER: inner.x = outer.x + (outer.w - w) / 2; break;
}
switch (gravity & G_VERTMASK) {
case G_TOP: inner.y = outer.y + margins.top; break;
case G_BOTTOM: inner.y = outer.y + outer.h - h - margins.bottom; break;
case G_VCENTER: inner.y = outer.y + (outer.h - h) / 2; break;
}
}
ViewGroup::~ViewGroup() {
// Tear down the contents recursively.
Clear();
}
void ViewGroup::RemoveSubview(View *view) {
std::lock_guard<std::mutex> guard(modifyLock_);
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i] == view) {
views_.erase(views_.begin() + i);
delete view;
return;
}
}
}
bool ViewGroup::ContainsSubview(const View *view) const {
for (const View *subview : views_) {
if (subview == view || subview->ContainsSubview(view))
return true;
}
return false;
}
void ViewGroup::Clear() {
std::lock_guard<std::mutex> guard(modifyLock_);
for (size_t i = 0; i < views_.size(); i++) {
delete views_[i];
views_[i] = nullptr;
}
views_.clear();
}
void ViewGroup::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
std::lock_guard<std::mutex> guard(modifyLock_);
std::string tag = Tag();
if (tag.empty()) {
tag = anonId;
}
for (size_t i = 0; i < views_.size(); i++) {
views_[i]->PersistData(status, tag + "/" + StringFromInt((int)i), storage);
}
}
void ViewGroup::Touch(const TouchInput &input) {
std::lock_guard<std::mutex> guard(modifyLock_);
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
// TODO: If there is a transformation active, transform input coordinates accordingly.
if ((*iter)->GetVisibility() == V_VISIBLE)
(*iter)->Touch(input);
}
}
void ViewGroup::Query(float x, float y, std::vector<View *> &list) {
if (bounds_.Contains(x, y)) {
list.push_back(this);
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
(*iter)->Query(x, y, list);
}
}
}
bool ViewGroup::Key(const KeyInput &input) {
std::lock_guard<std::mutex> guard(modifyLock_);
bool ret = false;
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
// TODO: If there is a transformation active, transform input coordinates accordingly.
if ((*iter)->GetVisibility() == V_VISIBLE)
ret = ret || (*iter)->Key(input);
}
return ret;
}
void ViewGroup::Axis(const AxisInput &input) {
std::lock_guard<std::mutex> guard(modifyLock_);
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
// TODO: If there is a transformation active, transform input coordinates accordingly.
if ((*iter)->GetVisibility() == V_VISIBLE)
(*iter)->Axis(input);
}
}
void ViewGroup::DeviceLost() {
std::lock_guard<std::mutex> guard(modifyLock_);
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
(*iter)->DeviceLost();
}
}
void ViewGroup::DeviceRestored(Draw::DrawContext *draw) {
std::lock_guard<std::mutex> guard(modifyLock_);
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
(*iter)->DeviceRestored(draw);
}
}
void ViewGroup::Draw(UIContext &dc) {
if (hasDropShadow_) {
// Darken things behind.
dc.FillRect(UI::Drawable(0x60000000), dc.GetBounds().Expand(dropShadowExpand_));
float dropsize = 30.0f;
dc.Draw()->DrawImage4Grid(dc.theme->dropShadow4Grid,
bounds_.x - dropsize, bounds_.y,
bounds_.x2() + dropsize, bounds_.y2()+dropsize*1.5f, 0xDF000000, 3.0f);
}
if (clip_) {
dc.PushScissor(bounds_);
}
dc.FillRect(bg_, bounds_);
for (View *view : views_) {
if (view->GetVisibility() == V_VISIBLE) {
// Check if bounds are in current scissor rectangle.
if (dc.GetScissorBounds().Intersects(dc.TransformBounds(view->GetBounds())))
view->Draw(dc);
}
}
if (clip_) {
dc.PopScissor();
}
}
std::string ViewGroup::DescribeText() const {
std::stringstream ss;
bool needNewline = false;
for (View *view : views_) {
if (view->GetVisibility() != V_VISIBLE)
continue;
std::string s = view->DescribeText();
if (s.empty())
continue;
if (needNewline) {
ss << "\n";
}
ss << s;
needNewline = s[s.length() - 1] != '\n';
}
return ss.str();
}
std::string ViewGroup::DescribeListUnordered(const char *heading) const {
std::stringstream ss;
ss << heading << "\n";
bool needNewline = false;
for (View *view : views_) {
if (view->GetVisibility() != V_VISIBLE)
continue;
std::string s = view->DescribeText();
if (s.empty())
continue;
ss << " - " << IndentString(s, " ", true);
}
return ss.str();
}
std::string ViewGroup::DescribeListOrdered(const char *heading) const {
std::stringstream ss;
ss << heading << "\n";
// This is how much space we need for the highest number.
int sz = (int)floorf(log10f((float)views_.size())) + 1;
std::string indent = " " + std::string(sz, ' ');
bool needNewline = false;
int n = 1;
for (View *view : views_) {
if (view->GetVisibility() != V_VISIBLE)
continue;
std::string s = view->DescribeText();
if (s.empty())
continue;
ss << std::setw(sz) << n++ << ". " << IndentString(s, indent, true);
}
return ss.str();
}
void ViewGroup::Update() {
View::Update();
for (View *view : views_) {
if (view->GetVisibility() != V_GONE)
view->Update();
}
}
bool ViewGroup::SetFocus() {
std::lock_guard<std::mutex> guard(modifyLock_);
if (!CanBeFocused() && !views_.empty()) {
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i]->SetFocus())
return true;
}
}
return false;
}
bool ViewGroup::SubviewFocused(View *view) {
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i] == view)
return true;
if (views_[i]->SubviewFocused(view))
return true;
}
return false;
}
// Returns the percentage the smaller one overlaps the bigger one.
static float HorizontalOverlap(const Bounds &a, const Bounds &b) {
if (a.x2() < b.x || b.x2() < a.x)
return 0.0f;
// okay they do overlap. Let's clip.
float maxMin = std::max(a.x, b.x);
float minMax = std::min(a.x2(), b.x2());
float overlap = minMax - maxMin;
if (overlap < 0.0f)
return 0.0f;
else
return std::min(1.0f, overlap / std::min(a.w, b.w));
}
// Returns the percentage the smaller one overlaps the bigger one.
static float VerticalOverlap(const Bounds &a, const Bounds &b) {
if (a.y2() < b.y || b.y2() < a.y)
return 0.0f;
// okay they do overlap. Let's clip.
float maxMin = std::max(a.y, b.y);
float minMax = std::min(a.y2(), b.y2());
float overlap = minMax - maxMin;
if (overlap < 0.0f)
return 0.0f;
else
return std::min(1.0f, overlap / std::min(a.h, b.h));
}
float GetTargetScore(const Point &originPos, int originIndex, View *origin, View *destination, FocusDirection direction) {
// Skip labels and things like that.
if (!destination->CanBeFocused())
return 0.0f;
if (destination->IsEnabled() == false)
return 0.0f;
if (destination->GetVisibility() != V_VISIBLE)
return 0.0f;
Point destPos = destination->GetFocusPosition(Opposite(direction));
float dx = destPos.x - originPos.x;
float dy = destPos.y - originPos.y;
float distance = sqrtf(dx*dx + dy*dy);
float overlap = 0.0f;
float dirX = dx / distance;
float dirY = dy / distance;
bool wrongDirection = false;
bool vertical = false;
float horizOverlap = HorizontalOverlap(origin->GetBounds(), destination->GetBounds());
float vertOverlap = VerticalOverlap(origin->GetBounds(), destination->GetBounds());
if (horizOverlap == 1.0f && vertOverlap == 1.0f) {
if (direction != FOCUS_PREV_PAGE && direction != FOCUS_NEXT_PAGE) {
INFO_LOG(SYSTEM, "Contain overlap");
return 0.0;
}
}
float originSize = 0.0f;
switch (direction) {
case FOCUS_LEFT:
overlap = vertOverlap;
originSize = origin->GetBounds().w;
if (dirX > 0.0f) {
wrongDirection = true;
}
break;
case FOCUS_UP:
overlap = horizOverlap;
originSize = origin->GetBounds().h;
if (dirY > 0.0f) {
wrongDirection = true;
}
vertical = true;
break;
case FOCUS_RIGHT:
overlap = vertOverlap;
originSize = origin->GetBounds().w;
if (dirX < 0.0f) {
wrongDirection = true;
}
break;
case FOCUS_DOWN:
overlap = horizOverlap;
originSize = origin->GetBounds().h;
if (dirY < 0.0f) {
wrongDirection = true;
}
vertical = true;
break;
case FOCUS_FIRST:
if (originIndex == -1)
return 0.0f;
if (dirX > 0.0f || dirY > 0.0f)
return 0.0f;
// More distance is good.
return distance;
case FOCUS_LAST:
if (originIndex == -1)
return 0.0f;
if (dirX < 0.0f || dirY < 0.0f)
return 0.0f;
// More distance is good.
return distance;
case FOCUS_PREV_PAGE:
case FOCUS_NEXT_PAGE:
// Not always, but let's go with the bonus on height.
vertical = true;
break;
case FOCUS_PREV:
case FOCUS_NEXT:
ERROR_LOG(SYSTEM, "Invalid focus direction");
break;
}
// At large distances, ignore overlap.
if (distance > 2.0 * originSize)
overlap = 0.0f;
if (wrongDirection) {
return 0.0f;
} else {
return 10.0f / std::max(1.0f, distance) + overlap * 2.0;
}
}
float GetDirectionScore(int originIndex, View *origin, View *destination, FocusDirection direction) {
Point originPos = origin->GetFocusPosition(direction);
return GetTargetScore(originPos, originIndex, origin, destination, direction);
}
NeighborResult ViewGroup::FindNeighbor(View *view, FocusDirection direction, NeighborResult result) {
if (!IsEnabled())
return result;
if (GetVisibility() != V_VISIBLE)
return result;
// First, find the position of the view in the list.
int num = -1;
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i] == view) {
num = (int)i;
break;
}
}
if (direction == FOCUS_PREV || direction == FOCUS_NEXT) {
switch (direction) {
case FOCUS_PREV:
// If view not found, no neighbor to find.
if (num == -1)
return NeighborResult(0, 0.0f);
return NeighborResult(views_[(num + views_.size() - 1) % views_.size()], 0.0f);
case FOCUS_NEXT:
// If view not found, no neighbor to find.
if (num == -1)
return NeighborResult(0, 0.0f);
return NeighborResult(views_[(num + 1) % views_.size()], 0.0f);
default:
return NeighborResult(nullptr, 0.0f);
}
}
switch (direction) {
case FOCUS_UP:
case FOCUS_LEFT:
case FOCUS_RIGHT:
case FOCUS_DOWN:
case FOCUS_FIRST:
case FOCUS_LAST:
{
// First, try the child views themselves as candidates
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i] == view)
continue;
float score = GetDirectionScore(num, view, views_[i], direction);
if (score > result.score) {
result.score = score;
result.view = views_[i];
}
}
// Then go right ahead and see if any of the children contain any better candidates.
for (auto iter = views_.begin(); iter != views_.end(); ++iter) {
if ((*iter)->IsViewGroup()) {
ViewGroup *vg = static_cast<ViewGroup *>(*iter);
if (vg)
result = vg->FindNeighbor(view, direction, result);
}
}
// Boost neighbors with the same parent
if (num != -1) {
//result.score += 100.0f;
}
return result;
}
case FOCUS_PREV_PAGE:
case FOCUS_NEXT_PAGE:
return FindScrollNeighbor(view, Point(INFINITY, INFINITY), direction, result);
default:
return result;
}
}
NeighborResult ViewGroup::FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) {
if (!IsEnabled())
return best;
if (GetVisibility() != V_VISIBLE)
return best;
if (target.x < INFINITY && target.y < INFINITY) {
for (auto v : views_) {
// Note: we consider the origin itself, which might already be the best option.
float score = GetTargetScore(target, -1, view, v, direction);
if (score > best.score) {
best.score = score;
best.view = v;
}
}
}
for (auto v : views_) {
if (v->IsViewGroup()) {
ViewGroup *vg = static_cast<ViewGroup *>(v);
if (vg)
best = vg->FindScrollNeighbor(view, target, direction, best);
}
}
return best;
}
// TODO: This code needs some cleanup/restructuring...
void LinearLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_);
MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_);
if (views_.empty())
return;
float sum = 0.0f;
float maxOther = 0.0f;
float totalWeight = 0.0f;
float weightSum = 0.0f;
float weightZeroSum = 0.0f;
int numVisible = 0;
for (View *view : views_) {
if (view->GetVisibility() == V_GONE)
continue;
numVisible++;
const LinearLayoutParams *linLayoutParams = view->GetLayoutParams()->As<LinearLayoutParams>();
Margins margins = defaultMargins_;
if (linLayoutParams) {
totalWeight += linLayoutParams->weight;
if (linLayoutParams->HasMargins())
margins = linLayoutParams->margins;
}
if (orientation_ == ORIENT_HORIZONTAL) {
MeasureSpec v = vert;
if (v.type == UNSPECIFIED && measuredHeight_ != 0.0f)
v = MeasureSpec(AT_MOST, measuredHeight_);
view->Measure(dc, MeasureSpec(UNSPECIFIED, measuredWidth_), v - (float)margins.vert());
if (horiz.type == AT_MOST && view->GetMeasuredWidth() + margins.horiz() > horiz.size - weightZeroSum) {
// Try again, this time with AT_MOST.
view->Measure(dc, horiz, v - (float)margins.vert());
}
} else if (orientation_ == ORIENT_VERTICAL) {
MeasureSpec h = horiz;
if (h.type == UNSPECIFIED && measuredWidth_ != 0.0f)
h = MeasureSpec(AT_MOST, measuredWidth_);
view->Measure(dc, h - (float)margins.horiz(), MeasureSpec(UNSPECIFIED, measuredHeight_));
if (vert.type == AT_MOST && view->GetMeasuredHeight() + margins.vert() > vert.size - weightZeroSum) {
// Try again, this time with AT_MOST.
view->Measure(dc, h - (float)margins.horiz(), vert);
}
}
float amount;
if (orientation_ == ORIENT_HORIZONTAL) {
amount = view->GetMeasuredWidth() + margins.horiz();
maxOther = std::max(maxOther, view->GetMeasuredHeight() + margins.vert());
} else {
amount = view->GetMeasuredHeight() + margins.vert();
maxOther = std::max(maxOther, view->GetMeasuredWidth() + margins.horiz());
}
sum += amount;
if (linLayoutParams) {
if (linLayoutParams->weight == 0.0f)
weightZeroSum += amount;
weightSum += linLayoutParams->weight;
} else {
weightZeroSum += amount;
}
}
weightZeroSum += spacing_ * (numVisible - 1);
// Alright, got the sum. Let's take the remaining space after the fixed-size views,
// and distribute among the weighted ones.
if (orientation_ == ORIENT_HORIZONTAL) {
MeasureBySpec(layoutParams_->width, weightZeroSum, horiz, &measuredWidth_);
// If we've got stretch, allow growing to fill the parent.
float allowedWidth = measuredWidth_;
if (horiz.type == AT_MOST && measuredWidth_ < horiz.size) {
allowedWidth = horiz.size;
}
float usedWidth = 0.0f;
// Redistribute the stretchy ones! and remeasure the children!
for (View *view : views_) {
if (view->GetVisibility() == V_GONE)
continue;
const LinearLayoutParams *linLayoutParams = view->GetLayoutParams()->As<LinearLayoutParams>();
if (linLayoutParams && linLayoutParams->weight > 0.0f) {
Margins margins = defaultMargins_;
if (linLayoutParams->HasMargins())
margins = linLayoutParams->margins;
MeasureSpec v = vert;
if (v.type == UNSPECIFIED && measuredHeight_ != 0.0f)
v = MeasureSpec(AT_MOST, measuredHeight_);
float unit = (allowedWidth - weightZeroSum) / weightSum;
MeasureSpec h(AT_MOST, unit * linLayoutParams->weight - margins.horiz());
if (horiz.type == EXACTLY) {
h.type = EXACTLY;
}
view->Measure(dc, h, v - (float)margins.vert());
usedWidth += view->GetMeasuredWidth();
maxOther = std::max(maxOther, view->GetMeasuredHeight() + margins.vert());
}
}
if (horiz.type == AT_MOST && measuredWidth_ < horiz.size) {
measuredWidth_ += usedWidth;
}
// Measure here in case maxOther moved (can happen due to word wrap.)
MeasureBySpec(layoutParams_->height, maxOther, vert, &measuredHeight_);
} else {
MeasureBySpec(layoutParams_->height, weightZeroSum, vert, &measuredHeight_);
// If we've got stretch, allow growing to fill the parent.
float allowedHeight = measuredHeight_;
if (vert.type == AT_MOST && measuredHeight_ < vert.size) {
allowedHeight = vert.size;
}
float usedHeight = 0.0f;
// Redistribute the stretchy ones! and remeasure the children!
for (View *view : views_) {
if (view->GetVisibility() == V_GONE)
continue;
const LinearLayoutParams *linLayoutParams = view->GetLayoutParams()->As<LinearLayoutParams>();
if (linLayoutParams && linLayoutParams->weight > 0.0f) {
Margins margins = defaultMargins_;
if (linLayoutParams->HasMargins())
margins = linLayoutParams->margins;
MeasureSpec h = horiz;
if (h.type == UNSPECIFIED && measuredWidth_ != 0.0f)
h = MeasureSpec(AT_MOST, measuredWidth_);
float unit = (allowedHeight - weightZeroSum) / weightSum;
MeasureSpec v(AT_MOST, unit * linLayoutParams->weight - margins.vert());
if (vert.type == EXACTLY) {
v.type = EXACTLY;
}
view->Measure(dc, h - (float)margins.horiz(), v);
usedHeight += view->GetMeasuredHeight();
maxOther = std::max(maxOther, view->GetMeasuredWidth() + margins.horiz());
}
}
if (vert.type == AT_MOST && measuredHeight_ < vert.size) {
measuredHeight_ += usedHeight;
}
// Measure here in case maxOther moved (can happen due to word wrap.)
MeasureBySpec(layoutParams_->width, maxOther, horiz, &measuredWidth_);
}
}
// weight != 0 = fill remaining space.
void LinearLayout::Layout() {
const Bounds &bounds = bounds_;
Bounds itemBounds;
float pos;
if (orientation_ == ORIENT_HORIZONTAL) {
pos = bounds.x;
itemBounds.y = bounds.y;
itemBounds.h = measuredHeight_;
} else {
pos = bounds.y;
itemBounds.x = bounds.x;
itemBounds.w = measuredWidth_;
}
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i]->GetVisibility() == V_GONE)
continue;
const LinearLayoutParams *linLayoutParams = views_[i]->GetLayoutParams()->As<LinearLayoutParams>();
Gravity gravity = G_TOPLEFT;
Margins margins = defaultMargins_;
if (linLayoutParams) {
if (linLayoutParams->HasMargins())
margins = linLayoutParams->margins;
gravity = linLayoutParams->gravity;
}
if (orientation_ == ORIENT_HORIZONTAL) {
itemBounds.x = pos;
itemBounds.w = views_[i]->GetMeasuredWidth() + margins.horiz();
} else {
itemBounds.y = pos;
itemBounds.h = views_[i]->GetMeasuredHeight() + margins.vert();
}
Bounds innerBounds;
ApplyGravity(itemBounds, margins,
views_[i]->GetMeasuredWidth(), views_[i]->GetMeasuredHeight(),
gravity, innerBounds);
views_[i]->SetBounds(innerBounds);
views_[i]->Layout();
pos += spacing_ + (orientation_ == ORIENT_HORIZONTAL ? itemBounds.w : itemBounds.h);
}
}
std::string LinearLayoutList::DescribeText() const {
auto u = GetI18NCategory("UI Elements");
return DescribeListOrdered(u->T("List:"));
}
void FrameLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
if (views_.empty()) {
MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_);
MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_);
return;
}
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i]->GetVisibility() == V_GONE)
continue;
views_[i]->Measure(dc, horiz, vert);
}
}
void FrameLayout::Layout() {
for (size_t i = 0; i < views_.size(); i++) {
if (views_[i]->GetVisibility() == V_GONE)
continue;
float w = views_[i]->GetMeasuredWidth();
float h = views_[i]->GetMeasuredHeight();
Bounds bounds;
bounds.w = w;
bounds.h = h;
bounds.x = bounds_.x + (measuredWidth_ - w) / 2;
bounds.y = bounds_.y + (measuredWidth_ - h) / 2;
views_[i]->SetBounds(bounds);
}
}
void ScrollView::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
// Respect margins
Margins margins;
if (views_.size()) {
const LinearLayoutParams *linLayoutParams = views_[0]->GetLayoutParams()->As<LinearLayoutParams>();
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<LinearLayoutParams>();
if (linLayoutParams) {
margins = linLayoutParams->margins;
}
scrolled.w = views_[0]->GetMeasuredWidth() - margins.horiz();
scrolled.h = views_[0]->GetMeasuredHeight() - margins.vert();
float layoutScrollPos = ClampedScrollPos(scrollPos_);
switch (orientation_) {
case ORIENT_HORIZONTAL:
if (scrolled.w != lastViewSize_) {
ScrollTo(0.0f);
lastViewSize_ = scrolled.w;
}
scrolled.x = bounds_.x - layoutScrollPos;
scrolled.y = bounds_.y + margins.top;
break;
case ORIENT_VERTICAL:
if (scrolled.h != lastViewSize_ && scrollToTopOnSizeChange_) {
ScrollTo(0.0f);
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);
if (input.flags & KEY_DOWN) {
switch (input.keyCode) {
case NKCODE_EXT_MOUSEWHEEL_UP:
ScrollRelative(-250);
break;
case NKCODE_EXT_MOUSEWHEEL_DOWN:
ScrollRelative(250);
break;
}
}
return ViewGroup::Key(input);
}
const float friction = 0.92f;
const float stop_threshold = 0.1f;
void 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)) {
ViewGroup::Touch(input2);
}
}
void ScrollView::Draw(UIContext &dc) {
if (!views_.size()) {
ViewGroup::Draw(dc);
return;
}
dc.PushScissor(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 / 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_);
switch (orientation_) {
case ORIENT_HORIZONTAL:
if (vBounds.x2() > bounds_.x2()) {
ScrollTo(pos + vBounds.x2() - bounds_.x2() + overscroll);
}
if (vBounds.x < bounds_.x) {
ScrollTo(pos + (vBounds.x - bounds_.x) - overscroll);
}
break;
case ORIENT_VERTICAL:
if (vBounds.y2() > bounds_.y2()) {
ScrollTo(pos + vBounds.y2() - bounds_.y2() + overscroll);
}
if (vBounds.y < bounds_.y) {
ScrollTo(pos + (vBounds.y - bounds_.y) - overscroll);
}
break;
}
return true;
}
NeighborResult ScrollView::FindScrollNeighbor(View *view, const Point &target, FocusDirection direction, NeighborResult best) {
if (ContainsSubview(view) && views_[0]->IsViewGroup()) {
ViewGroup *vg = static_cast<ViewGroup *>(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 && !rememberPosition_) {
// Since this is no longer shown, forget the scroll position.
// For example, this happens when switching tabs.
ScrollTo(0.0f);
}
}
float ScrollView::GetScrollPosition() {
return scrollPos_;
}
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()) {
return 0.0f;
}
float childSize = orientation_ == ORIENT_VERTICAL ? views_[0]->GetBounds().h : views_[0]->GetBounds().w;
float scrollMax = std::max(0.0f, childSize - (orientation_ == ORIENT_VERTICAL ? bounds_.h : bounds_.w));
Gesture gesture = orientation_ == ORIENT_VERTICAL ? GESTURE_DRAG_VERTICAL : GESTURE_DRAG_HORIZONTAL;
if (scrollTouchId_ >= 0 && gesture_.IsGestureActive(gesture, scrollTouchId_) && bounds_.h > 0) {
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_;
}
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::Update() {
if (visibility_ != V_VISIBLE) {
inertia_ = 0.0f;
}
ViewGroup::Update();
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;
}
}
}
void AnchorLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_);
MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_);
MeasureViews(dc, horiz, vert);
const bool unspecifiedWidth = layoutParams_->width == WRAP_CONTENT && (overflow_ || horiz.type == UNSPECIFIED);
const bool unspecifiedHeight = layoutParams_->height == WRAP_CONTENT && (overflow_ || vert.type == UNSPECIFIED);
if (unspecifiedWidth || unspecifiedHeight) {
// Give everything another chance to size, given the new measurements.
MeasureSpec h = unspecifiedWidth ? MeasureSpec(AT_MOST, measuredWidth_) : horiz;
MeasureSpec v = unspecifiedHeight ? MeasureSpec(AT_MOST, measuredHeight_) : vert;
MeasureViews(dc, h, v);
}
}
void AnchorLayout::MeasureViews(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
for (size_t i = 0; i < views_.size(); i++) {
Size width = WRAP_CONTENT;
Size height = WRAP_CONTENT;
MeasureSpec specW(UNSPECIFIED, measuredWidth_);
MeasureSpec specH(UNSPECIFIED, measuredHeight_);
if (!overflow_) {
if (horiz.type != UNSPECIFIED) {
specW = MeasureSpec(AT_MOST, horiz.size);
}
if (vert.type != UNSPECIFIED) {
specH = MeasureSpec(AT_MOST, vert.size);
}
}
const AnchorLayoutParams *params = views_[i]->GetLayoutParams()->As<AnchorLayoutParams>();
if (params) {
width = params->width;
height = params->height;
if (!params->center) {
if (params->left > NONE && params->right > NONE) {
width = measuredWidth_ - params->left - params->right;
}
if (params->top > NONE && params->bottom > NONE) {
height = measuredHeight_ - params->top - params->bottom;
}
}
if (width >= 0) {
specW = MeasureSpec(EXACTLY, width);
}
if (height >= 0) {
specH = MeasureSpec(EXACTLY, height);
}
}
views_[i]->Measure(dc, specW, specH);
if (layoutParams_->width == WRAP_CONTENT)
measuredWidth_ = std::max(measuredWidth_, views_[i]->GetMeasuredWidth());
if (layoutParams_->height == WRAP_CONTENT)
measuredHeight_ = std::max(measuredHeight_, views_[i]->GetMeasuredHeight());
}
}
void AnchorLayout::Layout() {
for (size_t i = 0; i < views_.size(); i++) {
const AnchorLayoutParams *params = views_[i]->GetLayoutParams()->As<AnchorLayoutParams>();
Bounds vBounds;
vBounds.w = views_[i]->GetMeasuredWidth();
vBounds.h = views_[i]->GetMeasuredHeight();
// Clamp width/height to our own
if (vBounds.w > bounds_.w) vBounds.w = bounds_.w;
if (vBounds.h > bounds_.h) vBounds.h = bounds_.h;
float left = 0, top = 0, right = 0, bottom = 0;
bool center = false;
if (params) {
left = params->left;
top = params->top;
right = params->right;
bottom = params->bottom;
center = params->center;
}
if (left > NONE) {
vBounds.x = bounds_.x + left;
if (center)
vBounds.x -= vBounds.w * 0.5f;
} else if (right > NONE) {
vBounds.x = bounds_.x2() - right - vBounds.w;
if (center) {
vBounds.x += vBounds.w * 0.5f;
}
}
if (top > NONE) {
vBounds.y = bounds_.y + top;
if (center)
vBounds.y -= vBounds.h * 0.5f;
} else if (bottom > NONE) {
vBounds.y = bounds_.y2() - bottom - vBounds.h;
if (center)
vBounds.y += vBounds.h * 0.5f;
}
views_[i]->SetBounds(vBounds);
views_[i]->Layout();
}
}
GridLayout::GridLayout(GridLayoutSettings settings, LayoutParams *layoutParams)
: ViewGroup(layoutParams), settings_(settings), numColumns_(1) {
if (settings.orientation != ORIENT_HORIZONTAL)
ERROR_LOG(SYSTEM, "GridLayout: Vertical layouts not yet supported");
}
void GridLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
MeasureSpecType measureType = settings_.fillCells ? EXACTLY : AT_MOST;
for (size_t i = 0; i < views_.size(); i++) {
views_[i]->Measure(dc, MeasureSpec(measureType, settings_.columnWidth), MeasureSpec(measureType, settings_.rowHeight));
}
// Use the max possible width so AT_MOST gives us the full size.
float maxWidth = (settings_.columnWidth + settings_.spacing) * views_.size() + settings_.spacing;
MeasureBySpec(layoutParams_->width, maxWidth, horiz, &measuredWidth_);
// Okay, got the width we are supposed to adjust to. Now we can calculate the number of columns.
numColumns_ = (measuredWidth_ - settings_.spacing) / (settings_.columnWidth + settings_.spacing);
if (!numColumns_) numColumns_ = 1;
int numRows = (int)(views_.size() + (numColumns_ - 1)) / numColumns_;
float estimatedHeight = (settings_.rowHeight + settings_.spacing) * numRows;
MeasureBySpec(layoutParams_->height, estimatedHeight, vert, &measuredHeight_);
}
void GridLayout::Layout() {
int y = 0;
int x = 0;
int count = 0;
for (size_t i = 0; i < views_.size(); i++) {
const GridLayoutParams *lp = views_[i]->GetLayoutParams()->As<GridLayoutParams>();
Bounds itemBounds, innerBounds;
Gravity grav = lp ? lp->gravity : G_CENTER;
itemBounds.x = bounds_.x + x;
itemBounds.y = bounds_.y + y;
itemBounds.w = settings_.columnWidth;
itemBounds.h = settings_.rowHeight;
ApplyGravity(itemBounds, Margins(0.0f),
views_[i]->GetMeasuredWidth(), views_[i]->GetMeasuredHeight(),
grav, innerBounds);
views_[i]->SetBounds(innerBounds);
views_[i]->Layout();
count++;
if (count == numColumns_) {
count = 0;
x = 0;
y += itemBounds.h + settings_.spacing;
} else {
x += itemBounds.w + settings_.spacing;
}
}
}
std::string GridLayoutList::DescribeText() const {
auto u = GetI18NCategory("UI Elements");
return DescribeListOrdered(u->T("List:"));
}
TabHolder::TabHolder(Orientation orientation, float stripSize, LayoutParams *layoutParams)
: LinearLayout(Opposite(orientation), layoutParams), stripSize_(stripSize) {
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 {
tabStrip_ = new ChoiceStrip(orientation, new LayoutParams(stripSize, WRAP_CONTENT));
tabStrip_->SetTopTabs(true);
Add(tabStrip_);
}
tabStrip_->OnChoice.Handle(this, &TabHolder::OnTabClick);
contents_ = new AnchorLayout(new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
Add(contents_)->SetClip(true);
}
void TabHolder::AddTabContents(const std::string &title, View *tabContents) {
tabContents->ReplaceLayoutParams(new AnchorLayoutParams(FILL_PARENT, FILL_PARENT));
tabs_.push_back(tabContents);
tabStrip_->AddChoice(title);
contents_->Add(tabContents);
if (tabs_.size() > 1)
tabContents->SetVisibility(V_GONE);
// Will be filled in later.
tabTweens_.push_back(nullptr);
}
void TabHolder::SetCurrentTab(int tab, bool skipTween) {
if (tab >= (int)tabs_.size()) {
// Ignore
return;
}
auto setupTween = [&](View *view, AnchorTranslateTween *&tween) {
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);
return EVENT_DONE;
});
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(Point(0.0f, 0.0f));
tabTweens_[tab]->Apply(tabs_[tab]);
} else {
tabTweens_[currentTab_]->Reset(Point(0.0f, 0.0f));
if (orient == ORIENT_HORIZONTAL) {
tabTweens_[tab]->Reset(Point(bounds_.w * dir, 0.0f));
tabTweens_[currentTab_]->Divert(Point(bounds_.w * -dir, 0.0f));
} else {
tabTweens_[tab]->Reset(Point(0.0f, bounds_.h * dir));
tabTweens_[currentTab_]->Divert(Point(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(Point(0.0f, 0.0f));
}
tabs_[tab]->SetVisibility(V_VISIBLE);
currentTab_ = tab;
}
tabStrip_->SetSelection(tab, false);
}
EventReturn 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) {
SetCurrentTab((int)e.a);
}
return EVENT_DONE;
}
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) {
SetCurrentTab(buffer[0], true);
}
break;
}
}
ChoiceStrip::ChoiceStrip(Orientation orientation, LayoutParams *layoutParams)
: LinearLayout(orientation, layoutParams), selected_(0), topTabs_(false) {
SetSpacing(0.0f);
}
void ChoiceStrip::AddChoice(const std::string &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();
}
EventReturn 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).
return 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::HighlightChoice(unsigned int choice){
if (choice < (unsigned int)views_.size()){
Choice(choice)->HighlightChanged(true);
}
};
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);
}
void ChoiceStrip::Draw(UIContext &dc) {
ViewGroup::Draw(dc);
if (topTabs_) {
if (orientation_ == ORIENT_HORIZONTAL)
dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x, bounds_.y2() - 4, bounds_.x2(), bounds_.y2(), dc.theme->itemDownStyle.background.color );
else if (orientation_ == ORIENT_VERTICAL)
dc.Draw()->DrawImageCenterTexel(dc.theme->whiteImage, bounds_.x2() - 4, bounds_.y, bounds_.x2(), bounds_.y2(), dc.theme->itemDownStyle.background.color );
}
}
std::string ChoiceStrip::DescribeText() const {
auto u = GetI18NCategory("UI Elements");
return DescribeListUnordered(u->T("Choices:"));
}
StickyChoice *ChoiceStrip::Choice(int index) {
if ((size_t)index < views_.size())
return static_cast<StickyChoice *>(views_[index]);
return nullptr;
}
ListView::ListView(ListAdaptor *a, std::set<int> 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<EventReturn(EventParams&)> 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<EventReturn(EventParams&)> callback) {
Choice *choice = (Choice *)view;
choice->OnClick.Add(callback);
return EVENT_DONE;
}
} // namespace UI