- BillboardInstance: per-instance uv sub-rect, feet anchor, turn, dithered alpha, flash; BillboardBatch::sharedTexture; Texture createEmpty/update/cellUV/cellAnchor/nearest - SpriteAtlas: fixed-cell runtime atlas with gutters - hd2d::ui: Octopath-style panels, fonts (Cinzel, EB Garamond), dialogue box with typewriter + portraits + choices, menus, nameplates, toasts, ImGui theme - DayCycle (dawn/day/dusk/night keyframes, sun + moon arcs), Weather overcast/mist/ lightning, night-aware presets, stars and moon in the sky - Hidden-window offscreen captures (Window visible=false, Renderer output FBO, OffscreenTarget); --screenshot never opens a window - Camera::worldToScreen; CMake usable via add_subdirectory (HD2D_BUILD_DEMO, HD2D_ENGINE_ASSET_DIR)
747 lines
34 KiB
C++
747 lines
34 KiB
C++
#define IMGUI_DEFINE_MATH_OPERATORS // before the first imgui.h include
|
|
#include "hd2d/UI.h"
|
|
|
|
#include "hd2d/Log.h"
|
|
|
|
#include <imgui_impl_opengl3.h>
|
|
#include <imgui_internal.h>
|
|
|
|
#include <algorithm>
|
|
#include <cfloat>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
|
|
namespace hd2d::ui {
|
|
namespace {
|
|
|
|
Fonts g_fonts;
|
|
|
|
// ---- colour helpers ---------------------------------------------------------
|
|
|
|
float alphaOf(ImU32 c) { return float((c >> IM_COL32_A_SHIFT) & 0xFF) / 255.0f; }
|
|
|
|
ImU32 withAlpha(ImU32 c, float a) {
|
|
int na = int(std::clamp(alphaOf(c) * a, 0.0f, 1.0f) * 255.0f + 0.5f);
|
|
return (c & ~IM_COL32_A_MASK) | (ImU32(na) << IM_COL32_A_SHIFT);
|
|
}
|
|
|
|
ImU32 rgba(int r, int g, int b, float a) { return IM_COL32(r, g, b, int(std::clamp(a, 0.0f, 1.0f) * 255.0f + 0.5f)); }
|
|
|
|
// Component-wise multiply (rgb and alpha).
|
|
ImU32 modulate(ImU32 c, ImU32 t) {
|
|
ImVec4 a = ImGui::ColorConvertU32ToFloat4(c), b = ImGui::ColorConvertU32ToFloat4(t);
|
|
return ImGui::ColorConvertFloat4ToU32(ImVec4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w));
|
|
}
|
|
|
|
ImU32 mix(ImU32 c0, ImU32 c1, float t) {
|
|
ImVec4 a = ImGui::ColorConvertU32ToFloat4(c0), b = ImGui::ColorConvertU32ToFloat4(c1);
|
|
return ImGui::ColorConvertFloat4ToU32(
|
|
ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t));
|
|
}
|
|
|
|
ImVec2 snap(ImVec2 p) { return {std::floor(p.x + 0.5f), std::floor(p.y + 0.5f)}; }
|
|
|
|
ImFont* orDefault(ImFont* f) { return f ? f : ImGui::GetFont(); }
|
|
float displayScale() { return scaleFor(ImGui::GetIO().DisplaySize); }
|
|
|
|
// Fills whatever `draw` adds with a vertical gradient (vertex alpha is kept).
|
|
template <class F>
|
|
void verticalGradient(ImDrawList* dl, float y0, float y1, ImU32 top, ImU32 bottom, F&& draw) {
|
|
int v0 = dl->VtxBuffer.Size;
|
|
draw();
|
|
ImGui::ShadeVertsLinearColorGradientKeepAlpha(dl, v0, dl->VtxBuffer.Size, ImVec2(0, y0), ImVec2(0, y1), top,
|
|
bottom);
|
|
}
|
|
|
|
// Horizontal line fading out towards both ends (or only towards the right).
|
|
void fadeLine(ImDrawList* dl, ImVec2 a, ImVec2 b, float thickness, ImU32 col, bool bothEnds = true) {
|
|
ImU32 clear = withAlpha(col, 0.0f);
|
|
float y0 = a.y - thickness * 0.5f, y1 = a.y + thickness * 0.5f;
|
|
if (bothEnds) {
|
|
float mid = (a.x + b.x) * 0.5f;
|
|
dl->AddRectFilledMultiColor(ImVec2(a.x, y0), ImVec2(mid, y1), clear, col, col, clear);
|
|
dl->AddRectFilledMultiColor(ImVec2(mid, y0), ImVec2(b.x, y1), col, clear, clear, col);
|
|
} else {
|
|
dl->AddRectFilledMultiColor(ImVec2(a.x, y0), ImVec2(b.x, y1), col, clear, clear, col);
|
|
}
|
|
}
|
|
|
|
// Diamond gem: gold body, lighter upper-left facet, dark outline.
|
|
void gem(ImDrawList* dl, ImVec2 c, float r, ImU32 body, ImU32 light, float alpha) {
|
|
ImVec2 t(c.x, c.y - r), rt(c.x + r, c.y), b(c.x, c.y + r), l(c.x - r, c.y);
|
|
dl->AddQuadFilled(t, rt, b, l, withAlpha(IM_COL32(10, 10, 16, 255), 0.75f * alpha));
|
|
float ri = std::max(1.0f, r - 1.2f);
|
|
ImVec2 ti(c.x, c.y - ri), rti(c.x + ri, c.y), bi(c.x, c.y + ri), li(c.x - ri, c.y);
|
|
dl->AddQuadFilled(ti, rti, bi, li, withAlpha(body, alpha));
|
|
dl->AddTriangleFilled(li, ti, c, withAlpha(light, alpha));
|
|
}
|
|
|
|
// ---- text layout ---------------------------------------------------------------
|
|
|
|
struct Span {
|
|
const char* b;
|
|
const char* e;
|
|
};
|
|
|
|
int utf8Len(unsigned char c) { return c < 0x80 ? 1 : (c >> 5) == 0x6 ? 2 : (c >> 4) == 0xE ? 3 : (c >> 3) == 0x1E ? 4 : 1; }
|
|
|
|
// Splits `[b, e)` into lines: hard breaks on '\n', word wrap at `wrap` px (<= 0: none).
|
|
void layoutLines(ImFont* font, float size, const char* b, const char* e, float wrap, std::vector<Span>& out) {
|
|
out.clear();
|
|
const float scale = size / font->FontSize;
|
|
const char* p = b;
|
|
for (;;) {
|
|
const char* nl = static_cast<const char*>(std::memchr(p, '\n', size_t(e - p)));
|
|
const char* pe = nl ? nl : e;
|
|
const char* s = p;
|
|
if (s == pe) out.push_back({s, s});
|
|
while (s < pe) {
|
|
const char* eol = wrap > 0.0f ? font->CalcWordWrapPositionA(scale, s, pe, wrap) : pe;
|
|
if (eol <= s) eol = std::min(pe, s + utf8Len(static_cast<unsigned char>(*s)));
|
|
const char* le = eol;
|
|
while (le > s && (le[-1] == ' ' || le[-1] == '\t')) --le;
|
|
out.push_back({s, le});
|
|
s = eol;
|
|
while (s < pe && (*s == ' ' || *s == '\t')) ++s;
|
|
}
|
|
if (!nl) break;
|
|
p = nl + 1;
|
|
}
|
|
}
|
|
|
|
float lineAdvance(float size) { return std::round(size * 1.06f); }
|
|
|
|
void drawSpan(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, const char* b, const char* e,
|
|
bool shadow) {
|
|
if (b >= e) return;
|
|
pos = snap(pos);
|
|
if (shadow) {
|
|
float o = std::max(1.0f, std::round(size / 36.0f));
|
|
float a = alphaOf(color);
|
|
// Soft halo for contrast on busy backgrounds plus the crisp 1px offset shadow.
|
|
dl->AddText(font, size, ImVec2(pos.x + o, pos.y + o * 2.0f), rgba(0, 0, 0, 0.30f * a), b, e);
|
|
dl->AddText(font, size, ImVec2(pos.x + o, pos.y + o), rgba(4, 4, 10, 0.85f * a), b, e);
|
|
}
|
|
dl->AddText(font, size, pos, color, b, e);
|
|
}
|
|
|
|
// ---- option list (menu + dialogue choices) ----------------------------------------
|
|
|
|
struct ListMetrics {
|
|
float s, size, titleSize, rowH, padX, padY, cursorW, titleH;
|
|
ImVec2 panel;
|
|
};
|
|
|
|
ListMetrics listMetrics(const std::vector<std::string>& items, std::string_view title, float scale) {
|
|
ListMetrics m;
|
|
m.s = scale;
|
|
m.size = std::round(kBodySize * 0.9f * scale);
|
|
m.titleSize = std::round(kNameSize * scale);
|
|
m.rowH = std::round(m.size * 1.3f);
|
|
m.padX = std::round(26.0f * scale);
|
|
m.padY = std::round(18.0f * scale);
|
|
m.cursorW = std::round(30.0f * scale);
|
|
m.titleH = title.empty() ? 0.0f : std::round(m.titleSize + 22.0f * scale);
|
|
ImFont* body = orDefault(g_fonts.body);
|
|
float w = 0.0f;
|
|
for (const std::string& it : items) w = std::max(w, measure(body, m.size, it).x);
|
|
if (!title.empty()) w = std::max(w, measure(orDefault(g_fonts.name), m.titleSize, title).x - m.cursorW);
|
|
m.panel.x = std::round(std::max(w + m.cursorW + m.padX * 2.0f + 8.0f * scale, 200.0f * scale));
|
|
m.panel.y = std::round(m.padY * 2.0f + m.titleH + m.rowH * float(items.size()));
|
|
return m;
|
|
}
|
|
|
|
void drawList(ImDrawList* dl, ImVec2 min, const ListMetrics& m, const std::vector<std::string>& items, int selected,
|
|
std::string_view title) {
|
|
const float s = m.s;
|
|
ImVec2 max = min + m.panel;
|
|
PanelStyle ps;
|
|
ps.scale = s;
|
|
panel(dl, min, max, ps);
|
|
|
|
float y = min.y + m.padY;
|
|
if (!title.empty()) {
|
|
textCentered(dl, g_fonts.name, m.titleSize, ImVec2((min.x + max.x) * 0.5f, y + m.titleSize * 0.5f), kGold,
|
|
title);
|
|
float ly = std::round(y + m.titleSize + 10.0f * s) + 0.5f;
|
|
fadeLine(dl, ImVec2(min.x + 14.0f * s, ly), ImVec2(max.x - 14.0f * s, ly), std::max(1.0f, s), kGoldDim);
|
|
y += m.titleH;
|
|
}
|
|
|
|
const float t = float(ImGui::GetTime());
|
|
ImFont* body = orDefault(g_fonts.body);
|
|
for (int i = 0; i < int(items.size()); ++i) {
|
|
ImVec2 r0(min.x + 8.0f * s, y), r1(max.x - 8.0f * s, y + m.rowH);
|
|
bool sel = i == selected;
|
|
float textY = y + (m.rowH - m.size) * 0.5f;
|
|
if (sel) {
|
|
ImU32 hi = IM_COL32(222, 189, 120, 70), clear = IM_COL32(222, 189, 120, 0);
|
|
dl->AddRectFilledMultiColor(r0, r1, hi, clear, clear, hi);
|
|
fadeLine(dl, ImVec2(r0.x, r0.y + 0.5f), ImVec2(r1.x, r0.y + 0.5f), 1.0f, IM_COL32(222, 189, 120, 110),
|
|
false);
|
|
fadeLine(dl, ImVec2(r0.x, r1.y - 0.5f), ImVec2(r1.x, r1.y - 0.5f), 1.0f, IM_COL32(222, 189, 120, 110),
|
|
false);
|
|
// Right-pointing gold cursor that bobs gently.
|
|
float bob = std::sin(t * 6.0f) * 2.5f * s;
|
|
float cx = min.x + m.padX + 4.0f * s + bob, cy = y + m.rowH * 0.5f, h = 8.0f * s;
|
|
ImVec2 a(cx - h * 0.6f, cy - h), b(cx + h * 0.9f, cy), c(cx - h * 0.6f, cy + h);
|
|
dl->AddTriangleFilled(a + ImVec2(1, 1.5f) * s, b + ImVec2(1, 1.5f) * s, c + ImVec2(1, 1.5f) * s,
|
|
IM_COL32(0, 0, 0, 150));
|
|
dl->AddTriangleFilled(a, b, c, kGold);
|
|
dl->AddTriangleFilled(a, b, ImVec2(cx - h * 0.6f, cy), IM_COL32(255, 238, 190, 255));
|
|
}
|
|
drawSpan(dl, body, m.size, ImVec2(min.x + m.padX + m.cursorW, textY), sel ? IM_COL32(255, 248, 230, 255) : kTextMuted,
|
|
items[size_t(i)].data(), items[size_t(i)].data() + items[size_t(i)].size(), true);
|
|
y += m.rowH;
|
|
}
|
|
}
|
|
|
|
bool isSentenceEnd(const char* p, size_t len) {
|
|
if (len == 1) return *p == '.' || *p == '!' || *p == '?';
|
|
return len == 3 && !std::memcmp(p, "\xE2\x80\xA6", 3); // ellipsis
|
|
}
|
|
bool isClauseEnd(const char* p, size_t len) {
|
|
if (len == 1) return *p == ',' || *p == ';' || *p == ':';
|
|
return len == 3 && (!std::memcmp(p, "\xE2\x80\x94", 3) || !std::memcmp(p, "\xE2\x80\x93", 3)); // em/en dash
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---- fonts / theme -------------------------------------------------------------------
|
|
|
|
bool loadFonts(const std::string& fontDir, float pixelScale) {
|
|
static const ImWchar kRanges[] = {
|
|
0x0020, 0x00FF, // Basic Latin + Latin-1
|
|
0x0100, 0x017F, // Latin Extended-A
|
|
0x2010, 0x2027, // dashes, quotes, bullets, ellipsis
|
|
0x2030, 0x203A, // per mille, guillemets
|
|
0x20AC, 0x20AC, // euro
|
|
0x2122, 0x2122, // trade mark
|
|
0,
|
|
};
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImFontAtlas* atlas = io.Fonts;
|
|
if (io.BackendRendererUserData) ImGui_ImplOpenGL3_DestroyFontsTexture();
|
|
atlas->Clear();
|
|
atlas->AddFontDefault(); // stays io.FontDefault (first font) for debug panels
|
|
io.FontDefault = nullptr;
|
|
g_fonts = {};
|
|
|
|
pixelScale = std::max(pixelScale, 0.25f);
|
|
bool ok = true;
|
|
auto add = [&](const char* file, float size, float multiply) -> ImFont* {
|
|
std::string path = fontDir + "/" + file;
|
|
if (!std::filesystem::exists(path)) {
|
|
HD2D_ERR("ui: missing font %s", path.c_str());
|
|
ok = false;
|
|
return nullptr;
|
|
}
|
|
ImFontConfig cfg;
|
|
cfg.OversampleH = 2;
|
|
cfg.OversampleV = 1;
|
|
cfg.RasterizerMultiply = multiply;
|
|
std::snprintf(cfg.Name, sizeof cfg.Name, "%s %.0fpx", file, size * pixelScale);
|
|
return atlas->AddFontFromFileTTF(path.c_str(), std::round(size * pixelScale), &cfg, kRanges);
|
|
};
|
|
g_fonts.body = add("EBGaramond.ttf", kBodySize, 1.15f);
|
|
g_fonts.small = add("EBGaramond.ttf", kSmallSize, 1.2f);
|
|
g_fonts.name = add("Cinzel.ttf", kNameSize, 1.1f);
|
|
g_fonts.title = add("CinzelDecorative-Bold.ttf", kTitleSize, 1.0f);
|
|
// The backend re-uploads the atlas on the next NewFrame().
|
|
return ok;
|
|
}
|
|
|
|
const Fonts& fonts() { return g_fonts; }
|
|
|
|
float scaleFor(ImVec2 viewport) { return viewport.y > 0.0f ? viewport.y / 1080.0f : 1.0f; }
|
|
|
|
void applyTheme() {
|
|
ImGuiStyle& st = ImGui::GetStyle();
|
|
st.WindowRounding = 6.0f;
|
|
st.ChildRounding = 4.0f;
|
|
st.FrameRounding = 4.0f;
|
|
st.PopupRounding = 4.0f;
|
|
st.GrabRounding = 3.0f;
|
|
st.TabRounding = 4.0f;
|
|
st.ScrollbarRounding = 6.0f;
|
|
st.WindowBorderSize = 1.0f;
|
|
st.FrameBorderSize = 1.0f;
|
|
st.PopupBorderSize = 1.0f;
|
|
st.WindowPadding = ImVec2(12, 10);
|
|
st.FramePadding = ImVec2(8, 4);
|
|
st.ItemSpacing = ImVec2(8, 6);
|
|
st.WindowTitleAlign = ImVec2(0.5f, 0.5f);
|
|
|
|
auto c = [](int r, int g, int b, float a = 1.0f) { return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a); };
|
|
ImVec4* k = st.Colors;
|
|
const ImVec4 gold = c(222, 189, 120), goldDim = c(150, 122, 74);
|
|
k[ImGuiCol_Text] = c(246, 240, 226);
|
|
k[ImGuiCol_TextDisabled] = c(140, 134, 120);
|
|
k[ImGuiCol_WindowBg] = c(14, 17, 30, 0.92f);
|
|
k[ImGuiCol_ChildBg] = c(0, 0, 0, 0.0f);
|
|
k[ImGuiCol_PopupBg] = c(12, 14, 26, 0.96f);
|
|
k[ImGuiCol_Border] = c(150, 122, 74, 0.70f);
|
|
k[ImGuiCol_BorderShadow] = c(0, 0, 0, 0.0f);
|
|
k[ImGuiCol_FrameBg] = c(28, 32, 52, 0.85f);
|
|
k[ImGuiCol_FrameBgHovered] = c(44, 48, 72, 0.90f);
|
|
k[ImGuiCol_FrameBgActive] = c(58, 56, 70, 1.0f);
|
|
k[ImGuiCol_TitleBg] = c(20, 22, 36, 0.95f);
|
|
k[ImGuiCol_TitleBgActive] = c(46, 38, 26, 0.98f);
|
|
k[ImGuiCol_TitleBgCollapsed] = c(14, 17, 30, 0.75f);
|
|
k[ImGuiCol_MenuBarBg] = c(20, 22, 36, 1.0f);
|
|
k[ImGuiCol_ScrollbarBg] = c(8, 10, 18, 0.6f);
|
|
k[ImGuiCol_ScrollbarGrab] = c(110, 92, 60, 0.8f);
|
|
k[ImGuiCol_ScrollbarGrabHovered] = goldDim;
|
|
k[ImGuiCol_ScrollbarGrabActive] = gold;
|
|
k[ImGuiCol_CheckMark] = gold;
|
|
k[ImGuiCol_SliderGrab] = goldDim;
|
|
k[ImGuiCol_SliderGrabActive] = gold;
|
|
k[ImGuiCol_Button] = c(40, 42, 62, 0.90f);
|
|
k[ImGuiCol_ButtonHovered] = c(96, 78, 46, 0.95f);
|
|
k[ImGuiCol_ButtonActive] = c(150, 122, 74, 1.0f);
|
|
k[ImGuiCol_Header] = c(96, 78, 46, 0.55f);
|
|
k[ImGuiCol_HeaderHovered] = c(120, 98, 58, 0.75f);
|
|
k[ImGuiCol_HeaderActive] = c(150, 122, 74, 0.90f);
|
|
k[ImGuiCol_Separator] = c(150, 122, 74, 0.55f);
|
|
k[ImGuiCol_SeparatorHovered] = goldDim;
|
|
k[ImGuiCol_SeparatorActive] = gold;
|
|
k[ImGuiCol_ResizeGrip] = c(150, 122, 74, 0.25f);
|
|
k[ImGuiCol_ResizeGripHovered] = c(150, 122, 74, 0.65f);
|
|
k[ImGuiCol_ResizeGripActive] = gold;
|
|
k[ImGuiCol_Tab] = c(30, 32, 50, 0.90f);
|
|
k[ImGuiCol_TabHovered] = c(120, 98, 58, 0.85f);
|
|
k[ImGuiCol_TabSelected] = c(86, 70, 42, 1.0f);
|
|
k[ImGuiCol_TabSelectedOverline] = gold;
|
|
k[ImGuiCol_TabDimmed] = c(20, 22, 36, 0.90f);
|
|
k[ImGuiCol_TabDimmedSelected] = c(52, 44, 32, 1.0f);
|
|
k[ImGuiCol_PlotLines] = goldDim;
|
|
k[ImGuiCol_PlotLinesHovered] = gold;
|
|
k[ImGuiCol_PlotHistogram] = goldDim;
|
|
k[ImGuiCol_PlotHistogramHovered] = gold;
|
|
k[ImGuiCol_TableHeaderBg] = c(30, 32, 50, 1.0f);
|
|
k[ImGuiCol_TableBorderStrong] = c(150, 122, 74, 0.6f);
|
|
k[ImGuiCol_TableBorderLight] = c(150, 122, 74, 0.3f);
|
|
k[ImGuiCol_TableRowBg] = c(0, 0, 0, 0.0f);
|
|
k[ImGuiCol_TableRowBgAlt] = c(255, 255, 255, 0.03f);
|
|
k[ImGuiCol_TextSelectedBg] = c(150, 122, 74, 0.45f);
|
|
k[ImGuiCol_DragDropTarget] = gold;
|
|
k[ImGuiCol_NavCursor] = gold;
|
|
k[ImGuiCol_NavWindowingHighlight] = c(255, 255, 255, 0.7f);
|
|
k[ImGuiCol_NavWindowingDimBg] = c(0, 0, 0, 0.4f);
|
|
k[ImGuiCol_ModalWindowDimBg] = c(0, 0, 0, 0.55f);
|
|
}
|
|
|
|
// ---- primitives ------------------------------------------------------------------------
|
|
|
|
void panel(ImDrawList* dl, ImVec2 min, ImVec2 max, const PanelStyle& style) {
|
|
const float s = style.scale > 0.0f ? style.scale : displayScale();
|
|
const float fade = alphaOf(style.tint);
|
|
if (fade <= 0.0f) return;
|
|
min = snap(min);
|
|
max = snap(max);
|
|
const float r = style.rounding * s;
|
|
|
|
// Soft drop shadow: stacked, expanding translucent rounded rects.
|
|
const ImVec2 off(0.0f, std::round(5.0f * s));
|
|
const float spread = 14.0f * s;
|
|
for (int i = 8; i >= 1; --i) {
|
|
float e = spread * float(i) / 8.0f;
|
|
dl->AddRectFilled(min + off - ImVec2(e, e), max + off + ImVec2(e, e), rgba(0, 0, 0, 0.045f * fade), r + e);
|
|
}
|
|
|
|
// Navy -> near-black vertical gradient body.
|
|
verticalGradient(dl, min.y, max.y, IM_COL32(32, 39, 66, 255), IM_COL32(8, 10, 21, 255),
|
|
[&] { dl->AddRectFilled(min, max, rgba(255, 255, 255, style.alpha * fade), r); });
|
|
// Faint top sheen.
|
|
fadeLine(dl, ImVec2(min.x + r, min.y + 2.0f * s), ImVec2(max.x - r, min.y + 2.0f * s), std::max(1.0f, s),
|
|
rgba(255, 255, 255, 0.10f * fade));
|
|
|
|
// Thin warm-gold double border.
|
|
const ImU32 gold = withAlpha(modulate(kGold, style.tint | IM_COL32_A_MASK), fade);
|
|
const ImU32 goldDim = withAlpha(modulate(kGoldDim, style.tint | IM_COL32_A_MASK), 0.85f * fade);
|
|
const float outer = std::max(1.0f, std::round(1.5f * s));
|
|
const float inset = std::round(5.0f * s);
|
|
const ImVec2 h(0.5f, 0.5f);
|
|
dl->AddRect(min + h, max - h, gold, r, 0, outer);
|
|
dl->AddRect(min + ImVec2(inset, inset) + h, max - ImVec2(inset, inset) - h, goldDim, std::max(0.0f, r - inset), 0,
|
|
std::max(1.0f, s));
|
|
|
|
if (!style.ornate) return;
|
|
// Corner gems sitting across both border lines, bright L-brackets along the
|
|
// inner rule, plus a small gem centred on the top and bottom rules.
|
|
const float g = 6.5f * s, arm = std::round(24.0f * s), c = std::round(inset * 0.5f) + 0.5f;
|
|
const ImU32 light = withAlpha(IM_COL32(255, 240, 200, 255), fade);
|
|
const ImVec2 corners[4] = {{min.x + c, min.y + c}, {max.x - c, min.y + c}, {max.x - c, max.y - c}, {min.x + c, max.y - c}};
|
|
const ImVec2 dirs[4] = {{1, 1}, {-1, 1}, {-1, -1}, {1, -1}};
|
|
const float lt = std::max(1.0f, std::round(2.0f * s));
|
|
for (int i = 0; i < 4; ++i) {
|
|
ImVec2 p = corners[i], d = dirs[i];
|
|
ImVec2 q(p.x + d.x * (inset - c + 1.0f), p.y + d.y * (inset - c + 1.0f)); // on the inner rule
|
|
dl->AddLine(ImVec2(q.x + d.x * g * 0.6f, q.y), ImVec2(q.x + d.x * arm, q.y), gold, lt);
|
|
dl->AddLine(ImVec2(q.x, q.y + d.y * g * 0.6f), ImVec2(q.x, q.y + d.y * arm), gold, lt);
|
|
dl->AddCircleFilled(ImVec2(q.x + d.x * (arm + 3.0f * s), q.y), 1.6f * s, gold);
|
|
dl->AddCircleFilled(ImVec2(q.x, q.y + d.y * (arm + 3.0f * s)), 1.6f * s, gold);
|
|
gem(dl, p, g, gold, light, 1.0f);
|
|
}
|
|
if (max.x - min.x > 240.0f * s) {
|
|
float cx = std::round((min.x + max.x) * 0.5f);
|
|
gem(dl, ImVec2(cx, min.y + 0.5f), 3.5f * s, gold, light, 1.0f);
|
|
gem(dl, ImVec2(cx, max.y - 0.5f), 3.5f * s, gold, light, 1.0f);
|
|
}
|
|
}
|
|
|
|
void text(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s, bool shadow) {
|
|
drawSpan(dl, orDefault(font), size, pos, color, s.data(), s.data() + s.size(), shadow);
|
|
}
|
|
|
|
void textWrapped(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, ImU32 color, std::string_view s,
|
|
float wrapWidth, bool shadow) {
|
|
font = orDefault(font);
|
|
static thread_local std::vector<Span> lines;
|
|
layoutLines(font, size, s.data(), s.data() + s.size(), wrapWidth, lines);
|
|
for (const Span& l : lines) {
|
|
drawSpan(dl, font, size, pos, color, l.b, l.e, shadow);
|
|
pos.y += lineAdvance(size);
|
|
}
|
|
}
|
|
|
|
ImVec2 measure(ImFont* font, float size, std::string_view s, float wrapWidth) {
|
|
font = orDefault(font);
|
|
if (wrapWidth <= 0.0f) return font->CalcTextSizeA(size, FLT_MAX, 0.0f, s.data(), s.data() + s.size());
|
|
static thread_local std::vector<Span> lines;
|
|
layoutLines(font, size, s.data(), s.data() + s.size(), wrapWidth, lines);
|
|
float w = 0.0f;
|
|
for (const Span& l : lines) w = std::max(w, font->CalcTextSizeA(size, FLT_MAX, 0.0f, l.b, l.e).x);
|
|
return ImVec2(w, lineAdvance(size) * float(lines.size() - 1) + size);
|
|
}
|
|
|
|
void textCentered(ImDrawList* dl, ImFont* font, float size, ImVec2 center, ImU32 color, std::string_view s,
|
|
bool shadow) {
|
|
ImVec2 sz = measure(font, size, s);
|
|
text(dl, font, size, ImVec2(center.x - sz.x * 0.5f, center.y - sz.y * 0.5f), color, s, shadow);
|
|
}
|
|
|
|
void titleText(ImDrawList* dl, ImVec2 center, std::string_view s, float size) {
|
|
ImFont* font = orDefault(g_fonts.title);
|
|
const char* b = s.data();
|
|
const char* e = b + s.size();
|
|
ImVec2 sz = font->CalcTextSizeA(size, FLT_MAX, 0.0f, b, e);
|
|
ImVec2 pos = snap(ImVec2(center.x - sz.x * 0.5f, center.y - sz.y * 0.5f));
|
|
const float u = size / 96.0f;
|
|
|
|
// Soft dark halo (legible on bright scenery), then a warm glow around the glyphs.
|
|
for (int ring = 0; ring < 3; ++ring) {
|
|
float rad = (ring == 0 ? 10.0f : ring == 1 ? 6.0f : 3.5f) * u;
|
|
ImU32 col = ring == 0 ? IM_COL32(0, 0, 0, 22) : ring == 1 ? IM_COL32(10, 6, 0, 34) : IM_COL32(255, 200, 110, 46);
|
|
for (int i = 0; i < 12; ++i) {
|
|
float a = float(i) * 3.14159265f / 6.0f;
|
|
dl->AddText(font, size, pos + ImVec2(std::cos(a), std::sin(a)) * rad, col, b, e);
|
|
}
|
|
}
|
|
// Dark outline + drop shadow keep the letterforms crisp against the glow.
|
|
for (int i = 0; i < 8; ++i) {
|
|
float a = float(i) * 3.14159265f / 4.0f;
|
|
dl->AddText(font, size, pos + ImVec2(std::cos(a), std::sin(a)) * (2.0f * u), IM_COL32(20, 12, 4, 200), b, e);
|
|
}
|
|
dl->AddText(font, size, pos + ImVec2(0.0f, 4.0f * u), IM_COL32(0, 0, 0, 150), b, e);
|
|
// Gold face: pale highlight at the top to deep gold at the bottom.
|
|
verticalGradient(dl, pos.y + sz.y * 0.2f, pos.y + sz.y * 0.85f, IM_COL32(255, 246, 214, 255),
|
|
IM_COL32(200, 142, 52, 255), [&] { dl->AddText(font, size, pos, IM_COL32_WHITE, b, e); });
|
|
|
|
// Flourish rule underneath: fading gold lines with a centre gem, on a dark underlay.
|
|
float y = std::round(pos.y + sz.y * 0.95f) + 0.5f;
|
|
float half = sz.x * 0.5f + 40.0f * u;
|
|
float cx = center.x;
|
|
const float lt = std::max(1.0f, std::round(2.0f * u));
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
ImU32 col = pass ? kGold : IM_COL32(0, 0, 0, 110);
|
|
ImU32 clear = withAlpha(col, 0.0f);
|
|
float t = pass ? lt : lt + 2.0f * std::max(1.0f, u);
|
|
dl->AddRectFilledMultiColor(ImVec2(cx - half, y - t * 0.5f), ImVec2(cx - 12.0f * u, y + t * 0.5f), clear, col,
|
|
col, clear);
|
|
fadeLine(dl, ImVec2(cx + 12.0f * u, y), ImVec2(cx + half, y), t, col, false);
|
|
}
|
|
gem(dl, ImVec2(cx, y), 8.0f * u, kGold, IM_COL32(255, 240, 200, 255), 1.0f);
|
|
gem(dl, ImVec2(cx - 24.0f * u, y), 4.0f * u, kGold, IM_COL32(255, 240, 200, 255), 1.0f);
|
|
gem(dl, ImVec2(cx + 24.0f * u, y), 4.0f * u, kGold, IM_COL32(255, 240, 200, 255), 1.0f);
|
|
}
|
|
|
|
void nameplate(ImDrawList* dl, ImVec2 anchor, std::string_view s, ImU32 accent, float scale) {
|
|
ImFont* font = orDefault(g_fonts.name);
|
|
const float size = std::round(kNameSize * 0.82f * scale);
|
|
ImVec2 ts = measure(font, size, s);
|
|
const float padX = std::round(16.0f * scale), padY = std::round(6.0f * scale), tip = std::round(8.0f * scale);
|
|
ImVec2 sz(std::round(ts.x + padX * 2.0f), std::round(size + padY * 2.0f));
|
|
ImVec2 min = snap(ImVec2(anchor.x - sz.x * 0.5f, anchor.y - tip - sz.y));
|
|
ImVec2 max = min + sz;
|
|
const float r = sz.y * 0.5f;
|
|
|
|
dl->AddRectFilled(min + ImVec2(0, 3.0f * scale) - ImVec2(2, 2) * scale, max + ImVec2(0, 3.0f * scale) + ImVec2(2, 2) * scale,
|
|
IM_COL32(0, 0, 0, 70), r + 2.0f * scale);
|
|
// Pointer below the pill.
|
|
ImVec2 t0(anchor.x - tip, max.y - 1.0f), t1(anchor.x + tip, max.y - 1.0f), t2(anchor.x, anchor.y);
|
|
dl->AddTriangleFilled(t0 - ImVec2(1.5f, 0) * scale, t1 + ImVec2(1.5f, 0) * scale, t2 + ImVec2(0, 2.0f) * scale, accent);
|
|
dl->AddTriangleFilled(t0 + ImVec2(1.5f, 0) * scale, t1 - ImVec2(1.5f, 0) * scale, t2 - ImVec2(0, 1.0f) * scale,
|
|
IM_COL32(10, 12, 24, 235));
|
|
verticalGradient(dl, min.y, max.y, IM_COL32(34, 40, 66, 255), IM_COL32(10, 12, 24, 255),
|
|
[&] { dl->AddRectFilled(min, max, IM_COL32(255, 255, 255, 230), r); });
|
|
dl->AddRect(min + ImVec2(0.5f, 0.5f), max - ImVec2(0.5f, 0.5f), accent, r, 0, std::max(1.0f, std::round(1.5f * scale)));
|
|
text(dl, font, size, ImVec2(min.x + padX, min.y + padY - 1.0f * scale), kTextColor, s);
|
|
}
|
|
|
|
ImVec2 menuSize(const std::vector<std::string>& items, std::string_view title, float scale) {
|
|
return listMetrics(items, title, scale).panel;
|
|
}
|
|
|
|
void menuAt(ImDrawList* dl, ImVec2 pos, ImVec2 pivot, const std::vector<std::string>& items, int selected,
|
|
std::string_view title, float scale) {
|
|
ListMetrics m = listMetrics(items, title, scale);
|
|
drawList(dl, ImVec2(pos.x - m.panel.x * pivot.x, pos.y - m.panel.y * pivot.y), m, items, selected, title);
|
|
}
|
|
|
|
void menu(ImDrawList* dl, ImVec2 center, const std::vector<std::string>& items, int selected, std::string_view title,
|
|
float scale) {
|
|
menuAt(dl, center, ImVec2(0.5f, 0.5f), items, selected, title, scale);
|
|
}
|
|
|
|
void progressBar(ImDrawList* dl, ImVec2 min, ImVec2 max, float t, ImU32 color) {
|
|
min = snap(min);
|
|
max = snap(max);
|
|
const float h = max.y - min.y, r = h * 0.5f;
|
|
dl->AddRectFilled(min, max, IM_COL32(6, 8, 14, 225), r);
|
|
const float in = std::max(1.0f, std::round(h * 0.18f));
|
|
ImVec2 f0(min.x + in, min.y + in), f1(max.x - in, max.y - in);
|
|
float w = (f1.x - f0.x) * std::clamp(t, 0.0f, 1.0f);
|
|
if (w > 0.5f) {
|
|
f1.x = f0.x + w;
|
|
float fr = std::min(r - in, w * 0.5f);
|
|
verticalGradient(dl, f0.y, f1.y, mix(color, IM_COL32_WHITE, 0.45f), mix(color, IM_COL32_BLACK, 0.25f),
|
|
[&] { dl->AddRectFilled(f0, f1, IM_COL32(255, 255, 255, int(alphaOf(color) * 255.0f)), fr); });
|
|
float hy = f0.y + std::max(1.0f, (f1.y - f0.y) * 0.25f);
|
|
if (w > 2.0f * fr)
|
|
dl->AddLine(ImVec2(f0.x + fr, hy), ImVec2(f1.x - fr, hy), IM_COL32(255, 255, 255, 70), 1.0f);
|
|
}
|
|
dl->AddRect(min + ImVec2(0.5f, 0.5f), max - ImVec2(0.5f, 0.5f), withAlpha(kGoldDim, 0.9f), r, 0, 1.0f);
|
|
}
|
|
|
|
void advanceIndicator(ImDrawList* dl, ImVec2 c, float scale, float time) {
|
|
const float bob = std::sin(time * 4.5f) * 2.5f * scale;
|
|
const float a = 0.6f + 0.4f * std::cos(time * 4.5f);
|
|
const float w = 8.0f * scale, h = 10.0f * scale;
|
|
c.y += bob;
|
|
ImVec2 l(c.x - w, c.y - h * 0.4f), r(c.x + w, c.y - h * 0.4f), b(c.x, c.y + h * 0.6f), top(c.x, c.y - h * 0.9f);
|
|
ImVec2 sh(1.0f * scale, 2.0f * scale);
|
|
dl->AddQuadFilled(top + sh, r + sh, b + sh, l + sh, rgba(0, 0, 0, 0.55f * a));
|
|
dl->AddQuadFilled(top, r, b, l, withAlpha(kGold, a));
|
|
dl->AddTriangleFilled(l, top, ImVec2(c.x, c.y - h * 0.4f), withAlpha(IM_COL32(255, 244, 210, 255), a));
|
|
dl->AddTriangleFilled(ImVec2(c.x, c.y - h * 0.4f), r, b, withAlpha(IM_COL32(170, 128, 60, 255), a));
|
|
}
|
|
|
|
// ---- DialogueBox ---------------------------------------------------------------------------
|
|
|
|
void DialogueBox::start(std::vector<DialogueLine> lines, std::vector<std::string> choices) {
|
|
lines_ = std::move(lines);
|
|
if (lines_.empty()) lines_.emplace_back();
|
|
choices_ = std::move(choices);
|
|
line_ = 0;
|
|
revealed_ = 0;
|
|
budget_ = 0.0f;
|
|
selected_ = 0;
|
|
chosen_ = -1;
|
|
open_ = true;
|
|
}
|
|
|
|
bool DialogueBox::lineRevealed() const { return !open_ || revealed_ >= lines_[line_].text.size(); }
|
|
|
|
bool DialogueBox::choicesVisible() const {
|
|
return open_ && !choices_.empty() && line_ + 1 == lines_.size() && lineRevealed();
|
|
}
|
|
|
|
void DialogueBox::stepChar() {
|
|
const std::string& t = lines_[line_].text;
|
|
const char* p = t.data() + revealed_;
|
|
size_t len = std::min(size_t(utf8Len(static_cast<unsigned char>(*p))), t.size() - revealed_);
|
|
// Swallow malformed continuation bytes so we always land on a boundary.
|
|
while (revealed_ + len < t.size() && (static_cast<unsigned char>(t[revealed_ + len]) & 0xC0) == 0x80) ++len;
|
|
revealed_ += len;
|
|
bool atBreak = revealed_ >= t.size() || t[revealed_] == ' ' || t[revealed_] == '\n' || t[revealed_] == '"' ||
|
|
t[revealed_] == '\'' || t[revealed_] == ')';
|
|
if (atBreak && revealed_ < t.size()) {
|
|
if (isSentenceEnd(p, len)) budget_ -= sentencePause;
|
|
else if (isClauseEnd(p, len)) budget_ -= clausePause;
|
|
}
|
|
}
|
|
|
|
void DialogueBox::update(float dt) {
|
|
if (!open_) return;
|
|
time_ += dt;
|
|
if (lineRevealed()) {
|
|
budget_ = 0.0f;
|
|
return;
|
|
}
|
|
budget_ += dt;
|
|
const float cost = 1.0f / std::max(1.0f, charsPerSecond);
|
|
while (!lineRevealed() && budget_ >= cost) {
|
|
budget_ -= cost;
|
|
stepChar();
|
|
}
|
|
}
|
|
|
|
bool DialogueBox::advance() {
|
|
if (!open_) return false;
|
|
if (!lineRevealed()) {
|
|
revealed_ = lines_[line_].text.size();
|
|
return false;
|
|
}
|
|
if (line_ + 1 < lines_.size()) {
|
|
++line_;
|
|
revealed_ = 0;
|
|
budget_ = 0.0f;
|
|
return false;
|
|
}
|
|
chosen_ = choices_.empty() ? -1 : selected_;
|
|
open_ = false;
|
|
return true;
|
|
}
|
|
|
|
void DialogueBox::moveChoice(int delta) {
|
|
if (!choicesVisible()) return;
|
|
int n = int(choices_.size());
|
|
selected_ = ((selected_ + delta) % n + n) % n;
|
|
}
|
|
|
|
void DialogueBox::close() {
|
|
open_ = false;
|
|
chosen_ = -1;
|
|
}
|
|
|
|
void DialogueBox::draw(ImVec2 viewport) {
|
|
if (!open_) return;
|
|
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
|
const DialogueLine& line = lines_[line_];
|
|
const float s = scaleFor(viewport);
|
|
ImFont* body = orDefault(g_fonts.body);
|
|
const float size = std::round(kBodySize * s);
|
|
const float lh = lineAdvance(size);
|
|
const float padX = std::round(48.0f * s), padY = std::round(30.0f * s);
|
|
|
|
const float boxW = std::round(std::min(viewport.x * 0.70f, viewport.y * 1.6f));
|
|
float textX = padX, textW = boxW - padX * 2.0f;
|
|
const float minTextH = lh * 2.0f + size; // room for three lines
|
|
float portrait = 0.0f;
|
|
if (line.portrait) {
|
|
portrait = std::round(minTextH + padY * 2.0f - 28.0f * s);
|
|
textX = std::round(20.0f * s + portrait + 30.0f * s);
|
|
textW = boxW - textX - padX;
|
|
}
|
|
|
|
static thread_local std::vector<Span> spans;
|
|
const char* tb = line.text.data();
|
|
const char* te = tb + line.text.size();
|
|
layoutLines(body, size, tb, te, textW, spans);
|
|
const float textH = std::max(minTextH, lh * float(spans.size() - 1) + size);
|
|
const float boxH = std::round(textH + padY * 2.0f);
|
|
const ImVec2 min = snap(ImVec2((viewport.x - boxW) * 0.5f, viewport.y - std::round(40.0f * s) - boxH));
|
|
const ImVec2 max = min + ImVec2(boxW, boxH);
|
|
|
|
PanelStyle ps;
|
|
ps.scale = s;
|
|
panel(dl, min, max, ps);
|
|
|
|
if (line.portrait) {
|
|
ImVec2 p0 = snap(ImVec2(min.x + 20.0f * s, min.y + (boxH - portrait) * 0.5f)), p1 = p0 + ImVec2(portrait, portrait);
|
|
dl->AddRectFilled(p0, p1, IM_COL32(4, 6, 12, 200), 3.0f * s);
|
|
dl->AddImage(line.portrait, p0, p1, line.uv0, line.uv1);
|
|
dl->AddRect(p0 - ImVec2(0.5f, 0.5f), p1 + ImVec2(0.5f, 0.5f), withAlpha(kGoldDim, 0.9f), 3.0f * s, 0,
|
|
std::max(1.0f, s));
|
|
}
|
|
|
|
// Typewriter: every line was wrapped from the full text, so words never jump.
|
|
const char* shown = tb + revealed_;
|
|
ImVec2 pos(min.x + textX, min.y + padY);
|
|
for (const Span& sp : spans) {
|
|
if (sp.b >= shown) break;
|
|
drawSpan(dl, body, size, pos, kTextColor, sp.b, std::min(sp.e, shown), true);
|
|
pos.y += lh;
|
|
}
|
|
|
|
// Gold speaker name plate straddling the top-left edge.
|
|
if (!line.speaker.empty()) {
|
|
ImFont* nf = orDefault(g_fonts.name);
|
|
const float ns = std::round(kNameSize * s);
|
|
ImVec2 ts = measure(nf, ns, line.speaker);
|
|
ImVec2 sz(std::round(ts.x + 44.0f * s), std::round(ns + 16.0f * s));
|
|
ImVec2 n0 = snap(ImVec2(min.x + 46.0f * s, min.y - sz.y * 0.62f)), n1 = n0 + sz;
|
|
const float r = 4.0f * s;
|
|
for (int i = 4; i >= 1; --i) {
|
|
float e = 2.0f * s * float(i);
|
|
dl->AddRectFilled(n0 - ImVec2(e, e) + ImVec2(0, 3.0f * s), n1 + ImVec2(e, e) + ImVec2(0, 3.0f * s),
|
|
IM_COL32(0, 0, 0, 22), r + e);
|
|
}
|
|
verticalGradient(dl, n0.y, n1.y, IM_COL32(246, 222, 160, 255), IM_COL32(170, 124, 56, 255),
|
|
[&] { dl->AddRectFilled(n0, n1, IM_COL32_WHITE, r); });
|
|
dl->AddRect(n0 + ImVec2(0.5f, 0.5f), n1 - ImVec2(0.5f, 0.5f), IM_COL32(92, 64, 26, 255), r, 0, std::max(1.0f, s));
|
|
float in = std::round(3.0f * s);
|
|
dl->AddRect(n0 + ImVec2(in, in) + ImVec2(0.5f, 0.5f), n1 - ImVec2(in, in) - ImVec2(0.5f, 0.5f),
|
|
IM_COL32(255, 244, 210, 150), std::max(0.0f, r - in), 0, 1.0f);
|
|
ImVec2 tp = snap(ImVec2(n0.x + (sz.x - ts.x) * 0.5f, n0.y + (sz.y - ts.y) * 0.5f));
|
|
const char* sb = line.speaker.data();
|
|
const char* se = sb + line.speaker.size();
|
|
dl->AddText(nf, ns, tp + ImVec2(0, 1.0f), IM_COL32(255, 248, 225, 140), sb, se); // engraved highlight
|
|
dl->AddText(nf, ns, tp, IM_COL32(40, 26, 10, 255), sb, se);
|
|
}
|
|
|
|
if (choicesVisible()) {
|
|
menuAt(dl, ImVec2(max.x - 28.0f * s, min.y - 14.0f * s), ImVec2(1.0f, 1.0f), choices_, selected_, {}, s * 0.9f);
|
|
} else if (lineRevealed()) {
|
|
advanceIndicator(dl, ImVec2(max.x - 34.0f * s, max.y - 26.0f * s), s, time_);
|
|
}
|
|
}
|
|
|
|
// ---- Toasts -------------------------------------------------------------------------------
|
|
|
|
void Toasts::push(std::string text, ImU32 accent, float seconds) {
|
|
items_.push_back({std::move(text), accent, std::max(seconds, 0.6f)});
|
|
if (items_.size() > 6) items_.erase(items_.begin());
|
|
}
|
|
|
|
void Toasts::update(float dt) {
|
|
for (Item& it : items_) it.age += dt;
|
|
std::erase_if(items_, [](const Item& it) { return it.age >= it.seconds; });
|
|
}
|
|
|
|
void Toasts::draw(ImVec2 viewport) {
|
|
if (items_.empty()) return;
|
|
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
|
const float s = scaleFor(viewport);
|
|
ImFont* font = orDefault(g_fonts.small);
|
|
const float size = std::round(kSmallSize * s);
|
|
const float padX = std::round(28.0f * s), padY = std::round(12.0f * s), gemW = std::round(22.0f * s);
|
|
float y = std::round(64.0f * s);
|
|
for (const Item& it : items_) {
|
|
float in = std::clamp(it.age / 0.25f, 0.0f, 1.0f), out = std::clamp((it.seconds - it.age) / 0.5f, 0.0f, 1.0f);
|
|
float a = std::min(in, out);
|
|
in = 1.0f - (1.0f - in) * (1.0f - in);
|
|
ImVec2 ts = measure(font, size, it.text);
|
|
ImVec2 sz(std::round(ts.x + padX * 2.0f + gemW), std::round(size + padY * 2.0f));
|
|
ImVec2 min = snap(ImVec2((viewport.x - sz.x) * 0.5f, y - (1.0f - in) * 14.0f * s)), max = min + sz;
|
|
PanelStyle ps;
|
|
ps.scale = s;
|
|
ps.ornate = false;
|
|
ps.rounding = 4.0f;
|
|
ps.alpha = 0.85f;
|
|
ps.tint = withAlpha(IM_COL32_WHITE, a);
|
|
panel(dl, min, max, ps);
|
|
gem(dl, ImVec2(min.x + padX + 4.0f * s, (min.y + max.y) * 0.5f), 6.0f * s, it.accent,
|
|
mix(it.accent, IM_COL32_WHITE, 0.55f), a);
|
|
fadeLine(dl, ImVec2(min.x + 10.0f * s, max.y - 7.0f * s), ImVec2(max.x - 10.0f * s, max.y - 7.0f * s),
|
|
std::max(1.0f, s), withAlpha(it.accent, 0.7f * a));
|
|
text(dl, font, size, ImVec2(min.x + padX + gemW, min.y + padY - 1.0f * s), withAlpha(kTextColor, a), it.text);
|
|
y += std::round((sz.y + 10.0f * s) * std::max(in, 0.0f));
|
|
}
|
|
}
|
|
|
|
} // namespace hd2d::ui
|