#include "UiLayout.hpp" #include #include namespace jdi { UiInsets UiInsets::All(float value) { UiInsets insets; insets.left = value; insets.top = value; insets.right = value; insets.bottom = value; return insets; } UiInsets UiInsets::Symmetric(float horizontal, float vertical) { UiInsets insets; insets.left = horizontal; insets.right = horizontal; insets.top = vertical; insets.bottom = vertical; return insets; } UiInsets UiInsets::Scaled(float scale) const { UiInsets insets; insets.left = left * scale; insets.top = top * scale; insets.right = right * scale; insets.bottom = bottom * scale; return insets; } bool UiRectHasArea(const gd::RectF& rect) { return rect.Width > 0.0f && rect.Height > 0.0f; } bool UiRectIntersect(const gd::RectF& a, const gd::RectF& b, gd::RectF& out) { if (!UiRectHasArea(a) || !UiRectHasArea(b)) { out = gd::RectF(); return false; } const float left = std::max(a.X, b.X); const float top = std::max(a.Y, b.Y); const float right = std::min(a.X + a.Width, b.X + b.Width); const float bottom = std::min(a.Y + a.Height, b.Y + b.Height); if (right <= left || bottom <= top) { out = gd::RectF(); return false; } out = gd::RectF(left, top, right - left, bottom - top); return true; } gd::RectF UiRectUnion(const gd::RectF& a, const gd::RectF& b) { if (!UiRectHasArea(a)) { return b; } if (!UiRectHasArea(b)) { return a; } const float left = std::min(a.X, b.X); const float top = std::min(a.Y, b.Y); const float right = std::max(a.X + a.Width, b.X + b.Width); const float bottom = std::max(a.Y + a.Height, b.Y + b.Height); return gd::RectF(left, top, right - left, bottom - top); } gd::RectF UiRectDeflate(const gd::RectF& rect, const UiInsets& insets) { const float width = rect.Width - insets.Horizontal(); const float height = rect.Height - insets.Vertical(); if (width <= 0.0f || height <= 0.0f) { return gd::RectF(); } return gd::RectF(rect.X + insets.left, rect.Y + insets.top, width, height); } gd::RectF UiRectInflate(const gd::RectF& rect, float amount) { if (!UiRectHasArea(rect) || amount <= 0.0f) { return rect; } return gd::RectF(rect.X - amount, rect.Y - amount, rect.Width + amount * 2.0f, rect.Height + amount * 2.0f); } gd::RectF UiRectSnapInward(const gd::RectF& rect, float scale) { const float safeScale = (scale > 0.0f) ? scale : 1.0f; const float x0 = std::ceil(rect.X * safeScale - 0.001f) / safeScale; const float y0 = std::ceil(rect.Y * safeScale - 0.001f) / safeScale; const float x1 = std::floor((rect.X + rect.Width) * safeScale + 0.001f) / safeScale; const float y1 = std::floor((rect.Y + rect.Height) * safeScale + 0.001f) / safeScale; if (x1 <= x0 || y1 <= y0) { return gd::RectF(); } return gd::RectF(x0, y0, x1 - x0, y1 - y0); } gd::RectF UiAlignInRect(const gd::RectF& outer, float width, float height, UiAlign horizontal, UiAlign vertical, float offsetX, float offsetY) { float boxWidth = width; float boxHeight = height; if (horizontal == UiAlign::STRETCH) { boxWidth = outer.Width; } if (vertical == UiAlign::STRETCH) { boxHeight = outer.Height; } boxWidth = std::min(boxWidth, outer.Width); boxHeight = std::min(boxHeight, outer.Height); float x = outer.X; switch (horizontal) { case UiAlign::CENTER: x = outer.X + (outer.Width - boxWidth) * 0.5f; break; case UiAlign::END: x = outer.X + outer.Width - boxWidth; break; default: break; } float y = outer.Y; switch (vertical) { case UiAlign::CENTER: y = outer.Y + (outer.Height - boxHeight) * 0.5f; break; case UiAlign::END: y = outer.Y + outer.Height - boxHeight; break; default: break; } return gd::RectF(x + offsetX, y + offsetY, boxWidth, boxHeight); } float UiStackExtent(const std::vector& sizes, float gap) { float total = 0.0f; std::size_t fixedCount = 0; for (float size : sizes) { if (size >= 0.0f) { total += size; ++fixedCount; } } if (fixedCount > 0 && sizes.size() > 1) { total += gap * static_cast(sizes.size() - 1); } return total; } void UiStackRects(const gd::RectF& area, UiAxis axis, const std::vector& sizes, float gap, std::vector& out, float* usedExtent) { out.clear(); if (sizes.empty()) { if (usedExtent != nullptr) { *usedExtent = 0.0f; } return; } const bool horizontal = (axis == UiAxis::HORIZONTAL); const float totalExtent = horizontal ? area.Width : area.Height; const float totalGap = (sizes.size() > 1) ? gap * static_cast(sizes.size() - 1) : 0.0f; const float fixedExtent = UiStackExtent(sizes, gap) - totalGap; std::size_t autoCount = 0; for (float size : sizes) { if (size < 0.0f) { ++autoCount; } } // 剩余空间不足时按 0 处理(**不允许**出现负尺寸:负宽高的矩形会画出反向的线)。 const float remaining = std::max(0.0f, totalExtent - fixedExtent - totalGap); const float autoExtent = (autoCount > 0) ? (remaining / static_cast(autoCount)) : 0.0f; float cursor = horizontal ? area.X : area.Y; out.reserve(sizes.size()); for (std::size_t index = 0; index < sizes.size(); ++index) { const float extent = (sizes[index] < 0.0f) ? autoExtent : std::max(0.0f, sizes[index]); if (horizontal) { out.push_back(gd::RectF(cursor, area.Y, extent, area.Height)); } else { out.push_back(gd::RectF(area.X, cursor, area.Width, extent)); } cursor += extent; if (index + 1 < sizes.size()) { cursor += gap; } } if (usedExtent != nullptr) { *usedExtent = cursor - (horizontal ? area.X : area.Y); } } float UiFlowRects(const gd::RectF& area, UiAxis axis, const std::vector& sizes, float gap, std::vector& out, std::size_t& overflow) { out.clear(); overflow = sizes.size(); if (sizes.empty()) { return 0.0f; } const bool horizontal = (axis == UiAxis::HORIZONTAL); const float mainExtent = horizontal ? area.Width : area.Height; const float crossExtent = horizontal ? area.Height : area.Width; float main = 0.0f; // 当前行内已用主轴长度 float cross = 0.0f; // 当前行起点(交叉轴) float lineCross = 0.0f; // 当前行最大交叉尺寸 out.reserve(sizes.size()); for (std::size_t index = 0; index < sizes.size(); ++index) { const float itemMain = horizontal ? sizes[index].Width : sizes[index].Height; const float itemCross = horizontal ? sizes[index].Height : sizes[index].Width; const float needed = (main > 0.0f) ? (main + gap + itemMain) : itemMain; if (needed > mainExtent && main > 0.0f) { // 换行 cross += lineCross + gap; main = 0.0f; lineCross = 0.0f; } if (itemMain > mainExtent || cross + itemCross > crossExtent) { overflow = index; break; } const float offset = (main > 0.0f) ? (main + gap) : 0.0f; const float x = horizontal ? (area.X + offset) : (area.X + cross); const float y = horizontal ? (area.Y + cross) : (area.Y + offset); out.push_back(gd::RectF(x, y, horizontal ? itemMain : itemCross, horizontal ? itemCross : itemMain)); main += offset + itemMain; lineCross = std::max(lineCross, itemCross); } return cross + lineCross; } gd::RectF UiAbsRect(const gd::RectF& outer, float dx, float dy, float width, float height) { return gd::RectF(outer.X + dx, outer.Y + dy, std::max(0.0f, width), std::max(0.0f, height)); } std::uint64_t UiMixKey(std::uint64_t seed, std::uint64_t value) { // FNV-1a 64 位的一份变体:与项目里其它指纹(SceneContentHash 等)同一风格。 seed ^= value + 0x9E3779B97F4A7C15ull + (seed << 6) + (seed >> 2); return seed; } std::uint64_t UiMixKeyFloat(std::uint64_t seed, float value) { // 量化到 1/1024 逻辑像素:亚像素抖动不应该让布局缓存反复失效。 const long long quantized = static_cast(std::llround(value * 1024.0f)); return UiMixKey(seed, static_cast(quantized)); } UiLayoutCache::UiLayoutCache(std::size_t capacity) : m_capacity(std::max(1, capacity)) { } const UiLayoutResult* UiLayoutCache::Find(std::uint64_t key) { for (UiLayoutResult& entry : m_entries) { if (entry.key == key) { entry.stamp = ++m_stamp; return &entry; } } return nullptr; } const UiLayoutResult& UiLayoutCache::Store(UiLayoutResult result) { for (UiLayoutResult& entry : m_entries) { if (entry.key == result.key) { entry = std::move(result); entry.stamp = ++m_stamp; return entry; } } result.stamp = ++m_stamp; m_entries.push_back(std::move(result)); Trim(); // 淘汰只会从队首进行,而新条目在队尾 ⇒ 刚存入的条目一定仍然有效。 return m_entries.back(); } void UiLayoutCache::Invalidate(std::uint64_t key) { for (auto iterator = m_entries.begin(); iterator != m_entries.end(); ++iterator) { if (iterator->key == key) { m_entries.erase(iterator); return; } } } void UiLayoutCache::Clear() { m_entries.clear(); } void UiLayoutCache::Trim() { while (m_entries.size() > m_capacity) { // 淘汰"最久未被命中"的条目(stamp 最小)。 auto oldest = m_entries.begin(); for (auto iterator = m_entries.begin(); iterator != m_entries.end(); ++iterator) { if (iterator->stamp < oldest->stamp) { oldest = iterator; } } m_entries.erase(oldest); } } } // namespace jdi