diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index be1f667..a643005 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -79,6 +79,18 @@ target_link_libraries(volk PUBLIC Vulkan::Headers) # Dear ImGui and YAPF have no CMake of their own; build them here. FetchContent_MakeAvailable(imgui yapf) +# One hook in ImGui: RenderFrame (the frame behind buttons, fields, checkboxes, combos) asks the +# engine's UI first, which draws Niveus's keys and wells instead (engine/ui/ui.cpp). +file(READ ${imgui_SOURCE_DIR}/imgui.cpp _imgui_cpp) +if(NOT _imgui_cpp MATCHES "GImRenderFrameHook") + set(_from "void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders, float rounding)\n{\n ImGuiContext& g = *GImGui;\n ImGuiWindow* window = g.CurrentWindow;\n") + string(FIND "${_imgui_cpp}" "${_from}" _at) + if(_at EQUAL -1) + message(FATAL_ERROR "ImGui's RenderFrame has changed: update the hook in cmake/Dependencies.cmake") + endif() + string(REPLACE "${_from}" "bool (*GImRenderFrameHook)(ImDrawList*, ImVec2, ImVec2, ImU32, bool, float) = nullptr;\n${_from} if (GImRenderFrameHook && GImRenderFrameHook(window->DrawList, p_min, p_max, fill_col, borders, rounding))\n return;\n" _imgui_cpp "${_imgui_cpp}") + file(WRITE ${imgui_SOURCE_DIR}/imgui.cpp "${_imgui_cpp}") +endif() add_library(imgui STATIC ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp ${imgui_SOURCE_DIR}/imgui_widgets.cpp diff --git a/content/engine/fonts/InstrumentSans-Bold.ttf b/content/engine/fonts/InstrumentSans-Bold.ttf new file mode 100644 index 0000000..9a55c17 Binary files /dev/null and b/content/engine/fonts/InstrumentSans-Bold.ttf differ diff --git a/content/engine/fonts/InstrumentSans-Regular.ttf b/content/engine/fonts/InstrumentSans-Regular.ttf new file mode 100644 index 0000000..0eb2260 Binary files /dev/null and b/content/engine/fonts/InstrumentSans-Regular.ttf differ diff --git a/content/engine/fonts/InstrumentSans-SemiBold.ttf b/content/engine/fonts/InstrumentSans-SemiBold.ttf new file mode 100644 index 0000000..5c3a092 Binary files /dev/null and b/content/engine/fonts/InstrumentSans-SemiBold.ttf differ diff --git a/content/engine/fonts/JetBrainsMono-Regular.ttf b/content/engine/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..660323e Binary files /dev/null and b/content/engine/fonts/JetBrainsMono-Regular.ttf differ diff --git a/content/engine/fonts/OFL-InstrumentSans.txt b/content/engine/fonts/OFL-InstrumentSans.txt new file mode 100644 index 0000000..26bd2f9 --- /dev/null +++ b/content/engine/fonts/OFL-InstrumentSans.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/content/engine/fonts/OFL-JetBrainsMono.txt b/content/engine/fonts/OFL-JetBrainsMono.txt new file mode 100644 index 0000000..821a3da --- /dev/null +++ b/content/engine/fonts/OFL-JetBrainsMono.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/engine/ui/ui.cpp b/engine/ui/ui.cpp index 3489d8c..7426bae 100644 --- a/engine/ui/ui.cpp +++ b/engine/ui/ui.cpp @@ -5,15 +5,21 @@ #include #include +#include + #include +#include #include +// ImGui's RenderFrame asks this first (patched in by cmake/Dependencies.cmake). +extern bool (*GImRenderFrameHook)(ImDrawList*, ImVec2, ImVec2, ImU32, bool, float); + namespace keel::ui { namespace { -ImFont* g_fonts[3] = {}; +ImFont* g_fonts[4] = {}; float g_scale = 1.0f; -ImFont* loadFont(const char* path, float size) { +ImFont* loadFont(const char* path, float size, const char* fallback = nullptr) { auto data = fs::read(path); if (!data) { log::warn("Font {} not found, using the built-in one", path); @@ -21,74 +27,285 @@ ImFont* loadFont(const char* path, float size) { } void* owned = IM_ALLOC(data->size()); // the atlas frees it std::memcpy(owned, data->data(), data->size()); - return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(owned, int(data->size()), size); + ImFont* f = ImGui::GetIO().Fonts->AddFontFromMemoryTTF(owned, int(data->size()), size); + if (auto more = fallback ? fs::read(fallback) : std::nullopt) { // what the face hasn't got (arrows, symbols): from another + ImFontConfig cfg; + cfg.MergeMode = true; + void* o = IM_ALLOC(more->size()); + std::memcpy(o, more->data(), more->size()); + ImGui::GetIO().Fonts->AddFontFromMemoryTTF(o, int(more->size()), size, &cfg); + } + return f; } -void theme() { - ImGuiStyle& s = ImGui::GetStyle(); - s = ImGuiStyle(); - s.WindowRounding = 2; - s.FrameRounding = 1; - s.GrabRounding = 1; - s.TabRounding = 1; - s.ScrollbarRounding = 1; - s.WindowBorderSize = 1; - s.FrameBorderSize = 1; - s.WindowPadding = ImVec2(14, 12); - s.FramePadding = ImVec2(8, 5); - s.ItemSpacing = ImVec2(10, 8); - s.WindowTitleAlign = ImVec2(0.0f, 0.5f); - ImVec4* c = s.Colors; +// ── Colour: Niveus's derivation (tokens/color.css, themes/*.css), in OKLab ── +struct Lab { + float L, a, b, alpha = 1.0f; +}; +float toLinear(float c) { return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); } +float toSrgb(float c) { return c <= 0.0031308f ? 12.92f * c : 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f; } +Lab lab(uint32_t rgb, float alpha = 1.0f) { + const float r = toLinear(float((rgb >> 16) & 255) / 255.0f), g = toLinear(float((rgb >> 8) & 255) / 255.0f), b = toLinear(float(rgb & 255) / 255.0f); + const float l = std::cbrt(0.4122214708f * r + 0.5363325363f * g + 0.0514459929f * b); + const float m = std::cbrt(0.2119034982f * r + 0.6806995451f * g + 0.1073969566f * b); + const float s = std::cbrt(0.0883024619f * r + 0.2817188376f * g + 0.6299787005f * b); + return {0.2104542553f * l + 0.7936177850f * m - 0.0040720468f * s, 1.9779984951f * l - 2.4285922050f * m + 0.4505937099f * s, + 0.0259040371f * l + 0.7827717662f * m - 0.8086757660f * s, alpha}; +} +ImVec4 rgba(const Lab& c) { + const float l = std::pow(c.L + 0.3963377774f * c.a + 0.2158037573f * c.b, 3.0f); + const float m = std::pow(c.L - 0.1055613458f * c.a - 0.0638541728f * c.b, 3.0f); + const float s = std::pow(c.L - 0.0894841775f * c.a - 1.2914855480f * c.b, 3.0f); + auto out = [](float v) { return std::clamp(toSrgb(std::max(v, 0.0f)), 0.0f, 1.0f); }; + return ImVec4(out(4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s), out(-1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s), + out(-0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s), c.alpha); +} +Lab mix(const Lab& x, const Lab& y, float t) { return {x.L + (y.L - x.L) * t, x.a + (y.a - x.a) * t, x.b + (y.b - x.b) * t, 1.0f}; } +float chroma(const Lab& c) { return std::hypot(c.a, c.b); } +// The same hue with lightness L and chroma C (oklch(from c L C h)). +Lab lch(const Lab& c, float L, float C, float alpha = 1.0f) { + const float k = chroma(c) > 1e-6f ? C / chroma(c) : 0.0f; + return {L, c.a * k, c.b * k, alpha}; +} +// A fill pushed out of the mid-tones until its label reads (--nv-contrast-*). +Lab fill(const Lab& c) { return lch(c, c.L < 0.7f ? std::min(c.L, 0.53f) : std::max(c.L, 0.78f), std::clamp(chroma(c), 0.02f, 0.25f)); } +Lab onFill(const Lab& f) { return lch(f, 0.14f + std::clamp((0.68f - f.L) * 100.0f, 0.0f, 1.0f) * 0.84f, chroma(f) * 0.2f); } + +uint32_t g_brand = 0xff7828; // Fry3DX's hot orange +Appearance g_appearance = Appearance::System; +bool g_dark = true, g_styled = false; +ImVec4 g_role[size_t(Role::Count)]; + +void derive(bool darkLook) { + const Lab brand = lab(g_brand); + auto tint = [&](uint32_t grey, float t = 0.04f) { return rgba(mix(lab(grey), brand, t)); }; auto rgb = [](int r, int g, int b, float a = 1.0f) { return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a); }; - // Source 2013 GameUI's charcoal panels, grey frames and near-white text, with a red - // where Source has its orange. - const ImVec4 red = rgb(kAccent[0], kAccent[1], kAccent[2]), redLight = rgb(245, 70, 76), redDark = rgb(150, 24, 32); - c[ImGuiCol_Text] = rgb(235, 230, 220); - c[ImGuiCol_TextDisabled] = rgb(140, 136, 128); - c[ImGuiCol_WindowBg] = rgb(40, 40, 38, 0.94f); - c[ImGuiCol_ChildBg] = rgb(30, 30, 29, 0.6f); - c[ImGuiCol_PopupBg] = rgb(36, 36, 34, 0.98f); - c[ImGuiCol_Border] = rgb(90, 88, 82, 0.8f); - c[ImGuiCol_FrameBg] = rgb(28, 28, 27); - c[ImGuiCol_FrameBgHovered] = rgb(50, 49, 46); - c[ImGuiCol_FrameBgActive] = rgb(60, 58, 54); - c[ImGuiCol_TitleBg] = rgb(52, 51, 48); - c[ImGuiCol_TitleBgActive] = rgb(70, 30, 32); - c[ImGuiCol_Button] = rgb(70, 68, 63); - c[ImGuiCol_ButtonHovered] = rgb(95, 92, 84); - c[ImGuiCol_ButtonActive] = red; - c[ImGuiCol_Header] = rgb(70, 68, 63); - c[ImGuiCol_HeaderHovered] = rgb(95, 92, 84); - c[ImGuiCol_HeaderActive] = red; - c[ImGuiCol_Tab] = rgb(55, 54, 50); - c[ImGuiCol_TabHovered] = rgb(95, 92, 84); - c[ImGuiCol_TabSelected] = redDark; - c[ImGuiCol_TabSelectedOverline] = red; - c[ImGuiCol_SliderGrab] = red; - c[ImGuiCol_SliderGrabActive] = redLight; - c[ImGuiCol_CheckMark] = redLight; - c[ImGuiCol_TextSelectedBg] = rgb(kAccent[0], kAccent[1], kAccent[2], 0.45f); - c[ImGuiCol_NavCursor] = red; - c[ImGuiCol_Separator] = rgb(90, 88, 82, 0.6f); - c[ImGuiCol_ScrollbarBg] = rgb(28, 28, 27, 0.6f); - c[ImGuiCol_ScrollbarGrab] = rgb(80, 78, 72); + auto set = [](Role r, ImVec4 v) { g_role[size_t(r)] = v; }; + const Lab accent = fill(brand); + const Lab success = fill(mix(lab(0x189a60), brand, 0.12f)), warning = fill(mix(lab(0xeda520), brand, 0.12f)), danger = fill(mix(lab(0xe2334a), brand, 0.12f)); + set(Role::OnAccent, rgba(onFill(accent))); + set(Role::OnDanger, rgba(onFill(danger))); + set(Role::Success, rgba(success)), set(Role::Warning, rgba(warning)), set(Role::Danger, rgba(danger)); + if (darkLook) { + set(Role::Background, tint(0x121214)), set(Role::BackgroundSubtle, tint(0x18181b)), set(Role::Surface, tint(0x1f1f22)); + set(Role::SurfaceElevated, tint(0x2a2a2e)), set(Role::SurfaceInset, tint(0x111113)), set(Role::SurfaceHover, tint(0x313136)); + set(Role::SurfaceActive, tint(0x39393f)), set(Role::Scrim, rgb(6, 6, 8, 0.62f)); + set(Role::Text, rgb(0xec, 0xeb, 0xe8)), set(Role::TextSecondary, tint(0xa9a8a3, 0.08f)), set(Role::TextTertiary, tint(0x7d7c77, 0.08f)); + set(Role::TextDisabled, rgb(0x58, 0x58, 0x5c)); + set(Role::BorderSubtle, rgb(255, 255, 255, 0.06f)), set(Role::Border, rgb(255, 255, 255, 0.1f)), set(Role::BorderStrong, rgb(255, 255, 255, 0.2f)); + set(Role::Accent, rgba(accent)), set(Role::AccentHover, rgba(lch(accent, accent.L + 0.05f, chroma(accent)))); + set(Role::AccentActive, rgba(lch(accent, accent.L + 0.09f, chroma(accent)))); + set(Role::AccentSubtle, rgba({accent.L, accent.a, accent.b, 0.2f})), set(Role::AccentSubtleHover, rgba({accent.L, accent.a, accent.b, 0.28f})); + set(Role::AccentText, rgba(lch(accent, std::clamp(accent.L + 0.2f, 0.74f, 0.9f), chroma(accent) * 0.9f))); + auto text = [](const Lab& c) { return rgba(lch(c, std::clamp(c.L + 0.2f, 0.76f, 0.9f), chroma(c) * 0.9f)); }; + set(Role::SuccessText, text(success)), set(Role::WarningText, text(warning)), set(Role::DangerText, text(danger)); + set(Role::Highlight, rgb(255, 255, 255, 0.11f)), set(Role::Lowlight, rgb(0, 0, 0, 0.42f)), set(Role::Outline, rgb(0, 0, 0, 0.72f)); + set(Role::Shadow, rgb(0, 0, 0, 0.5f)), set(Role::ShadowInset, rgb(0, 0, 0, 0.65f)); + set(Role::SheenTop, rgb(255, 255, 255, 0.06f)), set(Role::SheenBottom, rgb(0, 0, 0, 0.09f)); + set(Role::SheenStrongTop, rgb(255, 255, 255, 0.2f)), set(Role::SheenStrongBottom, rgb(0, 0, 0, 0.12f)); + } else { + set(Role::Background, tint(0xe8e5de)), set(Role::BackgroundSubtle, tint(0xeeece6)), set(Role::Surface, rgb(0xfb, 0xfb, 0xfa)); + set(Role::SurfaceElevated, rgb(0xf8, 0xf8, 0xf6)), set(Role::SurfaceInset, tint(0xe3e2dd)), set(Role::SurfaceHover, tint(0xf1f0ec)); + set(Role::SurfaceActive, tint(0xe6e5e0)), set(Role::Scrim, rgb(28, 24, 18, 0.34f)); + set(Role::Text, rgb(0x1d, 0x1d, 0x20)), set(Role::TextSecondary, tint(0x63625d, 0.08f)), set(Role::TextTertiary, tint(0x87867f, 0.08f)); + set(Role::TextDisabled, rgb(0xae, 0xad, 0xa7)); + set(Role::BorderSubtle, rgb(28, 24, 18, 0.07f)), set(Role::Border, rgb(28, 24, 18, 0.13f)), set(Role::BorderStrong, rgb(28, 24, 18, 0.22f)); + set(Role::Accent, rgba(accent)), set(Role::AccentHover, rgba(lch(accent, accent.L - 0.045f, chroma(accent)))); + set(Role::AccentActive, rgba(lch(accent, accent.L - 0.09f, chroma(accent)))); + set(Role::AccentSubtle, rgba(lch(accent, 0.965f, chroma(accent) * 0.16f))), set(Role::AccentSubtleHover, rgba(lch(accent, 0.93f, chroma(accent) * 0.22f))); + set(Role::AccentText, rgba(lch(accent, std::clamp(accent.L - 0.12f, 0.3f, 0.5f), chroma(accent)))); + auto text = [](const Lab& c) { return rgba(lch(c, std::clamp(c.L - 0.1f, 0.3f, 0.48f), chroma(c))); }; + set(Role::SuccessText, text(success)), set(Role::WarningText, text(warning)), set(Role::DangerText, text(danger)); + set(Role::Highlight, rgb(255, 255, 255, 0.9f)), set(Role::Lowlight, rgb(40, 32, 20, 0.07f)), set(Role::Outline, rgb(40, 32, 20, 0.05f)); + set(Role::Shadow, rgb(40, 32, 20, 0.12f)), set(Role::ShadowInset, rgb(40, 32, 20, 0.17f)); + set(Role::SheenTop, rgb(255, 255, 255, 0.75f)), set(Role::SheenBottom, rgb(40, 32, 20, 0.04f)); + set(Role::SheenStrongTop, rgb(255, 255, 255, 0.24f)), set(Role::SheenStrongBottom, rgb(20, 16, 10, 0.1f)); + } +} +ImVec4 role(Role r) { return g_role[size_t(r)]; } +ImVec4 withAlpha(ImVec4 c, float a) { return c.w = a, c; } + +void theme() { + derive(g_dark); + ImGuiStyle& s = ImGui::GetStyle(); + const float fontScale = s.FontScaleMain, fontDpi = s.FontScaleDpi; + s = ImGuiStyle(); + s.FontScaleMain = fontScale, s.FontScaleDpi = fontDpi; + // Niveus's radii and 4px rhythm. Frames have no border of ImGui's: their edges are the + // keys' and wells' own (drawn by frameHook). + s.WindowRounding = 8, s.ChildRounding = 6, s.PopupRounding = 8, s.FrameRounding = 6, s.GrabRounding = 5, s.TabRounding = 6; + s.ScrollbarRounding = 8, s.ScrollbarSize = 11, s.GrabMinSize = 12; + s.WindowBorderSize = 1, s.PopupBorderSize = 1, s.FrameBorderSize = 0, s.TabBorderSize = 0, s.DockingSeparatorSize = 2; + s.WindowPadding = ImVec2(10, 10), s.FramePadding = ImVec2(8, 5), s.ItemSpacing = ImVec2(8, 6), s.ItemInnerSpacing = ImVec2(6, 4); + s.SeparatorTextBorderSize = 1, s.SeparatorTextPadding = ImVec2(10, 3); + s.WindowTitleAlign = ImVec2(0.0f, 0.5f); + s.WindowMenuButtonPosition = ImGuiDir_None; + ImVec4* c = s.Colors; + c[ImGuiCol_Text] = role(Role::Text); + c[ImGuiCol_TextDisabled] = role(Role::TextTertiary); + c[ImGuiCol_WindowBg] = role(Role::Surface); + c[ImGuiCol_ChildBg] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_PopupBg] = role(Role::SurfaceElevated); + c[ImGuiCol_Border] = role(Role::Border); + c[ImGuiCol_BorderShadow] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_FrameBg] = role(Role::SurfaceInset); + c[ImGuiCol_FrameBgHovered] = g_dark ? role(Role::Background) : role(Role::SurfaceActive); + c[ImGuiCol_FrameBgActive] = c[ImGuiCol_FrameBgHovered]; + c[ImGuiCol_TitleBg] = c[ImGuiCol_TitleBgActive] = c[ImGuiCol_TitleBgCollapsed] = role(Role::BackgroundSubtle); + c[ImGuiCol_MenuBarBg] = role(Role::BackgroundSubtle); + c[ImGuiCol_ScrollbarBg] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_ScrollbarGrab] = role(Role::BorderStrong); + c[ImGuiCol_ScrollbarGrabHovered] = role(Role::TextTertiary); + c[ImGuiCol_ScrollbarGrabActive] = role(Role::TextSecondary); + c[ImGuiCol_CheckMark] = role(Role::Accent); + c[ImGuiCol_SliderGrab] = role(Role::Accent); + c[ImGuiCol_SliderGrabActive] = role(Role::AccentHover); + c[ImGuiCol_Button] = role(Role::SurfaceElevated); + c[ImGuiCol_ButtonHovered] = role(Role::SurfaceHover); + c[ImGuiCol_ButtonActive] = role(Role::SurfaceActive); + c[ImGuiCol_Header] = role(Role::AccentSubtle); + c[ImGuiCol_HeaderHovered] = role(Role::SurfaceHover); + c[ImGuiCol_HeaderActive] = role(Role::AccentSubtleHover); + c[ImGuiCol_Separator] = role(Role::BorderSubtle); + c[ImGuiCol_SeparatorHovered] = c[ImGuiCol_SeparatorActive] = role(Role::Accent); + c[ImGuiCol_ResizeGrip] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_ResizeGripHovered] = c[ImGuiCol_ResizeGripActive] = role(Role::AccentSubtleHover); + c[ImGuiCol_InputTextCursor] = role(Role::Accent); + c[ImGuiCol_Tab] = c[ImGuiCol_TabDimmed] = role(Role::BackgroundSubtle); + c[ImGuiCol_TabHovered] = role(Role::SurfaceHover); + c[ImGuiCol_TabSelected] = c[ImGuiCol_TabDimmedSelected] = role(Role::Surface); + c[ImGuiCol_TabSelectedOverline] = role(Role::Accent); + c[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_DockingPreview] = withAlpha(role(Role::Accent), 0.4f); + c[ImGuiCol_DockingEmptyBg] = role(Role::Background); + c[ImGuiCol_PlotLines] = c[ImGuiCol_PlotHistogram] = role(Role::Accent); + c[ImGuiCol_PlotLinesHovered] = c[ImGuiCol_PlotHistogramHovered] = role(Role::AccentHover); + c[ImGuiCol_TableHeaderBg] = role(Role::BackgroundSubtle); + c[ImGuiCol_TableBorderStrong] = role(Role::Border); + c[ImGuiCol_TableBorderLight] = role(Role::BorderSubtle); + c[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0); + c[ImGuiCol_TableRowBgAlt] = withAlpha(role(Role::Text), 0.03f); + c[ImGuiCol_TextLink] = role(Role::AccentText); + c[ImGuiCol_TextSelectedBg] = withAlpha(role(Role::Accent), 0.3f); + c[ImGuiCol_TreeLines] = role(Role::BorderSubtle); + c[ImGuiCol_DragDropTarget] = role(Role::Accent); + c[ImGuiCol_NavCursor] = withAlpha(role(Role::Accent), 0.6f); + c[ImGuiCol_NavWindowingHighlight] = role(Role::Accent); + c[ImGuiCol_NavWindowingDimBg] = c[ImGuiCol_ModalWindowDimBg] = withAlpha(role(Role::Scrim), role(Role::Scrim).w * 0.5f); // (an editor: the work stays readable behind) +} + +ImU32 u32(ImVec4 c) { return ImGui::ColorConvertFloat4ToU32(c); } +ImVec4 over(ImVec4 base, ImVec4 top) { // `top` laid over `base` + const float a = top.w; + return ImVec4(base.x + (top.x - base.x) * a, base.y + (top.y - base.y) * a, base.z + (top.z - base.z) * a, base.w); +} +bool saturated(ImVec4 c) { return std::max({c.x, c.y, c.z}) - std::min({c.x, c.y, c.z}) > 0.3f; } + +// A key with `fillCol` as its face: contact shadow, the face with light falling down it, a lit top +// edge and a shaded bottom one; pressed, it sinks: flat, with the shade inside along its top. +void key(ImDrawList* dl, ImVec2 a, ImVec2 b, float r, ImVec4 face, bool pressed) { + const bool filled = saturated(face); + if (pressed) { + dl->AddRectFilled(a, b, u32(face), r); + dl->AddRect(a, b, u32(role(Role::Border)), r); + dl->PushClipRect(a, b, true); + dl->AddRect(ImVec2(a.x, a.y), ImVec2(b.x, b.y + 4), u32(withAlpha(role(Role::ShadowInset), role(Role::ShadowInset).w * 0.6f)), r, 0, 2.0f); + dl->PopClipRect(); + return; + } + dl->AddRectFilled(ImVec2(a.x, a.y + 1), ImVec2(b.x, b.y + 2), u32(withAlpha(role(Role::Shadow), role(Role::Shadow).w * 0.7f)), r); // its shadow + if (g_dark) dl->AddRect(ImVec2(a.x - 0.5f, a.y - 0.5f), ImVec2(b.x + 0.5f, b.y + 0.5f), u32(role(Role::Outline)), r + 0.5f); // the contact line + const ImVec4 top = over(face, role(filled ? Role::SheenStrongTop : Role::SheenTop)), bottom = over(face, role(filled ? Role::SheenStrongBottom : Role::SheenBottom)); + const int v0 = dl->VtxBuffer.Size; + dl->AddRectFilled(a, b, u32(face), r); + ImGui::ShadeVertsLinearColorGradientKeepAlpha(dl, v0, dl->VtxBuffer.Size, a, ImVec2(a.x, b.y), u32(top), u32(bottom)); + if (!filled) dl->AddRect(a, b, u32(role(Role::Border)), r); + const float in = std::min(r * 0.7f, (b.x - a.x) * 0.3f); + dl->AddLine(ImVec2(a.x + in, a.y + 1.0f), ImVec2(b.x - in, a.y + 1.0f), u32(filled ? withAlpha(role(Role::Highlight), 0.35f) : role(Role::Highlight))); + dl->AddLine(ImVec2(a.x + in, b.y - 1.0f), ImVec2(b.x - in, b.y - 1.0f), u32(role(Role::Lowlight))); +} + +// ImGui's RenderFrame (patched in, see cmake/Dependencies.cmake): buttons become keys and fields wells. +bool frameHook(ImDrawList* dl, ImVec2 a, ImVec2 b, ImU32 col, bool, float r) { + if (!g_styled) return false; + const ImU32 normal = ImGui::GetColorU32(ImGuiCol_Button), hovered = ImGui::GetColorU32(ImGuiCol_ButtonHovered), held = ImGui::GetColorU32(ImGuiCol_ButtonActive); + if (col == normal || col == hovered || col == held) { + key(dl, a, b, r, ImGui::ColorConvertU32ToFloat4(col), col == held && held != normal); + return true; + } + if (col == ImGui::GetColorU32(ImGuiCol_FrameBg) || col == ImGui::GetColorU32(ImGuiCol_FrameBgHovered) || col == ImGui::GetColorU32(ImGuiCol_FrameBgActive)) { + drawWell(dl, a, b, r, col != ImGui::GetColorU32(ImGuiCol_FrameBg)); + return true; + } + return false; +} + +Appearance fromSystem() { return SDL_GetSystemTheme() == SDL_SYSTEM_THEME_LIGHT ? Appearance::Light : Appearance::Dark; } +void restyle() { + g_dark = (g_appearance == Appearance::System ? fromSystem() : g_appearance) != Appearance::Light; + theme(); + g_styled = true; } } // namespace void init() { ImGuiIO& io = ImGui::GetIO(); - g_fonts[0] = loadFont("fonts/DejaVuSans.ttf", 16.0f); - g_fonts[1] = loadFont("fonts/DejaVuSans-Bold.ttf", 16.0f); - g_fonts[2] = loadFont("fonts/DejaVuSans-Bold.ttf", 48.0f); + g_fonts[0] = loadFont("fonts/InstrumentSans-Regular.ttf", 16.0f, "fonts/DejaVuSans.ttf"); + g_fonts[1] = loadFont("fonts/InstrumentSans-SemiBold.ttf", 16.0f, "fonts/DejaVuSans-Bold.ttf"); + g_fonts[2] = loadFont("fonts/InstrumentSans-Bold.ttf", 48.0f); + g_fonts[3] = loadFont("fonts/JetBrainsMono-Regular.ttf", 15.0f, "fonts/DejaVuSans.ttf"); + if (!g_fonts[0]) g_fonts[0] = loadFont("fonts/DejaVuSans.ttf", 16.0f); // (an older content folder) if (g_fonts[0]) io.FontDefault = g_fonts[0]; - theme(); + GImRenderFrameHook = frameHook; + restyle(); +} + +void setAppearance(Appearance a) { + g_appearance = a; + restyle(); +} +Appearance appearance() { return g_appearance; } +bool dark() { return g_dark; } +void setBrand(uint8_t r, uint8_t g, uint8_t b) { + g_brand = uint32_t(r) << 16 | uint32_t(g) << 8 | b; + restyle(); +} + +uint32_t color(Role r, float alpha) { + ImVec4 c = role(r); + c.w *= alpha; + return u32(c); } uint32_t accent(float alpha, float brightness) { - return ImGui::ColorConvertFloat4ToU32(ImVec4(std::min(kAccent[0] / 255.0f * brightness, 1.0f), std::min(kAccent[1] / 255.0f * brightness, 1.0f), - std::min(kAccent[2] / 255.0f * brightness, 1.0f), alpha)); + const ImVec4 c = role(Role::Accent); + return u32(ImVec4(std::min(c.x * brightness, 1.0f), std::min(c.y * brightness, 1.0f), std::min(c.z * brightness, 1.0f), alpha)); } +void drawKey(ImDrawList* dl, const ImVec2& a, const ImVec2& b, float rounding, KeyState state) { + switch (state) { + case KeyState::Normal: return key(dl, a, b, rounding, role(Role::SurfaceElevated), false); + case KeyState::Hovered: return key(dl, a, b, rounding, role(Role::SurfaceHover), false); + case KeyState::Pressed: return key(dl, a, b, rounding, role(Role::SurfaceActive), true); + case KeyState::On: return key(dl, a, b, rounding, role(Role::Accent), false); + } +} + +void drawWell(ImDrawList* dl, const ImVec2& a, const ImVec2& b, float r, bool hovered) { + dl->AddRectFilled(a, b, ImGui::GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), r); + // Shade inside along the top (the light's blocked by the rim), light along the bottom lip. + dl->PushClipRect(a, b, true); + const ImVec4 sh = role(Role::ShadowInset); + dl->AddRect(a, ImVec2(b.x, b.y + 6), u32(withAlpha(sh, sh.w * 0.55f)), r, 0, 1.5f); + dl->AddRect(ImVec2(a.x, a.y + 1), ImVec2(b.x, b.y + 6), u32(withAlpha(sh, sh.w * 0.2f)), r, 0, 1.5f); + dl->PopClipRect(); + dl->AddRect(a, b, u32(role(Role::BorderSubtle)), r); + const float in = std::min(r, (b.x - a.x) * 0.3f); + dl->AddLine(ImVec2(a.x + in, b.y + 0.5f), ImVec2(b.x - in, b.y + 0.5f), u32(withAlpha(role(Role::Highlight), role(Role::Highlight).w * 0.6f))); +} + +uint32_t keyText(KeyState state) { return u32(role(state == KeyState::On ? Role::OnAccent : Role::Text)); } + ImFont* font(Font f) { ImFont* r = g_fonts[int(f)]; return r ? r : ImGui::GetFont(); @@ -122,6 +339,7 @@ void applyPixelScale() { void beginFrame(float windowHeight) { g_scale = std::clamp(windowHeight / 1080.0f, 0.75f, 2.5f); ImGui::GetStyle().FontScaleMain = g_scale; + if (g_appearance == Appearance::System && g_styled && (fromSystem() != Appearance::Light) != g_dark) restyle(); // (the desktop changed) } namespace { @@ -162,7 +380,7 @@ void drawPopups() { // focus cancels the press, so OK would never click. const bool refocus = !ImGui::IsAnyMouseDown(); if (refocus) ImGui::SetNextWindowFocus(); - ImGui::PushStyleColor(ImGuiCol_WindowBg, IM_COL32(0, 0, 0, 120)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, color(Role::Scrim)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::Begin("##popupdim", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove); ImGui::End(); diff --git a/engine/ui/ui.h b/engine/ui/ui.h index 3130928..e2e2f47 100644 --- a/engine/ui/ui.h +++ b/engine/ui/ui.h @@ -1,16 +1,19 @@ #pragma once -// Game UI look: fonts from the game folder and a Source-like theme (dark -// translucent panels, flat buttons, light text, red highlights) on top of Dear ImGui. +// The UI's look: Niveus (github.com/Nexoniarz/niveus) on top of Dear ImGui. Buttons are keys, +// bevelled with light falling across them; fields are wells cut into the surface; warm +// graphite in dark, soft paper in light, and one brand colour that every accent is made from. #include #include #include #include struct ImFont; +struct ImDrawList; +struct ImVec2; namespace keel::ui { -enum class Font { Regular, Bold, Title }; +enum class Font { Regular, Bold, Title, Mono }; // After the renderer (ImGui context) and the VFS are up. void init(); @@ -25,10 +28,38 @@ float pixelScale(); // The backends call this between the platform's NewFrame and ImGui::NewFrame. void applyPixelScale(); -// The highlight colour: highlights, the logo. -inline constexpr uint8_t kAccent[3] = {227, 34, 45}; +// ── Appearance ── +enum class Appearance { System, Dark, Light }; +void setAppearance(Appearance a); // System: follows the desktop's (and changes with it) +Appearance appearance(); +bool dark(); // what's showing now +// The brand colour (sRGB): the accent and its states, the text on it and the tint in the greys +// are all made from it, in both appearances. +void setBrand(uint8_t r, uint8_t g, uint8_t b); + +// Niveus's semantic roles. +enum class Role { + Background, BackgroundSubtle, Surface, SurfaceElevated, SurfaceInset, SurfaceHover, SurfaceActive, Scrim, + Text, TextSecondary, TextTertiary, TextDisabled, + BorderSubtle, Border, BorderStrong, + Accent, AccentHover, AccentActive, AccentSubtle, AccentSubtleHover, AccentText, OnAccent, + Success, SuccessText, Warning, WarningText, Danger, DangerText, OnDanger, + Highlight, Lowlight, Outline, Shadow, ShadowInset, SheenTop, SheenBottom, SheenStrongTop, SheenStrongBottom, + Count +}; +uint32_t color(Role r, float alpha = 1.0f); // ImU32 +// The accent (alpha, and brighter or darker by `brightness`). uint32_t accent(float alpha = 1.0f, float brightness = 1.0f); // ImU32 +// The materials, for widgets drawn by hand. +enum class KeyState { Normal, Hovered, Pressed, On }; +// A raised key; `on` keys are filled with the accent. +void drawKey(ImDrawList* dl, const ImVec2& a, const ImVec2& b, float rounding, KeyState state); +// A well cut into the surface (fields, tracks). +void drawWell(ImDrawList* dl, const ImVec2& a, const ImVec2& b, float rounding, bool hovered = false); +// The text/icon colour that goes on a key in that state. +uint32_t keyText(KeyState state); + // ── Popups: modal messages over everything (disconnects, errors, questions) ── struct PopupButton { std::string label; diff --git a/src/app.cpp b/src/app.cpp index 2d4b924..4ccf238 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -206,7 +206,18 @@ glm::vec3 fromFrame(const Axes& a, const glm::vec3& c) { return a[0] * c.x + a[1 const ImU32 kAxisCol[3] = {IM_COL32(235, 70, 70, 255), IM_COL32(110, 210, 80, 255), IM_COL32(70, 130, 245, 255)}; const ImU32 kHot = IM_COL32(255, 225, 90, 255); constexpr ImU32 kAccent = IM_COL32(255, 120, 40, 255); -const ImVec4 kAccentText(1.0f, 0.6f, 0.3f, 1.0f); +// The UI's colours: Niveus's roles, in the appearance that's showing (keel/ui). +ImVec4 roleColor(keel::ui::Role r) { return ImGui::ColorConvertU32ToFloat4(keel::ui::color(r)); } +ImVec4 accentText() { return roleColor(keel::ui::Role::AccentText); } +// The one button that does it, filled with the accent (until popAccent). +void pushAccent() { + using keel::ui::Role; + ImGui::PushStyleColor(ImGuiCol_Button, roleColor(Role::Accent)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, roleColor(Role::AccentHover)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, roleColor(Role::AccentActive)); + ImGui::PushStyleColor(ImGuiCol_Text, roleColor(Role::OnAccent)); +} +void popAccent() { ImGui::PopStyleColor(4); } // An object as it'll be printed: where it stands, with its finish, checked (kept until it changes). struct PrintInfo { @@ -229,6 +240,7 @@ keel::Cvar fry_touch("fry_touch", "0", keel::CVAR_ARCHIVE, "Fry3DX: the phone la #endif keel::Cvar fry_uiscale("fry_uiscale", "0", 0, "Fry3DX: draw the UI this many times bigger (0: by the screen)"); keel::Cvar fry_uisize("fry_uisize", "1", keel::CVAR_ARCHIVE, "Fry3DX: the phone layout's size, times"); +keel::Cvar fry_theme("fry_theme", "0", keel::CVAR_ARCHIVE, "Fry3DX: how it looks: 0 like the desktop, 1 dark, 2 light"); keel::Cvar fry_autosave("fry_autosave", "2", keel::CVAR_ARCHIVE, "Fry3DX: autosave every this many minutes while there are changes (0: off)"); class App : public keel::Game { @@ -752,6 +764,7 @@ private: bool logoTried_ = false, haveLogo_ = false; // ── UI ── + int appliedTheme_ = -1; bool styled_ = false, layoutDone_ = false, showHelp_ = false, showAbout_ = false; std::string iniPath_, message_; double messageAt_ = -100.0; @@ -1062,9 +1075,9 @@ private: ImGui::Spacing(); const float bw = 150.0f; const int action = askSave_; - ImGui::PushStyleColor(ImGuiCol_Button, kAccent); + pushAccent(); const bool save = ImGui::Button("Save", ImVec2(bw, 0)) || ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter); - ImGui::PopStyleColor(); + popAccent(); ImGui::SameLine(); const bool drop = ImGui::Button(action == 1 ? "Don't save, close" : "Don't save", ImVec2(bw, 0)); ImGui::SameLine(); @@ -2466,7 +2479,7 @@ private: void layoutProperties(float w) { Object* o = selected_.empty() ? nullptr : find(active_); if (!o) return void(ImGui::TextWrapped("Click an object: its picture's layout shows in the Picture panel.")); - ImGui::TextColored(kAccentText, "%s", o->name.c_str()); + ImGui::TextColored(accentText(), "%s", o->name.c_str()); if (!o->picture || !o->uvsFit()) { if (ImGui::Button("Lay it out", ImVec2(w, 0))) readyToPaint(o->id); return; @@ -2683,8 +2696,8 @@ private: } } void printProperties(float w) { - auto good = [](const char* t) { ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.4f, 1), "%s", t); }; - auto bad = [](const std::string& t) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.45f, 0.3f, 1)), ImGui::TextWrapped("%s", t.c_str()), ImGui::PopStyleColor(); }; + auto good = [](const char* t) { ImGui::TextColored(roleColor(keel::ui::Role::SuccessText), "%s", t); }; + auto bad = [](const std::string& t) { ImGui::PushStyleColor(ImGuiCol_Text, roleColor(keel::ui::Role::DangerText)), ImGui::TextWrapped("%s", t.c_str()), ImGui::PopStyleColor(); }; ImGui::SeparatorText("Printer"); int printer = std::clamp(fry_printer.i(), 0, int(std::size(kPrinters)) - 1); ImGui::SetNextItemWidth(w); @@ -2965,7 +2978,7 @@ private: } if (ImGui::IsItemDeactivated()) renamingFolder_ = 0; } else { - ImGui::PushStyleColor(ImGuiCol_Text, kAccentText); + ImGui::PushStyleColor(ImGuiCol_Text, accentText()); const std::string label = std::format("{} ({})", f->name, inside.size()); if (ImGui::Selectable(label.c_str(), picked, ImGuiSelectableFlags_AllowDoubleClick)) { // a folder is a group: all of it picked if (mode_ == Mode::Edit) leaveEdit(); @@ -3179,9 +3192,9 @@ private: } ImGui::Checkbox("Joined into one object", &c.joined); ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_Button, kAccent); + pushAccent(); if (ImGui::Button("Make them", ImVec2(w, 0))) makeCopies(), showCopies_ = false; - ImGui::PopStyleColor(); + popAccent(); ImGui::End(); } void thickenActive() { @@ -3206,9 +3219,9 @@ private: ImGui::SetNextItemWidth(w); if (ImGui::Combo("##dir", &out, "Inwards (a closed shape: hollow)\0Outwards\0")) thicknessOut_ = out == 1; ImGui::TextWrapped("A flat or open surface gets a back and edges: a solid you can print. A closed shape becomes hollow, walls this thick (to save material or make a box)."); - ImGui::PushStyleColor(ImGuiCol_Button, kAccent); + pushAccent(); if (ImGui::Button("Make it thick", ImVec2(w, 0))) thickenActive(), showThicken_ = false; - ImGui::PopStyleColor(); + popAccent(); ImGui::End(); } void cleanFlatFaces() { @@ -3293,7 +3306,7 @@ private: ImGui::SetNextWindowBgAlpha(0.8f); ImGui::Begin("##touchbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking); - if (now() - messageAt_ < 4.0) ImGui::PushTextWrapPos(maxW), ImGui::TextColored(kAccentText, "%s", message_.c_str()), ImGui::PopTextWrapPos(); + if (now() - messageAt_ < 4.0) ImGui::PushTextWrapPos(maxW), ImGui::TextColored(accentText(), "%s", message_.c_str()), ImGui::PopTextWrapPos(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12, 8)); const ImGuiStyle& st = ImGui::GetStyle(); float x = 0; // (where the row's got to) @@ -3302,9 +3315,9 @@ private: if (x > 0 && x + st.ItemSpacing.x + w <= maxW) ImGui::SameLine(), x += st.ItemSpacing.x; else x = 0; x += w; - if (on) ImGui::PushStyleColor(ImGuiCol_Button, kAccent); + if (on) pushAccent(); const bool pressed = ImGui::Button(label); - if (on) ImGui::PopStyleColor(); + if (on) popAccent(); if (tip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip); return pressed; }; @@ -4872,38 +4885,10 @@ private: ImGui::GetIO().IniFilename = iniPath_.c_str(); if (stdfs::exists(iniPath_)) ImGui::LoadIniSettingsFromDisk(iniPath_.c_str()); } + if (fry_theme.i() != appliedTheme_) appliedTheme_ = fry_theme.i(), keel::ui::setAppearance(keel::ui::Appearance(std::clamp(appliedTheme_, 0, 2))); if (styled_) return; styled_ = true; - ImGuiStyle& st = ImGui::GetStyle(); - st.FramePadding = ImVec2(7, 5), st.ItemSpacing = ImVec2(7, 6), st.WindowPadding = ImVec2(9, 9); - st.WindowRounding = 5, st.FrameRounding = 4, st.TabRounding = 4, st.GrabRounding = 4, st.ScrollbarSize = 12; - st.FontScaleDpi = 0.9f; - ImVec4* c = st.Colors; // dark, with Fry3DX's hot orange - const ImVec4 orange(1.0f, 0.47f, 0.16f, 1.0f), dim(0.75f, 0.33f, 0.1f, 1.0f); - c[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.11f, 0.12f, 0.97f); - c[ImGuiCol_ChildBg] = ImVec4(0, 0, 0, 0); - c[ImGuiCol_PopupBg] = ImVec4(0.12f, 0.12f, 0.13f, 0.98f); - c[ImGuiCol_MenuBarBg] = ImVec4(0.09f, 0.09f, 0.1f, 1.0f); - c[ImGuiCol_TitleBg] = c[ImGuiCol_TitleBgActive] = c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.09f, 0.09f, 0.1f, 1.0f); - c[ImGuiCol_FrameBg] = ImVec4(0.18f, 0.18f, 0.2f, 1.0f); - c[ImGuiCol_FrameBgHovered] = ImVec4(0.24f, 0.24f, 0.27f, 1.0f); - c[ImGuiCol_FrameBgActive] = ImVec4(0.28f, 0.28f, 0.31f, 1.0f); - c[ImGuiCol_Button] = ImVec4(0.2f, 0.2f, 0.22f, 1.0f); - c[ImGuiCol_ButtonHovered] = ImVec4(0.3f, 0.3f, 0.33f, 1.0f); - c[ImGuiCol_ButtonActive] = dim; - c[ImGuiCol_Header] = ImVec4(0.22f, 0.22f, 0.25f, 1.0f); - c[ImGuiCol_HeaderHovered] = ImVec4(0.3f, 0.3f, 0.33f, 1.0f); - c[ImGuiCol_HeaderActive] = dim; - c[ImGuiCol_CheckMark] = c[ImGuiCol_SliderGrabActive] = orange; - c[ImGuiCol_SliderGrab] = dim; - c[ImGuiCol_Tab] = ImVec4(0.14f, 0.14f, 0.15f, 1.0f); - c[ImGuiCol_TabHovered] = ImVec4(0.3f, 0.3f, 0.33f, 1.0f); - c[ImGuiCol_TabSelected] = ImVec4(0.2f, 0.2f, 0.22f, 1.0f); - c[ImGuiCol_TabSelectedOverline] = orange; - c[ImGuiCol_TabDimmedSelected] = ImVec4(0.17f, 0.17f, 0.19f, 1.0f); - c[ImGuiCol_SeparatorHovered] = c[ImGuiCol_SeparatorActive] = c[ImGuiCol_ResizeGripActive] = orange; - c[ImGuiCol_DockingPreview] = ImVec4(1.0f, 0.47f, 0.16f, 0.5f); - c[ImGuiCol_TextSelectedBg] = ImVec4(1.0f, 0.47f, 0.16f, 0.35f); + ImGui::GetStyle().FontScaleDpi = 0.92f; // (the look itself: keel/ui, Niveus) } void defaultLayout(ImGuiID dock) { @@ -5046,6 +5031,12 @@ private: ImGui::MenuItem("X-ray (see and pick through)", "Alt+Z", &xray_); ImGui::MenuItem("Grid", nullptr, &showGrid_); ImGui::MenuItem("Ground (in Rendered)", nullptr, &scene_.look.ground); + if (ImGui::BeginMenu("Appearance")) { + const char* looks[] = {"Like the desktop", "Dark", "Light"}; + for (int i = 0; i < 3; ++i) + if (ImGui::MenuItem(looks[i], nullptr, fry_theme.i() == i)) keel::execute(std::format("fry_theme {}", i)); + ImGui::EndMenu(); + } ImGui::Separator(); if (ImGui::MenuItem("Perspective", "Numpad 5", view_ == View::Perspective)) leaveFlatView(); if (ImGui::MenuItem("Front", "Numpad 1", view_ == View::Front)) setView(View::Front); @@ -5072,7 +5063,7 @@ private: ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1); logoImage(h); } - ImGui::TextColored(ImVec4(1.0f, 0.47f, 0.16f, 1.0f), "Fry3DX"); + ImGui::PushFont(keel::ui::font(keel::ui::Font::Bold), 0.0f), ImGui::TextColored(accentText(), "Fry3DX"), ImGui::PopFont(); if (!touch()) menus(); else if (ImGui::BeginMenu("Menu")) { // (a phone: one menu, the others inside it) menus(); @@ -5115,14 +5106,21 @@ private: if (ImGui::MenuItem("Reference picture...")) dialog(4); } - // A square button with a picture on it (orange when it's the one in use). + // How the last item's key looks: filled when it's the one in use, sunk while held. + static keel::ui::KeyState keyState(bool on) { + using keel::ui::KeyState; + return on ? KeyState::On : ImGui::IsItemActive() ? KeyState::Pressed : ImGui::IsItemHovered() ? KeyState::Hovered : KeyState::Normal; + } + // A square button with a picture on it (the accent when it's the one in use). bool iconButton(const char* id, Icon icon, bool on, const char* tip, float size = 38.0f) { const ImVec2 p = ImGui::GetCursorScreenPos(); const bool hit = ImGui::InvisibleButton(id, ImVec2(size, size)); - const bool hov = ImGui::IsItemHovered(); + const keel::ui::KeyState k = keyState(on); ImDrawList* dl = ImGui::GetWindowDrawList(); - dl->AddRectFilled(p, ImVec2(p.x + size, p.y + size), on ? IM_COL32(196, 86, 26, 255) : hov ? IM_COL32(68, 68, 76, 255) : IM_COL32(40, 40, 46, 255), 6.0f); - drawIcon(dl, icon, ImVec2(p.x + size * 0.14f, p.y + size * 0.14f), size * 0.72f, on ? IM_COL32(255, 255, 255, 255) : IM_COL32(215, 215, 222, 255)); + keel::ui::drawKey(dl, p, ImVec2(p.x + size, p.y + size), 7.0f, k); + const float sink = k == keel::ui::KeyState::Pressed ? 1.0f : 0.0f; + drawIcon(dl, icon, ImVec2(p.x + size * 0.14f, p.y + size * 0.14f + sink), size * 0.72f, keel::ui::keyText(k)); + const bool hov = ImGui::IsItemHovered(); if (hov && tip) ImGui::SetTooltip("%s", tip); return hit; } @@ -5130,11 +5128,13 @@ private: bool wideButton(const char* id, Icon icon, const char* label, bool on, float width, const char* tip = nullptr, float h = 30.0f) { const ImVec2 p = ImGui::GetCursorScreenPos(); const bool hit = ImGui::InvisibleButton(id, ImVec2(width, h)); - const bool hov = ImGui::IsItemHovered(); + const keel::ui::KeyState k = keyState(on); ImDrawList* dl = ImGui::GetWindowDrawList(); - dl->AddRectFilled(p, ImVec2(p.x + width, p.y + h), on ? IM_COL32(196, 86, 26, 255) : hov ? IM_COL32(68, 68, 76, 255) : IM_COL32(40, 40, 46, 255), 6.0f); - drawIcon(dl, icon, ImVec2(p.x + h * 0.15f, p.y + h * 0.13f), h * 0.74f, IM_COL32(235, 235, 240, 255)); - dl->AddText(ImVec2(p.x + h + 2, p.y + (h - ImGui::GetFontSize()) * 0.5f), IM_COL32(235, 235, 240, 255), label); + keel::ui::drawKey(dl, p, ImVec2(p.x + width, p.y + h), 7.0f, k); + const float sink = k == keel::ui::KeyState::Pressed ? 1.0f : 0.0f; + drawIcon(dl, icon, ImVec2(p.x + h * 0.15f, p.y + h * 0.13f + sink), h * 0.74f, keel::ui::keyText(k)); + dl->AddText(ImVec2(p.x + h + 2, p.y + (h - ImGui::GetFontSize()) * 0.5f + sink), keel::ui::keyText(k), label); + const bool hov = ImGui::IsItemHovered(); if (hov && tip) ImGui::SetTooltip("%s", tip); return hit; } @@ -5209,7 +5209,7 @@ private: } ImGui::Spacing(); for (const ToolInfo& t : kTools) - if (t.tool == tool_) ImGui::TextColored(kAccentText, "%s", t.name); + if (t.tool == tool_) ImGui::TextColored(accentText(), "%s", t.name); ImGui::End(); } @@ -5230,7 +5230,7 @@ private: ImGui::PopID(); }); for (const PaintToolInfo& t : kPaintTools) - if (t.tool == paintTool_) ImGui::TextColored(kAccentText, "%s", t.name); + if (t.tool == paintTool_) ImGui::TextColored(accentText(), "%s", t.name); } void paintOptions(float w) { @@ -5565,7 +5565,7 @@ private: "or put pictures onto: copy one in any app and press Ctrl+V."); return; } - ImGui::TextColored(kAccentText, "%s", o->name.c_str()); + ImGui::TextColored(accentText(), "%s", o->name.c_str()); if (!o->picture) { int size = pictureSize_ <= 512 ? 0 : pictureSize_ <= 1024 ? 1 : pictureSize_ <= 2048 ? 2 : 3; ImGui::SetNextItemWidth(w); @@ -5992,7 +5992,7 @@ private: void statusBar() { ImGui::Begin("Status", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar); - if (now() - messageAt_ < 4.0) ImGui::TextColored(kAccentText, "%s", message_.c_str()); + if (now() - messageAt_ < 4.0) ImGui::TextColored(accentText(), "%s", message_.c_str()); else if (tab_ == Tab::Layout) { for (const LayToolInfo& t : kLayTools) if (t.tool == layTool_) @@ -6019,7 +6019,7 @@ private: ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking); static const char* names[] = {"Perspective", "Front", "Back", "Left", "Right", "Top", "Bottom"}; static const char* shadings[] = {"Solid", "Rendered", "Wireframe"}; - ImGui::TextColored(kAccentText, "%s", names[int(view_)]); + ImGui::TextColored(accentText(), "%s", names[int(view_)]); ImGui::SameLine(); ImGui::TextDisabled("%s%s", shadings[int(shading_)], xray_ ? ", X-ray" : ""); for (int i = 1; i < 7; ++i) { @@ -6058,7 +6058,7 @@ private: for (auto [k, what] : list) { ImGui::TableNextRow(); ImGui::TableNextColumn(); - ImGui::TextColored(kAccentText, "%s", k); + ImGui::TextColored(accentText(), "%s", k); ImGui::TableNextColumn(); ImGui::TextWrapped("%s", what); } @@ -6097,7 +6097,7 @@ private: logoImage(160); const char* title = "Fry3DX — Frying 3D Extreme"; ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize(title).x) * 0.5f); - ImGui::TextColored(ImVec4(1.0f, 0.47f, 0.16f, 1.0f), "%s", title); + ImGui::TextColored(accentText(), "%s", title); ImGui::TextWrapped("A 3D modeller that does what you'd expect: pick a tool, click and drag. Made by Nexoniarz; open source (Apache License 2.0)."); ImGui::End(); }