Files
ppsspp/Common/UI/ViewGroup.cpp
T
Henrik RydgårdandClaude Sonnet 5.5 cbb553ccc7 Build as C++20
Switch CMake (including the Core and GPU compile features and the Android
flag), the Visual Studio projects (stdcpp20), the legacy Android NDK
makefiles, the libretro makefiles and .ycm_extra_conf.py from C++17 to
C++20.

[=] lambdas that use this implicitly become [=, this], to avoid
-Wdeprecated-this-capture.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 00:02:06 +01:00

1145 lines
36 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 + margins.left + (outer.w - margins.horiz() - 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 + margins.top + (outer.h - margins.vert() - 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;
SetParentOf(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_);
}
if (roundCorners_ != RoundCorners::None) {
dc.FillRoundRect(bg_, bounds_, dc.GetTheme().choiceRoundRadius, roundCorners_);
} else {
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;
}
// Directional focus moves, the way Android's FocusFinder (and the W3C spatial navigation draft)
// do them. Only views that lie in the direction of the move are candidates. Of those, one that the
// origin's "beam" (its extent across the direction of the move) hits beats one off to the side,
// and otherwise the nearest wins, with distance along the move counting far more than distance
// across it. Measuring to edges rather than centers is what keeps a wide view sitting between
// two narrow ones from being skipped.
static bool IsInDirection(const Bounds &origin, const Bounds &dest, FocusMove direction) {
switch (direction) {
case FocusMove::LEFT:
return (origin.x2() > dest.x2() || origin.x >= dest.x2()) && origin.x > dest.x;
case FocusMove::RIGHT:
return (origin.x < dest.x || origin.x2() <= dest.x) && origin.x2() < dest.x2();
case FocusMove::UP:
return (origin.y2() > dest.y2() || origin.y >= dest.y2()) && origin.y > dest.y;
case FocusMove::DOWN:
return (origin.y < dest.y || origin.y2() <= dest.y) && origin.y2() < dest.y2();
default:
return false;
}
}
// Whether dest is past the origin's leading edge entirely.
static bool IsEntirelyInDirection(const Bounds &origin, const Bounds &dest, FocusMove direction) {
switch (direction) {
case FocusMove::LEFT: return origin.x >= dest.x2();
case FocusMove::RIGHT: return origin.x2() <= dest.x;
case FocusMove::UP: return origin.y >= dest.y2();
case FocusMove::DOWN: return origin.y2() <= dest.y;
default: return false;
}
}
static bool InBeam(const Bounds &origin, const Bounds &dest, FocusMove direction) {
if (direction == FocusMove::LEFT || direction == FocusMove::RIGHT) {
return dest.y2() > origin.y && dest.y < origin.y2();
} else {
return dest.x2() > origin.x && dest.x < origin.x2();
}
}
// From the origin's leading edge to the near edge of dest.
static float MajorAxisDistance(const Bounds &origin, const Bounds &dest, FocusMove direction) {
switch (direction) {
case FocusMove::LEFT: return std::max(0.0f, origin.x - dest.x2());
case FocusMove::RIGHT: return std::max(0.0f, dest.x - origin.x2());
case FocusMove::UP: return std::max(0.0f, origin.y - dest.y2());
case FocusMove::DOWN: return std::max(0.0f, dest.y - origin.y2());
default: return 0.0f;
}
}
// From the origin's leading edge to the far edge of dest.
static float MajorAxisDistanceToFarEdge(const Bounds &origin, const Bounds &dest, FocusMove direction) {
switch (direction) {
case FocusMove::LEFT: return std::max(1.0f, origin.x - dest.x);
case FocusMove::RIGHT: return std::max(1.0f, dest.x2() - origin.x2());
case FocusMove::UP: return std::max(1.0f, origin.y - dest.y);
case FocusMove::DOWN: return std::max(1.0f, dest.y2() - origin.y2());
default: return 1.0f;
}
}
// Across the move, from the origin's focus position (normally its center) to the nearest part of
// dest, so zero if dest is straight ahead. Android measures to dest's center instead, which lets a
// narrow view further on beat a wide one that's right there.
static float MinorAxisDistance(const Point2D &originPos, const Bounds &dest, FocusMove direction) {
if (direction == FocusMove::LEFT || direction == FocusMove::RIGHT) {
return std::max(0.0f, std::max(dest.y - originPos.y, originPos.y - dest.y2()));
} else {
return std::max(0.0f, std::max(dest.x - originPos.x, originPos.x - dest.x2()));
}
}
// Whether a, which is in the beam, beats b, which isn't.
static bool BeamBeats(const Bounds &origin, const Bounds &a, const Bounds &b, FocusMove direction) {
const bool aInBeam = InBeam(origin, a, direction);
const bool bInBeam = InBeam(origin, b, direction);
if (bInBeam || !aInBeam) {
return false;
}
// b overlaps the origin along the move, so it's only partly in that direction.
if (!IsEntirelyInDirection(origin, b, direction)) {
return true;
}
// Sideways moves always prefer the beam: things in a row line up.
if (direction == FocusMove::LEFT || direction == FocusMove::RIGHT) {
return true;
}
// Up and down, something off to the side in the next row beats something in the beam further on.
return MajorAxisDistance(origin, a, direction) < MajorAxisDistanceToFarEdge(origin, b, direction);
}
static float WeightedDistance(const Point2D &originPos, const Bounds &origin, const Bounds &dest, FocusMove direction) {
const float major = MajorAxisDistance(origin, dest, direction);
const float minor = MinorAxisDistance(originPos, dest, direction);
// Android's weighting, which makes being straight ahead count for a lot.
return 13.0f * major * major + minor * minor;
}
bool IsBetterFocusCandidate(const Bounds &origin, const Point2D &originPos, const Bounds &candidate, const Bounds *best, FocusMove direction) {
if (!IsInDirection(origin, candidate, direction)) {
return false;
}
if (!best) {
return true;
}
if (BeamBeats(origin, candidate, *best, direction)) {
return true;
}
if (BeamBeats(origin, *best, candidate, direction)) {
return false;
}
return WeightedDistance(originPos, origin, candidate, direction) < WeightedDistance(originPos, origin, *best, direction);
}
static bool CanTakeFocus(const View *view) {
return view->CanBeFocused() && view->IsEnabled() && view->GetVisibility() == V_VISIBLE;
}
float GetTargetScore(const Point2D &originPos, int originIndex, const View *origin, const View *destination, FocusMove direction) {
// Skip labels and things like that.
if (!CanTakeFocus(destination))
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);
const float dirX = dx / distance;
const float dirY = dy / distance;
switch (direction) {
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:
return 10.0f / std::max(1.0f, distance);
default:
// The arrow directions go through IsBetterFocusCandidate.
ERROR_LOG(Log::UI, "Invalid focus direction");
return 0.0f;
}
}
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:
{
// Candidates are compared with the best so far rather than scored, so score stays unused.
const Bounds &origin = view->GetBounds();
const Point2D originPos = view->GetFocusPosition(direction);
for (View *candidate : views_) {
if (candidate == view || !CanTakeFocus(candidate))
continue;
const Bounds *best = result.view ? &result.view->GetBounds() : nullptr;
if (IsBetterFocusCandidate(origin, originPos, candidate->GetBounds(), best, direction)) {
result.view = candidate;
}
}
// Then see if any of the children contain any better candidates.
for (View *v : views_) {
if (v->IsViewGroup()) {
result = static_cast<ViewGroup *>(v)->FindNeighbor(view, direction, result);
}
}
return result;
}
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);
}
}
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 - (float)padding_.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_.vert() > vert.size - weightZeroSum) {
// Try again, this time with AT_MOST.
view->Measure(dc, h - (float)margins.horiz() - (float)padding_.horiz(), vert - (float)padding_.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 + 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;
// 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 - padding_.horiz() - 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() + margins.horiz();
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;
// 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 - padding_.vert() - 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() + margins.vert();
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 = bounds.h - padding_.vert();
} else {
pos = bounds.y + padding_.top;
itemBounds.x = bounds.x + padding_.left;
itemBounds.w = bounds.w - padding_.horiz();
}
// Move the children as a group along the layout direction, if they don't fill it.
const Gravity contentGravity = (Gravity)((int)contentGravity_ & (int)(orientation_ == ORIENT_HORIZONTAL ? Gravity::G_HORIZMASK : Gravity::G_VERTMASK));
if (contentGravity != Gravity::G_TOPLEFT) {
float contentSize = 0.0f;
int numVisible = 0;
for (const View *view : views_) {
if (view->GetVisibility() == V_GONE)
continue;
const LinearLayoutParams *linLayoutParams = view->GetLayoutParams()->As<LinearLayoutParams>();
const Margins margins = (linLayoutParams && linLayoutParams->HasMargins()) ? linLayoutParams->margins : defaultMargins_;
contentSize += orientation_ == ORIENT_HORIZONTAL ? view->GetMeasuredWidth() + margins.horiz() : view->GetMeasuredHeight() + margins.vert();
numVisible++;
}
if (numVisible > 0) {
contentSize += spacing_ * (numVisible - 1);
}
const float freeSpace = (orientation_ == ORIENT_HORIZONTAL ? bounds.w - padding_.horiz() : bounds.h - padding_.vert()) - contentSize;
if (freeSpace > 0.0f) {
pos += (contentGravity == Gravity::G_HCENTER || contentGravity == Gravity::G_VCENTER) ? freeSpace * 0.5f : freeSpace;
}
}
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);
}
}
// Applies rounding to the corners of the child views, based on their position in the layout and the orientation. Masked by the outerCorners.
// Ignores invisible views.
void LinearLayout::ApplyRoundCorners(RoundCorners outerCorners) {
// Easy case first: There's spacing, just apply round corners to all subviews.
if (spacing_ > 0.0f) {
for (View *view : views_) {
view->SetRoundCorners(outerCorners);
}
return;
}
// Harder: There's no space, so only the first and last view will get rounded corners.
bool seenVisible = false;
View *lastVisible = nullptr;
for (View *view : views_) {
if (view->GetVisibility() == V_GONE) {
continue;
}
if (!seenVisible) {
// First visible view, round the leading edge.
view->SetRoundCorners((RoundCorners)((u32)(orientation_ == ORIENT_HORIZONTAL ? RoundCorners::Left : RoundCorners::Top) & (u32)outerCorners));
seenVisible = true;
} else {
view->SetRoundCorners(RoundCorners::None);
}
lastVisible = view;
}
if (lastVisible) {
lastVisible->SetRoundCorners(lastVisible->GetRoundCorners() | (RoundCorners)((u32)(orientation_ == ORIENT_HORIZONTAL ? RoundCorners::Right : RoundCorners::Bottom) & (u32)outerCorners));
}
}
std::string LinearLayoutList::DescribeText() const {
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
return DescribeListOrdered(u->T("List:"));
}
ChoiceGroup::ChoiceGroup(std::string_view title, LayoutParams *layoutParams)
: LinearLayout(ORIENT_VERTICAL, layoutParams ? layoutParams : new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)), title_(title) {
if (!title.empty()) {
Add(new ItemHeader(title));
}
SetSpacing(0.0f);
}
ChoiceGroupList::ChoiceGroupList(LayoutParams *layoutParams)
: LinearLayoutList(ORIENT_VERTICAL, layoutParams ? layoutParams : new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)) {
SetPadding(Margins(12, 12, 8, 12));
SetSpacing(24.0f);
}
void ChoiceGroup::Draw(UIContext &dc) {
if (views_.empty()) {
LinearLayout::Draw(dc);
return;
}
// Maybe we should require the user to Finish() the ChoiceGroup instead of running this every frame.
// At least it's not recursive.
ApplyRoundCorners(RoundCorners::All);
// Draw a roundrect as background.
LinearLayout::Draw(dc);
// Draw a line under all items except the last (to be reconsidered as part of visual design work).
/*
for (size_t i = 0; i < views_.size() - 1; i++) {
const Bounds &titleBounds = views_[i]->GetBounds();
dc.FillRect(dc.)
}*/
}
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++) {
if (views_[i]->GetVisibility() == V_GONE)
continue;
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++) {
if (views_[i]->GetVisibility() == V_GONE)
continue;
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) {}
// Grid children are mostly given LinearLayoutParams, so take the margins from those too.
static Margins GetGridChildMargins(const View *view) {
if (const GridLayoutParams *lp = view->GetLayoutParams()->As<GridLayoutParams>()) {
return lp->margins;
}
if (const LinearLayoutParams *lp = view->GetLayoutParams()->As<LinearLayoutParams>()) {
if (lp->HasMargins()) {
return lp->margins;
}
}
return Margins(0);
}
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) {
const Margins margins = GetGridChildMargins(views_[i]);
views_[i]->Measure(dc, MeasureSpec(measureType, settings_.columnWidth - margins.horiz()), MeasureSpec(measureType, settings_.rowHeight - margins.vert()));
numItems++;
}
}
// Use the max possible width so AT_MOST gives us the full size.
float maxWidth = (settings_.columnWidth + settings_.spacing) * numItems + settings_.spacing + padding_.horiz();
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_ - padding_.horiz() + settings_.spacing) / (settings_.columnWidth + settings_.spacing);
if (numColumns_ <= 0) numColumns_ = 1;
int numRows = (numItems + (numColumns_ - 1)) / numColumns_;
float estimatedHeight = (settings_.rowHeight + settings_.spacing) * numRows + padding_.vert();
MeasureBySpec(layoutParams_->height, estimatedHeight, vert, &measuredHeight_);
}
void GridLayout::Layout() {
int y = padding_.top;
int x = padding_.left;
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, GetGridChildMargins(views_[i]),
views_[i]->GetMeasuredWidth(), views_[i]->GetMeasuredHeight(),
grav, innerBounds);
views_[i]->SetBounds(innerBounds);
views_[i]->Layout();
count++;
if (count == numColumns_) {
count = 0;
x = padding_.left;
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_ = Add(new CollapsibleHeader(open_, title));
header_->OnClick.Add([=, this](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