mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 23:10:33 +02:00
Replaces the center-to-center scoring, which skipped a wide view between narrow ones, since its center is far away even when its edge is right there. Now it works like Android's FocusFinder: only views in the direction of the move are candidates, ones in the origin's beam beat ones off to the side (going up or down, unless those are in a nearer row), and otherwise the nearest wins, with distance along the move weighted 13 times distance across it. Unlike Android, the distance across is to the candidate's nearest edge rather than its center. First/last and page moves keep their scoring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1068 lines
33 KiB
C++
1068 lines
33 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;
|
|
}
|
|
|
|
// 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, 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
|