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.