#include "VirtualizingStackLayout.h" #include #include "../../GPU.h" #include "../Engine/Scale.h" namespace UI { namespace { // anything aboive this means anything goes constexpr float UnboundedConstraint = 1.0e7f; bool isUnbounded(float value) { return !(value >= 0.0f && value < UnboundedConstraint); } } VirtualizingStackLayout::VirtualizingStackLayout( std::size_t count, float itemSpacing, float itemOverscan, std::function itemSizeProvider, std::function(std::size_t)> factory, VirtualizingStackSizing sizing ) : itemCount(count), spacing(itemSpacing), overscan(itemOverscan), itemSizeProvider(std::move(itemSizeProvider)), itemFactory(std::move(factory)), sizing(sizing) { } Size VirtualizingStackLayout::ViewportExtent() const { Size extent(0,0); if (parent) { const Rect parentBounds = parent->GetLocalBounds(); const Thickness parentPadding = parent->GetPadding(); extent.width = std::max(0.0f, parentBounds.Width - parentPadding.HorizontalTotal()); extent.height = std::max(0.0f, parentBounds.Height - parentPadding.VerticalTotal()); if (extent.width > 0.0f && extent.height > 0.0f) { return extent; } } // fallback instead int width = 0; int height = 0; SDL_GetWindowSizeInPixels(GPU::GetWindow(), &width, &height); // the window reports physical pixels, the layout speaks logical units const Size viewport(PixelsToUnits(static_cast(width)), PixelsToUnits(static_cast(height))); if (!(extent.width > 0.0f)) { extent.width = viewport.width; } if (!(extent.height > 0.0f)) { extent.height = viewport.height; } return extent; } Size VirtualizingStackLayout::ResolveAvailableSpace(Size availableSpace) const { Size space = availableSpace; // use sane value instead of unbounded for scrolling const Size viewport = ViewportExtent(); if (isUnbounded(space.width)) { space.width = viewport.width; } if (isUnbounded(space.height)) { space.height = viewport.height; } // ignoring parent padding if (parent) { space.width -= parent->GetPadding().HorizontalTotal(); space.height -= parent->GetPadding().VerticalTotal(); } space.width = std::max(0.0f, space.width - margin.HorizontalTotal() - padding.HorizontalTotal()); space.height = std::max(0.0f, space.height - margin.VerticalTotal() - padding.VerticalTotal()); return space; } void VirtualizingStackLayout::RebuildMetrics(Size availableSpace) { if (availableSpace == metricsSpace && itemSizes.size() == itemCount) { return; } metricsSpace = availableSpace; hasVisibleRange = false; // offsets moved → stale items itemSizes.clear(); itemOffsets.clear(); itemSizes.reserve(itemCount); itemOffsets.reserve(itemCount); float currentOffset = padding.Left; for (std::size_t index = 0; index < itemCount; index++) { itemOffsets.push_back(currentOffset); Size itemSize = itemSizeProvider ? itemSizeProvider(index) : availableSpace; itemSizes.push_back({std::max(0.0f, itemSize.width), std::max(0.0f, itemSize.height)}); currentOffset += itemSizes.back().width; if (index + 1 < itemCount) { currentOffset += spacing; } } } std::size_t VirtualizingStackLayout::FindFirstVisibleIndex(float start) const { auto iterator = std::lower_bound(itemOffsets.begin(), itemOffsets.end(), start); if (iterator != itemOffsets.begin()) { --iterator; } return static_cast(std::distance(itemOffsets.begin(), iterator)); } std::size_t VirtualizingStackLayout::FindLastVisibleIndex(float end) const { auto iterator = std::upper_bound(itemOffsets.begin(), itemOffsets.end(), end); if (iterator == itemOffsets.begin()) { return 0; } return static_cast(std::distance(itemOffsets.begin(), iterator) - 1); } bool VirtualizingStackLayout::UpdateVisibleChildren() { if (!itemCount || !itemFactory || !parent) { children.clear(); hasVisibleRange = false; return true; } float viewportWidth = std::max(0.0f, parent->GetLocalBounds().Width - parent->GetPadding().HorizontalTotal()); float visibleStartOffset = std::max(0.0f, scrollOffset - overscan); float visibleEndOffset = scrollOffset + viewportWidth + overscan; std::size_t first = FindFirstVisibleIndex(visibleStartOffset); std::size_t last = FindLastVisibleIndex(visibleEndOffset); last = std::min(last, itemCount - 1); if (hasVisibleRange && first == visibleStart && last == visibleEnd) { return false; } children.clear(); visibleStart = first; visibleEnd = last; hasVisibleRange = true; for (std::size_t index = visibleStart; index <= visibleEnd; index++) { std::unique_ptr child = itemFactory(index); if (!child) { continue; } AddChild(std::move(child)); } return true; } void VirtualizingStackLayout::Measure(Size availableSpace) { RebuildMetrics(ResolveAvailableSpace(availableSpace)); float totalWidth = padding.HorizontalTotal(); if (itemCount) { totalWidth += itemOffsets.back() - padding.Left + itemSizes.back().width; totalWidth += padding.Right; } desiredSize.width = std::clamp(totalWidth + margin.HorizontalTotal(), minSize.width, maxSize.width); float contentHeight = 0.0f; for (std::size_t index = 0; index < itemCount; index++) { contentHeight = std::max(contentHeight, itemSizes[index].height); } desiredSize.height = std::clamp(contentHeight + padding.VerticalTotal() + margin.VerticalTotal(), minSize.height, maxSize.height); } void VirtualizingStackLayout::Arrange(Rect finalBounds) { ArrangeCore(finalBounds); UpdateVisibleChildren(); if (!hasVisibleRange) { ClearDirty(DirtyFlags::Layout | DirtyFlags::Hierarchy); return; } const float contentHeight = std::max(0.0f, localBounds.Height - padding.VerticalTotal()); for (std::size_t childIndex = 0; childIndex < children.size(); childIndex++) { std::size_t itemIndex = visibleStart + childIndex; if (itemIndex >= itemCount) { break; } const float childHeight = sizing == VirtualizingStackSizing::Fill ? std::min(itemSizes[itemIndex].height, contentHeight) : contentHeight; Widget* child = children[childIndex].get(); child->Measure(Size(itemSizes[itemIndex].width, childHeight)); child->Arrange(Rect( itemOffsets[itemIndex], padding.Top, itemSizes[itemIndex].width, childHeight )); } ClearDirty(DirtyFlags::Layout | DirtyFlags::Hierarchy); } void VirtualizingStackLayout::SetScrollOffset(Point offset) { float newOffset = std::max(0.0f, offset.x); if (newOffset == scrollOffset) { return; } scrollOffset = newOffset; bool rangeChanged = UpdateVisibleChildren(); if (rangeChanged && localBounds.Width > 0.0f) { Arrange(localBounds); } MarkDirty(DirtyFlags::Render); } }