mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 13:33:42 +08:00
95ed0213ec
* cabana: refine signal heatmap grid * cabana: restore direct signal color blending * cabana: strengthen heatmap signal saturation * Revert "cabana: strengthen heatmap signal saturation" This reverts commit 07ee9664d533f260b5bbfaed1afc4391c795603c. * cabana: remove signal cell gradients
252 lines
11 KiB
C++
252 lines
11 KiB
C++
#include "tools/cabana/ui/theme.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
|
|
#include "implot.h"
|
|
#include "tools/cabana/core/settings.h"
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
constexpr Palette DARK_PALETTE = {
|
|
.text = rgb(0xffffff), .text_disabled = rgb(0xcccccc), .text_inactive = rgb(0xa4a4a4), .text_selected = rgb(0xffffff),
|
|
.window = rgb(0x353535), .surface = rgb(0x2b2c2d),
|
|
.frame = rgb(0x2b2c2d), .frame_hovered = rgb(0x363738), .frame_active = rgb(0x434445),
|
|
.button = rgb(0x3f4041), .button_hovered = rgb(0x525354), .button_active = rgb(0x303132),
|
|
.header = rgb(0x2d74d2), .header_hovered = rgb(0x323944), .header_active = rgb(0x434445),
|
|
.accent = rgb(0x90bfff),
|
|
.border = rgb(0xaaaaaa), .separator = rgb(0x353535), .scrollbar_grab = rgb(0xaaaaaa),
|
|
.slider_track = rgb(0xaaaaaa),
|
|
.tab = rgb(0x2c2c2c), .tab_hovered = rgb(0x444647), .tab_selected = rgb(0x393a3b), .table_header = rgb(0x3f4041),
|
|
.grid = rgb(0xdddddd, 50.0f / 255.0f), .badge = rgb(0x626262),
|
|
.sparkline_saturation = 1.0f, .sparkline_value = 1.0f,
|
|
};
|
|
|
|
constexpr Palette LIGHT_PALETTE = {
|
|
.text = rgb(0x000000), .text_disabled = rgb(0x595959), .text_inactive = rgb(0x595959), .text_selected = rgb(0xffffff),
|
|
.window = rgb(0xefefef), .surface = rgb(0xffffff),
|
|
.frame = rgb(0xffffff), .frame_hovered = rgb(0xf5f9fc), .frame_active = rgb(0xe7f3fb),
|
|
.button = rgb(0xefefef), .button_hovered = rgb(0xe7f3fb), .button_active = rgb(0xd4e7f4),
|
|
.header = rgb(0x226a99), .header_hovered = rgb(0xe7f3fb), .header_active = rgb(0xd4e7f4),
|
|
.accent = rgb(0x226a99),
|
|
.border = rgb(0x747474), .separator = rgb(0xd0d0d0), .scrollbar_grab = rgb(0x747474),
|
|
.slider_track = rgb(0x747474),
|
|
.tab = rgb(0xe5e5e5), .tab_hovered = rgb(0xefefef), .tab_selected = rgb(0xffffff), .table_header = rgb(0xefefef),
|
|
.grid = rgb(0x000000, 50.0f / 255.0f), .badge = rgb(0x626262),
|
|
.sparkline_saturation = 2.0f, .sparkline_value = 0.7f,
|
|
};
|
|
|
|
const Palette *g_palette = &LIGHT_PALETTE;
|
|
ImFont *g_ui_font = nullptr;
|
|
ImFont *g_bold_font = nullptr;
|
|
ImFont *g_mono_font = nullptr;
|
|
ImFont *g_large_font = nullptr;
|
|
|
|
void addIconFont(float size, ImFont *base) {
|
|
ImFontConfig cfg;
|
|
cfg.MergeMode = base != nullptr;
|
|
cfg.GlyphMinAdvanceX = size;
|
|
if (base != nullptr) {
|
|
ImFontBaked *baked = base->GetFontBaked(size);
|
|
const float center = baked != nullptr ? (baked->Ascent + baked->Descent) * 0.5f : size * 0.5f;
|
|
cfg.GlyphOffset.y = std::round(size * 0.5f - center);
|
|
}
|
|
static const ImWchar ranges[] = {0xF000, 0xF8FF, 0};
|
|
ImGui::GetIO().Fonts->AddFontFromFileTTF(BOOTSTRAP_ICONS_TTF, size, &cfg, ranges);
|
|
}
|
|
|
|
ImFont *addFont(const fs::path &path, float size) {
|
|
ImFontConfig cfg;
|
|
cfg.OversampleH = 2;
|
|
cfg.OversampleV = 2;
|
|
ImFont *font = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &cfg);
|
|
if (font != nullptr) addIconFont(size, font);
|
|
return font;
|
|
}
|
|
|
|
double linearChannel(double value) {
|
|
value /= 255.0;
|
|
return value <= 0.04045 ? value / 12.92 : std::pow((value + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
double luminance(const CabanaColor &color) {
|
|
return 0.2126 * linearChannel(color.r) + 0.7152 * linearChannel(color.g) + 0.0722 * linearChannel(color.b);
|
|
}
|
|
|
|
// Directed rounding preserves the luminance bound in 8-bit sRGB.
|
|
CabanaColor boundLuminance(const CabanaColor &color, double bound, bool minimum) {
|
|
const double current = luminance(color);
|
|
if (minimum ? current >= bound : current <= bound) return color;
|
|
auto channel = [&](uint8_t value) {
|
|
double c = linearChannel(value);
|
|
c = minimum ? c + (1.0 - c) * (bound - current) / (1.0 - current) : c * bound / current;
|
|
const double srgb = c <= 0.0031308 ? 12.92 * c : 1.055 * std::pow(c, 1.0 / 2.4) - 0.055;
|
|
const double scaled = std::clamp(srgb * 255.0, 0.0, 255.0);
|
|
return static_cast<uint8_t>(minimum ? std::ceil(scaled) : std::floor(scaled));
|
|
};
|
|
return {channel(color.r), channel(color.g), channel(color.b), color.a};
|
|
}
|
|
|
|
CabanaColor composite(const CabanaColor &color, const CabanaColor &base) {
|
|
auto channel = [&](int foreground, int background) {
|
|
return static_cast<uint8_t>(std::lround((foreground * color.a + background * (255 - color.a)) / 255.0));
|
|
};
|
|
return {channel(color.r, base.r), channel(color.g, base.g), channel(color.b, base.b)};
|
|
}
|
|
|
|
ImVec4 alpha(ImVec4 c, float a) { return ImVec4(c.x, c.y, c.z, a); }
|
|
|
|
} // namespace
|
|
|
|
void loadFonts() {
|
|
ImGuiIO &io = ImGui::GetIO();
|
|
const fs::path fonts = fs::path(CABANA_FONTS_DIR);
|
|
g_ui_font = addFont(fonts / "Inter-Regular.ttf", UI_FONT_SIZE);
|
|
g_bold_font = addFont(fonts / "Inter-SemiBold.ttf", UI_FONT_SIZE);
|
|
g_mono_font = addFont(fonts / "JetBrainsMono-Medium.ttf", 15.0f);
|
|
g_large_font = addFont(fonts / "Inter-Bold.ttf", 50.0f);
|
|
if (g_ui_font != nullptr) io.FontDefault = g_ui_font;
|
|
if (g_bold_font == nullptr) g_bold_font = g_ui_font;
|
|
if (g_mono_font == nullptr) g_mono_font = g_ui_font;
|
|
if (g_large_font == nullptr) g_large_font = g_bold_font;
|
|
}
|
|
|
|
void applyTheme(int theme) {
|
|
g_palette = theme == DARK_THEME ? &DARK_PALETTE : &LIGHT_PALETTE;
|
|
const Palette &p = *g_palette;
|
|
const ImVec4 none(0, 0, 0, 0);
|
|
|
|
ImGuiStyle &style = ImGui::GetStyle();
|
|
style = ImGuiStyle();
|
|
style.WindowRounding = 6.0f; // dialogs and tooltips; docked panels and os windows ignore it
|
|
style.ChildRounding = 6.0f;
|
|
style.PopupRounding = 6.0f;
|
|
style.FrameRounding = 4.0f;
|
|
style.GrabRounding = 4.0f;
|
|
style.ScrollbarRounding = 4.0f;
|
|
style.TabRounding = 4.0f;
|
|
style.WindowBorderSize = 1.0f;
|
|
style.FrameBorderSize = 1.0f;
|
|
style.TabBorderSize = 1.0f;
|
|
style.WindowPadding = ImVec2(spacing::CONTROL, spacing::CONTROL);
|
|
style.FramePadding = ImVec2(spacing::CONTROL, spacing::INNER);
|
|
style.ItemSpacing = ImVec2(spacing::CONTROL, spacing::CONTROL);
|
|
style.ItemInnerSpacing = ImVec2(spacing::INNER, spacing::INNER);
|
|
style.CellPadding = ImVec2(6.0f, 4.0f);
|
|
style.ScrollbarSize = 14.0f;
|
|
style.GrabMinSize = 13.0f;
|
|
|
|
ImVec4 *c = style.Colors;
|
|
c[ImGuiCol_Text] = p.text;
|
|
c[ImGuiCol_TextDisabled] = p.text_disabled;
|
|
c[ImGuiCol_WindowBg] = c[ImGuiCol_ScrollbarBg] = c[ImGuiCol_DockingEmptyBg] = p.window;
|
|
c[ImGuiCol_MenuBarBg] = p.window;
|
|
c[ImGuiCol_TitleBg] = c[ImGuiCol_TitleBgActive] = c[ImGuiCol_TitleBgCollapsed] = p.window;
|
|
c[ImGuiCol_ChildBg] = c[ImGuiCol_PopupBg] = p.surface;
|
|
c[ImGuiCol_Border] = c[ImGuiCol_TableBorderStrong] = p.border;
|
|
c[ImGuiCol_Separator] = c[ImGuiCol_TableBorderLight] = p.separator;
|
|
c[ImGuiCol_BorderShadow] = c[ImGuiCol_ResizeGrip] = c[ImGuiCol_TableRowBg] = none;
|
|
c[ImGuiCol_FrameBg] = p.frame;
|
|
c[ImGuiCol_FrameBgHovered] = p.frame_hovered;
|
|
c[ImGuiCol_FrameBgActive] = p.frame_active;
|
|
c[ImGuiCol_Button] = p.button;
|
|
c[ImGuiCol_ButtonHovered] = p.button_hovered;
|
|
c[ImGuiCol_SliderGrab] = p.accent;
|
|
c[ImGuiCol_ScrollbarGrab] = p.scrollbar_grab;
|
|
c[ImGuiCol_ScrollbarGrabHovered] = p.text_disabled;
|
|
c[ImGuiCol_ButtonActive] = p.button_active;
|
|
c[ImGuiCol_Header] = p.header;
|
|
c[ImGuiCol_HeaderHovered] = p.header_hovered;
|
|
c[ImGuiCol_HeaderActive] = p.header_active;
|
|
c[ImGuiCol_CheckMark] = c[ImGuiCol_NavCursor] = c[ImGuiCol_SliderGrabActive] = c[ImGuiCol_ScrollbarGrabActive] = p.accent;
|
|
c[ImGuiCol_SeparatorActive] = c[ImGuiCol_ResizeGripActive] = c[ImGuiCol_DragDropTarget] = p.accent;
|
|
c[ImGuiCol_TabSelectedOverline] = c[ImGuiCol_PlotHistogram] = p.accent;
|
|
c[ImGuiCol_SeparatorHovered] = c[ImGuiCol_ResizeGripHovered] = alpha(p.accent, 0.6f);
|
|
c[ImGuiCol_DockingPreview] = alpha(p.accent, 0.5f);
|
|
c[ImGuiCol_TextSelectedBg] = alpha(p.accent, 0.35f);
|
|
c[ImGuiCol_Tab] = c[ImGuiCol_TabDimmed] = p.tab;
|
|
c[ImGuiCol_TabHovered] = p.tab_hovered;
|
|
c[ImGuiCol_TabSelected] = c[ImGuiCol_TabDimmedSelected] = p.tab_selected;
|
|
c[ImGuiCol_TabDimmedSelectedOverline] = none;
|
|
c[ImGuiCol_TableHeaderBg] = p.table_header;
|
|
c[ImGuiCol_TableRowBgAlt] = none;
|
|
c[ImGuiCol_PlotLines] = p.text;
|
|
// ImGuiStyle() seeds every slot from the dark theme: set the rest so the light theme does not keep a white caret.
|
|
c[ImGuiCol_InputTextCursor] = c[ImGuiCol_UnsavedMarker] = p.text;
|
|
c[ImGuiCol_TextLink] = c[ImGuiCol_PlotLinesHovered] = c[ImGuiCol_PlotHistogramHovered] = c[ImGuiCol_NavWindowingHighlight] = p.accent;
|
|
c[ImGuiCol_TreeLines] = p.separator;
|
|
c[ImGuiCol_DragDropTargetBg] = alpha(p.accent, 0.2f);
|
|
// Disable the modal dim fade to make dialogs appear immediately.
|
|
c[ImGuiCol_ModalWindowDimBg] = c[ImGuiCol_NavWindowingDimBg] = none;
|
|
|
|
// ChartView::drawAxes() pushes the other plot colors it needs; the rest are auto colors derived from the ImGui style.
|
|
ImPlot::GetStyle().Colors[ImPlotCol_AxisGrid] = p.grid;
|
|
}
|
|
|
|
const Palette &palette() { return *g_palette; }
|
|
|
|
CabanaColor contrastColor(CabanaColor color, const CabanaColor &background, double target) {
|
|
color.a = 255;
|
|
const double value = luminance(color), base = luminance(background);
|
|
if ((std::max(value, base) + 0.05) / (std::min(value, base) + 0.05) >= target) return color;
|
|
const bool lighter = base < 0.179;
|
|
const double bound = lighter ? target * (base + 0.05) - 0.05 : (base + 0.05) / target - 0.05;
|
|
return boundLuminance(color, std::clamp(bound, 0.0, 1.0), lighter);
|
|
}
|
|
|
|
CabanaColor byteColor(const CabanaColor &color) {
|
|
const bool dark = palette().text.x > 0.5f;
|
|
const auto [h, s, v] = color.hsv();
|
|
auto fill = CabanaColor::fromHsv(h, std::min(s, 0.30f), 0.86f, color.alphaF());
|
|
if (dark) fill = boundLuminance(fill, 0.16, false); // 5:1 with white text
|
|
fill = composite(fill, fromImVec4(palette().surface));
|
|
return dark ? fill : composite({255, 255, 255, 40}, fill);
|
|
}
|
|
|
|
CabanaColor signalFill(const CabanaColor &color, bool defined) {
|
|
const bool dark = palette().text.x > 0.5f;
|
|
// Constrain the full-strength color before blending to preserve the activity ramp.
|
|
if (defined) return composite(boundLuminance(color, 0.18, !dark), fromImVec4(palette().surface));
|
|
auto fill = composite(color, fromImVec4(palette().surface));
|
|
fill = composite(dark ? CabanaColor(0, 0, 0, 115) : CabanaColor(255, 255, 255, 40), fill);
|
|
return boundLuminance(fill, 0.18, !dark); // 4.5:1 with the theme's text
|
|
}
|
|
|
|
CabanaColor signalHighlight(CabanaColor color) {
|
|
color.a = 255;
|
|
return boundLuminance(boundLuminance(color, 0.175, true), 0.18, false);
|
|
}
|
|
|
|
CabanaColor signalOutline(CabanaColor color, bool hovered) {
|
|
const bool dark = palette().text.x > 0.5f;
|
|
if (dark && hovered) color = color.lighter(150);
|
|
color.a = 255;
|
|
// 3:1 against normal and highlighted fills, including neighboring signals.
|
|
return dark ? boundLuminance(color, 0.67, true) : boundLuminance(color, 0.02, false);
|
|
}
|
|
|
|
CabanaColor graphicColor(const CabanaColor &color, const ImVec4 &background) {
|
|
return contrastColor(color, fromImVec4(background), 3.0);
|
|
}
|
|
|
|
CabanaColor sparklineColor(const CabanaColor &color) {
|
|
const Palette &p = palette();
|
|
auto [h, s, v] = color.hsv();
|
|
return graphicColor(CabanaColor::fromHsv(h, std::min(1.0f, s * p.sparkline_saturation), v * p.sparkline_value));
|
|
}
|
|
|
|
ImFont *boldFont() { return g_bold_font; }
|
|
|
|
void pushMonoFont(float size) {
|
|
if (!g_mono_font) return;
|
|
size > 0.0f ? ImGui::PushFont(g_mono_font, size) : ImGui::PushFont(g_mono_font);
|
|
}
|
|
void popMonoFont() { if (g_mono_font) ImGui::PopFont(); }
|
|
void pushBoldFont() { if (g_bold_font) ImGui::PushFont(g_bold_font); }
|
|
void popBoldFont() { if (g_bold_font) ImGui::PopFont(); }
|
|
void pushLargeFont() { if (g_large_font) ImGui::PushFont(g_large_font); }
|
|
void popLargeFont() { if (g_large_font) ImGui::PopFont(); }
|