mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 09:45:24 +02:00
Unlike the directional moves, this doesn't look at where anything ended up on screen - it walks the hierarchy in the order views were added, flattening nested groups in place. That's what makes it predictable in the layouts where "what's to the right of this" has no good answer. A view is a stop if it's focusable and enabled, the same test the directional moves apply, so the two agree on what's reachable. Hidden subtrees are skipped whole, which is what keeps a TabHolder's inactive tabs - V_GONE rather than removed - out of the order without any special casing. Containers are gated on visibility only, not enabled, matching Key/Touch/Axis: disabling a container doesn't stop its children being interactive anywhere else either. Ctrl+Tab stays with ChoiceStrip, which uses it to switch tabs. focusMoves now holds FocusMove rather than raw keycodes, so the direction is decided in one place while the modifiers are still around, and a held key repeats in the direction it was originally pressed with - the synthesized repeat has no modifiers of its own. That also retires the keycode switch in UpdateViewHierarchy and IsScrollKey, which had no other callers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
1020 lines
30 KiB
C++
1020 lines
30 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/ScrollView.h"
|
|
#include "Common/UI/Tween.h"
|
|
#include "Common/UI/Root.h"
|
|
#include "Common/UI/View.h"
|
|
#include "Common/UI/UIScreen.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 {
|
|
|
|
void ApplyGravity(const Bounds &outer, const Margins &margins, float w, float h, Gravity gravity, Bounds &inner) {
|
|
inner.w = w;
|
|
inner.h = h;
|
|
|
|
switch ((Gravity)((int)gravity & (int)Gravity::G_HORIZMASK)) {
|
|
case Gravity::G_LEFT: inner.x = outer.x + margins.left; break;
|
|
case Gravity::G_RIGHT: inner.x = outer.x + outer.w - w - margins.right; break;
|
|
case Gravity::G_HCENTER: inner.x = outer.x + (outer.w - w) * 0.5f; break;
|
|
default: break;
|
|
}
|
|
|
|
switch ((Gravity)((int)gravity & (int)Gravity::G_VERTMASK)) {
|
|
case Gravity::G_TOP: inner.y = outer.y + margins.top; break;
|
|
case Gravity::G_BOTTOM: inner.y = outer.y + outer.h - h - margins.bottom; break;
|
|
case Gravity::G_VCENTER: inner.y = outer.y + (outer.h - h) * 0.5f; break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
ViewGroup::~ViewGroup() {
|
|
// Tear down the contents recursively.
|
|
Clear();
|
|
}
|
|
|
|
void ViewGroup::Recurse(std::function<void(View *)> func) {
|
|
for (View *view : views_) {
|
|
func(view);
|
|
view->Recurse(func);
|
|
}
|
|
}
|
|
|
|
void ViewGroup::RemoveSubview(View *subView) {
|
|
// loop counter needed, so can't convert loop.
|
|
for (size_t i = 0; i < views_.size(); i++) {
|
|
if (views_[i] == subView) {
|
|
views_.erase(views_.begin() + i);
|
|
delete subView;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ViewGroup::ContainsSubview(const View *view) const {
|
|
for (const View *subview : views_) {
|
|
if (subview == view || subview->ContainsSubview(view))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int ViewGroup::IndexOfSubview(const View *view) const {
|
|
int index = 0;
|
|
for (const View *subview : views_) {
|
|
if (subview == view)
|
|
return index;
|
|
index++;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void ViewGroup::Clear() {
|
|
for (View *view : views_) {
|
|
delete view;
|
|
}
|
|
views_.clear();
|
|
}
|
|
|
|
void ViewGroup::PersistData(PersistStatus status, std::string anonId, PersistMap &storage) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
bool ViewGroup::Touch(const TouchInput &input) {
|
|
bool any = false;
|
|
for (View *view : views_) {
|
|
// TODO: If there is a transformation active, transform input coordinates accordingly.
|
|
if (view->GetVisibility() == V_VISIBLE) {
|
|
bool touch = view->Touch(input);
|
|
any = any || touch;
|
|
if (exclusiveTouch_ && touch && (input.flags & TouchInputFlags::DOWN)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (clickableBackground_) {
|
|
return any || bounds_.Contains(input.x, input.y);
|
|
} else {
|
|
return any;
|
|
}
|
|
}
|
|
|
|
void ViewGroup::Query(float x, float y, std::vector<View *> &list) {
|
|
if (bounds_.Contains(x, y)) {
|
|
list.push_back(this);
|
|
for (View *view : views_) {
|
|
view->Query(x, y, list);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ViewGroup::Key(const KeyInput &input) {
|
|
bool ret = false;
|
|
for (View *view : views_) {
|
|
// TODO: If there is a transformation active, transform input coordinates accordingly.
|
|
if (view->GetVisibility() == V_VISIBLE)
|
|
ret = ret || view->Key(input);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void ViewGroup::Axis(const AxisInput &input) {
|
|
for (View *view : views_) {
|
|
// TODO: If there is a transformation active, transform input coordinates accordingly.
|
|
if (view->GetVisibility() == V_VISIBLE)
|
|
view->Axis(input);
|
|
}
|
|
}
|
|
|
|
void ViewGroup::DeviceLost() {
|
|
for (View *view : views_) {
|
|
view->DeviceLost();
|
|
}
|
|
}
|
|
|
|
void ViewGroup::DeviceRestored(Draw::DrawContext *draw) {
|
|
for (View *view : views_) {
|
|
view->DeviceRestored(draw);
|
|
}
|
|
}
|
|
|
|
bool ViewGroup::ReplaceSubview(View *view, View *newView) {
|
|
for (int i = 0; i < (int)views_.size(); i++) {
|
|
if (views_[i] == view) {
|
|
views_[i] = newView;
|
|
delete view;
|
|
return true;
|
|
}
|
|
}
|
|
delete newView;
|
|
return false;
|
|
}
|
|
|
|
void ViewGroup::Draw(UIContext &dc) {
|
|
if (hasDropShadow_) {
|
|
// Darken things behind.
|
|
dc.FillRect(UI::Drawable(0x60000000), dc.GetBounds().Expand(dropShadowExpand_));
|
|
const float dropsize = 30.0f;
|
|
dc.Draw()->DrawImage4Grid(dc.GetTheme().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';
|
|
}
|
|
const std::string desc = ss.str();
|
|
return desc.empty() ? "empty viewgroup" : desc;
|
|
}
|
|
|
|
std::string ViewGroup::DescribeListUnordered(std::string_view 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(std::string_view 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(FocusFlags cause) {
|
|
if (!CanBeFocused() && !views_.empty()) {
|
|
for (View *view : views_) {
|
|
if (view->SetFocus(cause))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ViewGroup::SubviewFocused(View *queryView) {
|
|
for (View *view : views_) {
|
|
if (view == queryView || view->SubviewFocused(queryView))
|
|
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 minW = std::min(a.w, b.w);
|
|
float overlap = minMax - maxMin;
|
|
if (overlap < 0.0f || minW <= 0.0f)
|
|
return 0.0f;
|
|
else
|
|
return std::min(1.0f, overlap / minW);
|
|
}
|
|
|
|
// 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 minH = std::min(a.h, b.h);
|
|
float overlap = minMax - maxMin;
|
|
if (overlap < 0.0f || minH <= 0.0f)
|
|
return 0.0f;
|
|
else
|
|
return std::min(1.0f, overlap / minH);
|
|
}
|
|
|
|
float GetTargetScore(const Point2D &originPos, int originIndex, const View *origin, const View *destination, FocusMove 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;
|
|
|
|
Point2D destPos = destination->GetFocusPosition(Opposite(direction));
|
|
|
|
float dx = destPos.x - originPos.x;
|
|
float dy = destPos.y - originPos.y;
|
|
|
|
const float distance = std::max(sqrtf(dx*dx + dy*dy), 0.001f);
|
|
float overlap = 0.0f;
|
|
const float dirX = dx / distance;
|
|
const float dirY = dy / distance;
|
|
|
|
bool wrongDirection = false;
|
|
bool vertical = false;
|
|
const float horizOverlap = HorizontalOverlap(origin->GetBounds(), destination->GetBounds());
|
|
const float vertOverlap = VerticalOverlap(origin->GetBounds(), destination->GetBounds());
|
|
if (horizOverlap == 1.0f && vertOverlap == 1.0f) {
|
|
if (direction != FocusMove::PREV_PAGE && direction != FocusMove::NEXT_PAGE) {
|
|
// INFO_LOG(Log::UI, "Contain overlap: %s, %s", origin->Tag().c_str(), destination->Tag().c_str());
|
|
return 0.0f;
|
|
}
|
|
}
|
|
float originSize = 0.0f;
|
|
switch (direction) {
|
|
case FocusMove::LEFT:
|
|
overlap = vertOverlap;
|
|
originSize = origin->GetBounds().w;
|
|
if (dirX > 0.0f) {
|
|
wrongDirection = true;
|
|
}
|
|
break;
|
|
case FocusMove::UP:
|
|
overlap = horizOverlap;
|
|
originSize = origin->GetBounds().h;
|
|
if (dirY > 0.0f) {
|
|
wrongDirection = true;
|
|
}
|
|
vertical = true;
|
|
break;
|
|
case FocusMove::RIGHT:
|
|
overlap = vertOverlap;
|
|
originSize = origin->GetBounds().w;
|
|
if (dirX < 0.0f) {
|
|
wrongDirection = true;
|
|
}
|
|
break;
|
|
case FocusMove::DOWN:
|
|
overlap = horizOverlap;
|
|
originSize = origin->GetBounds().h;
|
|
if (dirY < 0.0f) {
|
|
wrongDirection = true;
|
|
}
|
|
vertical = true;
|
|
break;
|
|
case FocusMove::FIRST:
|
|
if (originIndex == -1)
|
|
return 0.0f;
|
|
if (dirX > 0.0f || dirY > 0.0f)
|
|
return 0.0f;
|
|
// More distance is good.
|
|
return distance;
|
|
case FocusMove::LAST:
|
|
if (originIndex == -1)
|
|
return 0.0f;
|
|
if (dirX < 0.0f || dirY < 0.0f)
|
|
return 0.0f;
|
|
// More distance is good.
|
|
return distance;
|
|
case FocusMove::PREV_PAGE:
|
|
case FocusMove::NEXT_PAGE:
|
|
// Not always, but let's go with the bonus on height.
|
|
vertical = true;
|
|
break;
|
|
case FocusMove::PREV:
|
|
case FocusMove::NEXT:
|
|
ERROR_LOG(Log::UI, "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;
|
|
}
|
|
}
|
|
|
|
static float GetDirectionScore(int originIndex, const View *origin, View *destination, FocusMove direction) {
|
|
Point2D originPos = origin->GetFocusPosition(direction);
|
|
return GetTargetScore(originPos, originIndex, origin, destination, direction);
|
|
}
|
|
|
|
NeighborResult ViewGroup::FindNeighbor(View *view, FocusMove direction, NeighborResult result) {
|
|
if (!IsEnabled()) {
|
|
INFO_LOG(Log::UI, "Not enabled");
|
|
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;
|
|
}
|
|
}
|
|
|
|
switch (direction) {
|
|
case FocusMove::UP:
|
|
case FocusMove::LEFT:
|
|
case FocusMove::RIGHT:
|
|
case FocusMove::DOWN:
|
|
case FocusMove::FIRST:
|
|
case FocusMove::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 *v : views_) {
|
|
if (v->IsViewGroup()) {
|
|
ViewGroup *vg = static_cast<ViewGroup *>(v);
|
|
if (vg)
|
|
result = vg->FindNeighbor(view, direction, result);
|
|
}
|
|
}
|
|
|
|
// Boost neighbors with the same parent
|
|
if (num != -1) {
|
|
//result.score += 100.0f;
|
|
}
|
|
return result;
|
|
}
|
|
case FocusMove::PREV_PAGE:
|
|
case FocusMove::NEXT_PAGE:
|
|
return FindScrollNeighbor(view, Point2D(INFINITY, INFINITY), direction, result);
|
|
case FocusMove::PREV:
|
|
// If view not found, no neighbor to find.
|
|
if (num == -1)
|
|
return NeighborResult(nullptr, 0.0f);
|
|
return NeighborResult(views_[(num + views_.size() - 1) % views_.size()], 0.0f);
|
|
case FocusMove::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:
|
|
ERROR_LOG(Log::UI, "Bad focus direction %d", (int)direction);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
void ViewGroup::CollectTabOrder(std::vector<View *> *outViews) const {
|
|
for (View *view : views_) {
|
|
// Gate on visibility only, like Key/Touch/Axis do - a container being disabled doesn't
|
|
// stop its children from being interactive elsewhere, so it shouldn't here either.
|
|
if (view->GetVisibility() != V_VISIBLE) {
|
|
continue;
|
|
}
|
|
if (view->IsViewGroup()) {
|
|
// A group can be focusable itself (rare), in which case it's a stop and we still
|
|
// descend into it - same as arrow navigation, which considers both.
|
|
ViewGroup *vg = static_cast<ViewGroup *>(view);
|
|
if (vg->CanBeFocused() && vg->IsEnabled()) {
|
|
outViews->push_back(vg);
|
|
}
|
|
vg->CollectTabOrder(outViews);
|
|
} else if (view->CanBeFocused() && view->IsEnabled()) {
|
|
outViews->push_back(view);
|
|
}
|
|
}
|
|
}
|
|
|
|
NeighborResult ViewGroup::FindScrollNeighbor(View *view, const Point2D &target, FocusMove 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; // Total sum of weights
|
|
float weightZeroSum = 0.0f; // Sum of sizes of things with weight 0.0, a bit confusingly named
|
|
|
|
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() - (float)padding.vert());
|
|
if (horiz.type == AT_MOST && view->GetMeasuredWidth() + margins.horiz() + padding.horiz() > horiz.size - weightZeroSum) {
|
|
// Try again, this time with AT_MOST.
|
|
view->Measure(dc, horiz, v - (float)margins.vert() - (float)padding.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() - (float)padding.horiz(), MeasureSpec(UNSPECIFIED, measuredHeight_));
|
|
if (vert.type == AT_MOST && view->GetMeasuredHeight() + margins.vert() + padding.horiz() > vert.size - weightZeroSum) {
|
|
// Try again, this time with AT_MOST.
|
|
view->Measure(dc, h - (float)margins.horiz() - (float)padding.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); // +(orientation_ == ORIENT_HORIZONTAL) ? padding.horiz() : padding.vert();
|
|
|
|
// 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 + padding.horiz(), 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 + padding.horiz();
|
|
|
|
// Redistribute the stretchy ones! and remeasure the children!
|
|
for (View *view : views_) {
|
|
if (view->GetVisibility() == V_GONE)
|
|
continue;
|
|
// FILL_PARENT is not appropriate in this direction. It gets ignored though.
|
|
// We have a bit too many of these due to the hack in the ClickableItem constructor.
|
|
// _dbg_assert_(view->GetLayoutParams()->width != UI::FILL_PARENT);
|
|
|
|
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;
|
|
if (weightSum == 0.0f) {
|
|
// We must have gotten an inf.
|
|
unit = 1.0f;
|
|
}
|
|
MeasureSpec h(AT_MOST, unit * linLayoutParams->weight - margins.horiz());
|
|
if (horiz.type == EXACTLY) {
|
|
h.type = EXACTLY;
|
|
}
|
|
view->Measure(dc, h, v - (float)margins.vert() - (float)padding.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 + padding.vert(), vert, &measuredHeight_);
|
|
} else {
|
|
MeasureBySpec(layoutParams_->height, weightZeroSum + padding.vert(), 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 + padding.vert();
|
|
|
|
// Redistribute the stretchy ones! and remeasure the children!
|
|
for (View *view : views_) {
|
|
if (view->GetVisibility() == V_GONE)
|
|
continue;
|
|
// FILL_PARENT is not appropriate in this direction. It gets ignored though.
|
|
// We have a bit too many of these due to the hack in the ClickableItem constructor.
|
|
// _dbg_assert_(view->GetLayoutParams()->height != UI::FILL_PARENT);
|
|
|
|
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;
|
|
if (weightSum == 0.0f) {
|
|
// We must have gotten an inf.
|
|
unit = 1.0f;
|
|
}
|
|
MeasureSpec v(AT_MOST, unit * linLayoutParams->weight - margins.vert());
|
|
if (vert.type == EXACTLY) {
|
|
v.type = EXACTLY;
|
|
}
|
|
view->Measure(dc, h - (float)margins.horiz() - (float)padding.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 + padding.horiz(), 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 + padding.left;
|
|
itemBounds.y = bounds.y + padding.top;
|
|
itemBounds.h = measuredHeight_ - padding.vert();
|
|
} else {
|
|
pos = bounds.y + padding.top;
|
|
itemBounds.x = bounds.x + padding.left;
|
|
itemBounds.w = measuredWidth_ - padding.horiz();
|
|
}
|
|
|
|
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 = 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(I18NCat::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) * 0.5f;
|
|
bounds.y = bounds_.y + (measuredWidth_ - h) * 0.5f;
|
|
views_[i]->SetBounds(bounds);
|
|
}
|
|
}
|
|
|
|
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->centering & Centering::Horizontal)) {
|
|
if (params->left > NONE && params->right > NONE) {
|
|
width = measuredWidth_ - params->left - params->right;
|
|
}
|
|
}
|
|
if (!(params->centering & Centering::Vertical)) {
|
|
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());
|
|
}
|
|
}
|
|
|
|
static void ApplyAnchorLayoutParams(float measuredWidth, float measuredHeight, const Bounds &container, const AnchorLayoutParams *params, Bounds *vBounds) {
|
|
vBounds->w = measuredWidth;
|
|
vBounds->h = measuredHeight;
|
|
|
|
// Clamp width/height to our own
|
|
if (vBounds->w > container.w) vBounds->w = container.w;
|
|
if (vBounds->h > container.h) vBounds->h = container.h;
|
|
|
|
float left = 0, top = 0, right = 0, bottom = 0;
|
|
Centering centering = Centering::None;
|
|
if (params) {
|
|
left = params->left;
|
|
top = params->top;
|
|
right = params->right;
|
|
bottom = params->bottom;
|
|
centering = params->centering;
|
|
}
|
|
|
|
if (left > NONE) {
|
|
vBounds->x = container.x + left;
|
|
if (centering & Centering::Horizontal)
|
|
vBounds->x -= vBounds->w * 0.5f;
|
|
} else if (right > NONE) {
|
|
vBounds->x = container.x2() - right - vBounds->w;
|
|
if (centering & Centering::Horizontal) {
|
|
vBounds->x += vBounds->w * 0.5f;
|
|
}
|
|
} else {
|
|
// Both left and right are NONE. Center.
|
|
vBounds->x = (container.w - vBounds->w) / 2.0f + container.x;
|
|
}
|
|
|
|
if (top > NONE) {
|
|
vBounds->y = container.y + top;
|
|
if (centering & Centering::Vertical)
|
|
vBounds->y -= vBounds->h * 0.5f;
|
|
} else if (bottom > NONE) {
|
|
vBounds->y = container.y2() - bottom - vBounds->h;
|
|
if (centering & Centering::Vertical)
|
|
vBounds->y += vBounds->h * 0.5f;
|
|
} else {
|
|
// Both top and bottom are NONE. Center.
|
|
vBounds->y = (container.h - vBounds->h) / 2.0f + container.y;
|
|
}
|
|
}
|
|
|
|
void AnchorLayout::Layout() {
|
|
for (size_t i = 0; i < views_.size(); i++) {
|
|
const AnchorLayoutParams *params = views_[i]->GetLayoutParams()->As<AnchorLayoutParams>();
|
|
Bounds vBounds;
|
|
ApplyAnchorLayoutParams(views_[i]->GetMeasuredWidth(), views_[i]->GetMeasuredHeight(), bounds_, params, &vBounds);
|
|
views_[i]->SetBounds(vBounds);
|
|
views_[i]->Layout();
|
|
}
|
|
}
|
|
|
|
GridLayout::GridLayout(GridLayoutSettings settings, LayoutParams *layoutParams)
|
|
: ViewGroup(layoutParams), settings_(settings) {
|
|
if (settings.orientation != ORIENT_HORIZONTAL)
|
|
ERROR_LOG(Log::UI, "GridLayout: Vertical layouts not yet supported");
|
|
}
|
|
|
|
void GridLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) {
|
|
MeasureSpecType measureType = settings_.fillCells ? EXACTLY : AT_MOST;
|
|
|
|
int numItems = 0;
|
|
for (size_t i = 0; i < views_.size(); i++) {
|
|
if (views_[i]->GetVisibility() != V_GONE) {
|
|
views_[i]->Measure(dc, MeasureSpec(measureType, settings_.columnWidth), MeasureSpec(measureType, settings_.rowHeight));
|
|
numItems++;
|
|
}
|
|
}
|
|
|
|
// Use the max possible width so AT_MOST gives us the full size.
|
|
float maxWidth = (settings_.columnWidth + settings_.spacing) * numItems + 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 = (numItems + (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++) {
|
|
if (views_[i]->GetVisibility() == V_GONE)
|
|
continue;
|
|
|
|
const GridLayoutParams *lp = views_[i]->GetLayoutParams()->As<GridLayoutParams>();
|
|
Bounds itemBounds, innerBounds;
|
|
Gravity grav = lp ? lp->gravity : 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(I18NCat::UI_ELEMENTS);
|
|
return DescribeListOrdered(u->T("List:"));
|
|
}
|
|
|
|
CollapsibleSection::CollapsibleSection(std::string_view title, LayoutParams *layoutParams) : LinearLayout(ORIENT_VERTICAL, layoutParams) {
|
|
open_ = &localOpen_;
|
|
SetSpacing(0.0f);
|
|
|
|
header_ = new CollapsibleHeader(open_, title);
|
|
views_.push_back(header_);
|
|
header_->OnClick.Add([=](UI::EventParams &) {
|
|
// Change the visibility of all children except the first one.
|
|
// Later maybe try something more ambitious.
|
|
UpdateVisibility();
|
|
});
|
|
}
|
|
|
|
void CollapsibleSection::Update() {
|
|
ViewGroup::Update();
|
|
header_->SetHasSubitems(views_.size() > 1);
|
|
}
|
|
|
|
void CollapsibleSection::UpdateVisibility() {
|
|
const bool open = *open_;
|
|
// Skipping over the header, we start from 1, not 0.
|
|
for (size_t i = 1; i < views_.size(); i++) {
|
|
views_[i]->SetVisibility(open ? V_VISIBLE : V_GONE);
|
|
}
|
|
}
|
|
|
|
} // namespace UI
|