From db9e1aa30f75f47fae0ddf288f787598ba3601aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 26 Oct 2026 08:37:24 +0100 Subject: [PATCH] UI: Add FlowLayout, use it for the game browser toolbar FlowLayout lays out its children left to right at their natural size and starts a new row when the next one doesn't fit, instead of squeezing them. Weighted children fill what's left of their row. Like LinearLayout, it has SetPadding for space inside its edges. On narrow screens the toolbar buttons used to run off the edge; now they wrap onto a second row. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/UI/ViewGroup.cpp | 124 ++++++++++++++++++++++++++++++++++++++++ Common/UI/ViewGroup.h | 42 ++++++++++++++ UI/GameBrowser.cpp | 5 +- 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp index 931e974a33..cf6983ca42 100644 --- a/Common/UI/ViewGroup.cpp +++ b/Common/UI/ViewGroup.cpp @@ -929,6 +929,130 @@ void ChoiceGroup::Draw(UIContext &dc) { }*/ } +static Margins FlowLayoutMargins(const View *view) { + const LinearLayoutParams *lp = view->GetLayoutParams()->As(); + return (lp && lp->HasMargins()) ? lp->margins : Margins(0); +} + +static float FlowLayoutWeight(const View *view) { + const LinearLayoutParams *lp = view->GetLayoutParams()->As(); + return lp ? lp->weight : 0.0f; +} + +void FlowLayout::BuildRows(float maxWidth, std::vector *rows) const { + rows->clear(); + Row row{}; + bool rowOpen = false; + for (size_t i = 0; i < views_.size(); i++) { + const View *view = views_[i]; + if (view->GetVisibility() == V_GONE) + continue; + const Margins margins = FlowLayoutMargins(view); + const float w = view->GetMeasuredWidth() + margins.horiz(); + const float h = view->GetMeasuredHeight() + margins.vert(); + if (rowOpen && row.width + spacing_ + w > maxWidth) { + rows->push_back(row); + rowOpen = false; + } + if (!rowOpen) { + row = Row{ i, i + 1, w, h, FlowLayoutWeight(view) }; + rowOpen = true; + } else { + row.end = i + 1; + row.width += spacing_ + w; + row.height = std::max(row.height, h); + row.weightSum += FlowLayoutWeight(view); + } + } + if (rowOpen) { + rows->push_back(row); + } +} + +void FlowLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) { + // Without a width limit, everything goes on one row. + float maxWidth = INFINITY; + if (horiz.type != UNSPECIFIED) { + maxWidth = std::max(0.0f, horiz.size - padding_.horiz()); + } + if (layoutParams_->width > 0.0f) { + maxWidth = std::min(maxWidth, std::max(0.0f, layoutParams_->width - padding_.horiz())); + } + + for (View *view : views_) { + if (view->GetVisibility() == V_GONE) + continue; + const Margins margins = FlowLayoutMargins(view); + view->Measure(dc, MeasureSpec(UNSPECIFIED, 0.0f), MeasureSpec(UNSPECIFIED, 0.0f)); + if (view->GetMeasuredWidth() + margins.horiz() > maxWidth) { + // Too wide even for a row of its own, so let it wrap its text or shrink. + view->Measure(dc, MeasureSpec(AT_MOST, std::max(0.0f, maxWidth - margins.horiz())), MeasureSpec(UNSPECIFIED, 0.0f)); + } + } + + std::vector rows; + BuildRows(maxWidth, &rows); + + float contentWidth = 0.0f; + float contentHeight = 0.0f; + for (const Row &row : rows) { + contentWidth = std::max(contentWidth, row.width); + contentHeight += row.height; + } + if (!rows.empty()) { + contentHeight += rowSpacing_ * (rows.size() - 1); + } + + MeasureBySpec(layoutParams_->width, contentWidth + padding_.horiz(), horiz, &measuredWidth_); + MeasureBySpec(layoutParams_->height, contentHeight + padding_.vert(), vert, &measuredHeight_); +} + +void FlowLayout::Layout() { + const float maxWidth = bounds_.w - padding_.horiz(); + std::vector rows; + BuildRows(maxWidth, &rows); + + float y = bounds_.y + padding_.top; + for (const Row &row : rows) { + const float leftover = std::max(0.0f, maxWidth - row.width); + float x = bounds_.x + padding_.left; + if (row.weightSum <= 0.0f) { + const Gravity rowGravity = (Gravity)((int)rowGravity_ & (int)Gravity::G_HORIZMASK); + if (rowGravity == Gravity::G_RIGHT) { + x += leftover; + } else if (rowGravity == Gravity::G_HCENTER) { + x += leftover * 0.5f; + } + } + + for (size_t i = row.first; i < row.end; i++) { + View *view = views_[i]; + if (view->GetVisibility() == V_GONE) + continue; + const LinearLayoutParams *lp = view->GetLayoutParams()->As(); + const Margins margins = FlowLayoutMargins(view); + const float weight = FlowLayoutWeight(view); + const Gravity gravity = lp ? (Gravity)((int)lp->gravity & (int)Gravity::G_VERTMASK) : Gravity::G_TOP; + + float w = view->GetMeasuredWidth() + margins.horiz(); + float innerWidth = view->GetMeasuredWidth(); + if (weight > 0.0f && row.weightSum > 0.0f) { + w += leftover * weight / row.weightSum; + innerWidth = w - margins.horiz(); + } + + Bounds itemBounds(x, y, w, row.height); + Bounds innerBounds; + ApplyGravity(itemBounds, margins, innerWidth, view->GetMeasuredHeight(), gravity, innerBounds); + view->SetBounds(innerBounds); + view->Layout(); + + x += w + spacing_; + } + y += row.height + rowSpacing_; + } +} + void AnchorLayout::Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) { MeasureBySpec(layoutParams_->width, 0.0f, horiz, &measuredWidth_); MeasureBySpec(layoutParams_->height, 0.0f, vert, &measuredHeight_); diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h index f71c4331b3..c898421319 100644 --- a/Common/UI/ViewGroup.h +++ b/Common/UI/ViewGroup.h @@ -283,6 +283,48 @@ public: PopupChoiceGroupList(LayoutParams *layoutParams = nullptr); }; +// Lays out its children left to right at their natural size, starting a new row when the next +// one doesn't fit. Takes LinearLayoutParams: the gravity places a child vertically in its row, +// and children with weight share whatever space is left in their row. +class FlowLayout : public ViewGroup { +public: + FlowLayout(LayoutParams *layoutParams = nullptr) : ViewGroup(layoutParams) {} + + void Measure(const UIContext &dc, MeasureSpec horiz, MeasureSpec vert) override; + void Layout() override; + void SetSpacing(float spacing) { + spacing_ = spacing; + } + void SetRowSpacing(float spacing) { + rowSpacing_ = spacing; + } + // Where the children go in a row that has space left over, and no weighted children to take it. + // G_LEFT, G_RIGHT or G_HCENTER. + void SetRowGravity(Gravity gravity) { + rowGravity_ = gravity; + } + // Space between the layout's edges and its children, on top of the children's own margins. + void SetPadding(const Margins &padding) { + padding_ = padding; + } + std::string DescribeLog() const override { return "FlowLayout: " + View::DescribeLog(); } + +private: + struct Row { + size_t first; // Index into views_. + size_t end; + float width; // Including margins and spacing. + float height; + float weightSum; + }; + void BuildRows(float maxWidth, std::vector *rows) const; + + Margins padding_; + float spacing_ = 5.0f; + float rowSpacing_ = 5.0f; + Gravity rowGravity_ = Gravity::G_LEFT; +}; + // GridLayout is a little different from the Android layout. This one has fixed size // rows and columns, filled row by row. Items are not allowed to deviate from the set sizes. struct GridLayoutSettings { diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index b40a08612d..3a88d303b1 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -439,7 +439,10 @@ void GameBrowser::Refresh() { gameList_ = nullptr; gameGrid_ = nullptr; - LinearLayout *topBar = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(0, 12, 8, 0))); + // Wraps the buttons onto another row when the screen is too narrow for them. + FlowLayout *topBar = new FlowLayout(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Margins(0, 12, 8, 0))); + topBar->SetSpacing(10.0f); + topBar->SetRowSpacing(10.0f); Add(topBar); // Recent isn't a real folder, so it gets no path or refresh.