#include "tools/cabana/ui/theme.h" #include #include #include #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(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(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(); }