Files
WoWee/include/ui/buff_bar_layout.hpp
Kelsi 512305692c refactor(ui): extract buff bar layout arithmetic and test it
renderBuffBar is ~400 lines and the layout maths was buried in the middle of it,
which is exactly where a silent bug lives: the row does not wrap, so it has a hard
capacity, and it has to stay clear of the minimap at every resolution and scale.

The arithmetic moves to buff_bar_layout.hpp as a pure function and combat_ui.cpp
calls it, so the tested maths is the maths that runs. 408 assertions sweep
resolutions from 1280x720 to 4K and scales from 0.75x to 1.5x, checking the row
never overlaps the minimap, never runs off screen, drops overflow rather than
wrapping, and always gives weapon enchants their slots before auras.
2026-07-14 15:08:45 -07:00

93 lines
4.1 KiB
C++

#pragma once
// Pure layout arithmetic for the buff bar, kept out of the ImGui code so it can be
// tested. The bar is a single row along the top of the screen, right of centre: it is
// right-aligned to end just left of the minimap and grows leftwards as auras are
// gained. Nothing wraps to a second row, so the row has a hard capacity and the
// arithmetic that decides it is worth pinning down.
#include <algorithm>
#include <cstdint>
namespace wowee {
namespace ui {
struct BuffBarLayout {
float iconSize = 0.0f; // per-icon edge length, in pixels
float iconSpacing = 0.0f; // gap between icons
float barWidth = 0.0f; // window width
float barX = 0.0f; // window left edge, in screen pixels
float barY = 0.0f; // window top edge
int maxIcons = 0; // how many icons fit on the row at all
int auraShown = 0; // auras that made the cut
int enchantShown = 0; // weapon enchants that made the cut
int iconCount = 0; // auraShown + enchantShown
};
struct BuffBarMetrics {
// 25% larger than the original 32px icons, at the 1080p reference height.
static constexpr float kBaseIconSize = 40.0f;
static constexpr float kBaseIconSpacing = 2.0f;
static constexpr float kWindowPadding = 16.0f; // 8px each side
static constexpr float kReferenceHeight = 1080.0f;
// The minimap is 200x200 at a 10px margin, so its left edge sits 210px in from the
// right of the screen. The row must stop before it.
static constexpr float kMinimapLeftEdge = 210.0f;
static constexpr float kRightGap = 10.0f;
static constexpr float kScreenMargin = 10.0f;
static constexpr float kTopMargin = 10.0f;
// Floor: the Dismiss Pet button needs somewhere to sit even with no auras.
static constexpr float kMinBarWidth = 140.0f;
// Bounds on the automatic resolution scaling.
static constexpr float kMinAutoScale = 0.75f;
static constexpr float kMaxAutoScale = 2.0f;
};
/// Icons track the window height so the bar keeps its proportions at any resolution,
/// with the user's Buff Bar Scale setting layered on top.
inline float buffBarScale(float screenH, float userScale) {
const float autoScale = std::clamp(screenH / BuffBarMetrics::kReferenceHeight,
BuffBarMetrics::kMinAutoScale,
BuffBarMetrics::kMaxAutoScale);
return autoScale * userScale;
}
/// Lay out a single row of `auraCount` auras plus `enchantCount` weapon enchants.
///
/// Weapon enchants claim their slots first: a wall of buffs must not push a sharpening
/// stone off the row. Anything that still does not fit is dropped rather than wrapped.
inline BuffBarLayout computeBuffBarLayout(float screenW, float screenH, float userScale,
int auraCount, int enchantCount) {
using M = BuffBarMetrics;
BuffBarLayout out;
const float scale = buffBarScale(screenH, userScale);
out.iconSize = M::kBaseIconSize * scale;
out.iconSpacing = M::kBaseIconSpacing * scale;
out.barY = M::kTopMargin;
// Space between the left edge of the screen and the left edge of the minimap.
const float availableW = screenW - M::kMinimapLeftEdge - M::kRightGap - M::kScreenMargin;
out.maxIcons = std::max(1, static_cast<int>(
(availableW - M::kWindowPadding + out.iconSpacing) / (out.iconSize + out.iconSpacing)));
out.enchantShown = std::clamp(enchantCount, 0, out.maxIcons);
out.auraShown = std::clamp(auraCount, 0, out.maxIcons - out.enchantShown);
out.iconCount = out.auraShown + out.enchantShown;
out.barWidth = M::kWindowPadding;
if (out.iconCount > 0) {
out.barWidth += out.iconCount * out.iconSize +
(out.iconCount - 1) * out.iconSpacing;
}
out.barWidth = std::max(out.barWidth, M::kMinBarWidth * scale);
// Right-align against the minimap's left edge, but never run off screen.
out.barX = std::max(M::kScreenMargin,
screenW - M::kMinimapLeftEdge - M::kRightGap - out.barWidth);
return out;
}
} // namespace ui
} // namespace wowee