The Niveus look in the app: bevelled keys and inset wells (a hook in ImGui's RenderFrame), warm graphite and paper appearances made from one brand colour, Instrument Sans and JetBrains Mono; View > Appearance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-20 16:56:20 +02:00
commit 53d8ecbfcd
10 changed files with 565 additions and 118 deletions

View file

@ -79,6 +79,18 @@ target_link_libraries(volk PUBLIC Vulkan::Headers)
# Dear ImGui and YAPF have no CMake of their own; build them here. # Dear ImGui and YAPF have no CMake of their own; build them here.
FetchContent_MakeAvailable(imgui yapf) 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 add_library(imgui STATIC
${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp ${imgui_SOURCE_DIR}/imgui.cpp ${imgui_SOURCE_DIR}/imgui_draw.cpp
${imgui_SOURCE_DIR}/imgui_tables.cpp ${imgui_SOURCE_DIR}/imgui_widgets.cpp ${imgui_SOURCE_DIR}/imgui_tables.cpp ${imgui_SOURCE_DIR}/imgui_widgets.cpp

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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.

View file

@ -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.

View file

@ -5,15 +5,21 @@
#include <imgui.h> #include <imgui.h>
#include <imgui_internal.h> #include <imgui_internal.h>
#include <SDL3/SDL_video.h>
#include <algorithm> #include <algorithm>
#include <cmath>
#include <cstring> #include <cstring>
// 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 keel::ui {
namespace { namespace {
ImFont* g_fonts[3] = {}; ImFont* g_fonts[4] = {};
float g_scale = 1.0f; 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); auto data = fs::read(path);
if (!data) { if (!data) {
log::warn("Font {} not found, using the built-in one", path); 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 void* owned = IM_ALLOC(data->size()); // the atlas frees it
std::memcpy(owned, data->data(), data->size()); 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() { // ── Colour: Niveus's derivation (tokens/color.css, themes/*.css), in OKLab ──
ImGuiStyle& s = ImGui::GetStyle(); struct Lab {
s = ImGuiStyle(); float L, a, b, alpha = 1.0f;
s.WindowRounding = 2; };
s.FrameRounding = 1; float toLinear(float c) { return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); }
s.GrabRounding = 1; float toSrgb(float c) { return c <= 0.0031308f ? 12.92f * c : 1.055f * std::pow(c, 1.0f / 2.4f) - 0.055f; }
s.TabRounding = 1; Lab lab(uint32_t rgb, float alpha = 1.0f) {
s.ScrollbarRounding = 1; 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);
s.WindowBorderSize = 1; const float l = std::cbrt(0.4122214708f * r + 0.5363325363f * g + 0.0514459929f * b);
s.FrameBorderSize = 1; const float m = std::cbrt(0.2119034982f * r + 0.6806995451f * g + 0.1073969566f * b);
s.WindowPadding = ImVec2(14, 12); const float s = std::cbrt(0.0883024619f * r + 0.2817188376f * g + 0.6299787005f * b);
s.FramePadding = ImVec2(8, 5); return {0.2104542553f * l + 0.7936177850f * m - 0.0040720468f * s, 1.9779984951f * l - 2.4285922050f * m + 0.4505937099f * s,
s.ItemSpacing = ImVec2(10, 8); 0.0259040371f * l + 0.7827717662f * m - 0.8086757660f * s, alpha};
s.WindowTitleAlign = ImVec2(0.0f, 0.5f); }
ImVec4* c = s.Colors; 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); }; 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 auto set = [](Role r, ImVec4 v) { g_role[size_t(r)] = v; };
// where Source has its orange. const Lab accent = fill(brand);
const ImVec4 red = rgb(kAccent[0], kAccent[1], kAccent[2]), redLight = rgb(245, 70, 76), redDark = rgb(150, 24, 32); 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));
c[ImGuiCol_Text] = rgb(235, 230, 220); set(Role::OnAccent, rgba(onFill(accent)));
c[ImGuiCol_TextDisabled] = rgb(140, 136, 128); set(Role::OnDanger, rgba(onFill(danger)));
c[ImGuiCol_WindowBg] = rgb(40, 40, 38, 0.94f); set(Role::Success, rgba(success)), set(Role::Warning, rgba(warning)), set(Role::Danger, rgba(danger));
c[ImGuiCol_ChildBg] = rgb(30, 30, 29, 0.6f); if (darkLook) {
c[ImGuiCol_PopupBg] = rgb(36, 36, 34, 0.98f); set(Role::Background, tint(0x121214)), set(Role::BackgroundSubtle, tint(0x18181b)), set(Role::Surface, tint(0x1f1f22));
c[ImGuiCol_Border] = rgb(90, 88, 82, 0.8f); set(Role::SurfaceElevated, tint(0x2a2a2e)), set(Role::SurfaceInset, tint(0x111113)), set(Role::SurfaceHover, tint(0x313136));
c[ImGuiCol_FrameBg] = rgb(28, 28, 27); set(Role::SurfaceActive, tint(0x39393f)), set(Role::Scrim, rgb(6, 6, 8, 0.62f));
c[ImGuiCol_FrameBgHovered] = rgb(50, 49, 46); set(Role::Text, rgb(0xec, 0xeb, 0xe8)), set(Role::TextSecondary, tint(0xa9a8a3, 0.08f)), set(Role::TextTertiary, tint(0x7d7c77, 0.08f));
c[ImGuiCol_FrameBgActive] = rgb(60, 58, 54); set(Role::TextDisabled, rgb(0x58, 0x58, 0x5c));
c[ImGuiCol_TitleBg] = rgb(52, 51, 48); 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));
c[ImGuiCol_TitleBgActive] = rgb(70, 30, 32); set(Role::Accent, rgba(accent)), set(Role::AccentHover, rgba(lch(accent, accent.L + 0.05f, chroma(accent))));
c[ImGuiCol_Button] = rgb(70, 68, 63); set(Role::AccentActive, rgba(lch(accent, accent.L + 0.09f, chroma(accent))));
c[ImGuiCol_ButtonHovered] = rgb(95, 92, 84); set(Role::AccentSubtle, rgba({accent.L, accent.a, accent.b, 0.2f})), set(Role::AccentSubtleHover, rgba({accent.L, accent.a, accent.b, 0.28f}));
c[ImGuiCol_ButtonActive] = red; set(Role::AccentText, rgba(lch(accent, std::clamp(accent.L + 0.2f, 0.74f, 0.9f), chroma(accent) * 0.9f)));
c[ImGuiCol_Header] = rgb(70, 68, 63); auto text = [](const Lab& c) { return rgba(lch(c, std::clamp(c.L + 0.2f, 0.76f, 0.9f), chroma(c) * 0.9f)); };
c[ImGuiCol_HeaderHovered] = rgb(95, 92, 84); set(Role::SuccessText, text(success)), set(Role::WarningText, text(warning)), set(Role::DangerText, text(danger));
c[ImGuiCol_HeaderActive] = red; 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));
c[ImGuiCol_Tab] = rgb(55, 54, 50); set(Role::Shadow, rgb(0, 0, 0, 0.5f)), set(Role::ShadowInset, rgb(0, 0, 0, 0.65f));
c[ImGuiCol_TabHovered] = rgb(95, 92, 84); set(Role::SheenTop, rgb(255, 255, 255, 0.06f)), set(Role::SheenBottom, rgb(0, 0, 0, 0.09f));
c[ImGuiCol_TabSelected] = redDark; set(Role::SheenStrongTop, rgb(255, 255, 255, 0.2f)), set(Role::SheenStrongBottom, rgb(0, 0, 0, 0.12f));
c[ImGuiCol_TabSelectedOverline] = red; } else {
c[ImGuiCol_SliderGrab] = red; set(Role::Background, tint(0xe8e5de)), set(Role::BackgroundSubtle, tint(0xeeece6)), set(Role::Surface, rgb(0xfb, 0xfb, 0xfa));
c[ImGuiCol_SliderGrabActive] = redLight; set(Role::SurfaceElevated, rgb(0xf8, 0xf8, 0xf6)), set(Role::SurfaceInset, tint(0xe3e2dd)), set(Role::SurfaceHover, tint(0xf1f0ec));
c[ImGuiCol_CheckMark] = redLight; set(Role::SurfaceActive, tint(0xe6e5e0)), set(Role::Scrim, rgb(28, 24, 18, 0.34f));
c[ImGuiCol_TextSelectedBg] = rgb(kAccent[0], kAccent[1], kAccent[2], 0.45f); set(Role::Text, rgb(0x1d, 0x1d, 0x20)), set(Role::TextSecondary, tint(0x63625d, 0.08f)), set(Role::TextTertiary, tint(0x87867f, 0.08f));
c[ImGuiCol_NavCursor] = red; set(Role::TextDisabled, rgb(0xae, 0xad, 0xa7));
c[ImGuiCol_Separator] = rgb(90, 88, 82, 0.6f); 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));
c[ImGuiCol_ScrollbarBg] = rgb(28, 28, 27, 0.6f); set(Role::Accent, rgba(accent)), set(Role::AccentHover, rgba(lch(accent, accent.L - 0.045f, chroma(accent))));
c[ImGuiCol_ScrollbarGrab] = rgb(80, 78, 72); 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 } // namespace
void init() { void init() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
g_fonts[0] = loadFont("fonts/DejaVuSans.ttf", 16.0f); g_fonts[0] = loadFont("fonts/InstrumentSans-Regular.ttf", 16.0f, "fonts/DejaVuSans.ttf");
g_fonts[1] = loadFont("fonts/DejaVuSans-Bold.ttf", 16.0f); g_fonts[1] = loadFont("fonts/InstrumentSans-SemiBold.ttf", 16.0f, "fonts/DejaVuSans-Bold.ttf");
g_fonts[2] = loadFont("fonts/DejaVuSans-Bold.ttf", 48.0f); 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]; 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) { 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), const ImVec4 c = role(Role::Accent);
std::min(kAccent[2] / 255.0f * brightness, 1.0f), alpha)); 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* font(Font f) {
ImFont* r = g_fonts[int(f)]; ImFont* r = g_fonts[int(f)];
return r ? r : ImGui::GetFont(); return r ? r : ImGui::GetFont();
@ -122,6 +339,7 @@ void applyPixelScale() {
void beginFrame(float windowHeight) { void beginFrame(float windowHeight) {
g_scale = std::clamp(windowHeight / 1080.0f, 0.75f, 2.5f); g_scale = std::clamp(windowHeight / 1080.0f, 0.75f, 2.5f);
ImGui::GetStyle().FontScaleMain = g_scale; ImGui::GetStyle().FontScaleMain = g_scale;
if (g_appearance == Appearance::System && g_styled && (fromSystem() != Appearance::Light) != g_dark) restyle(); // (the desktop changed)
} }
namespace { namespace {
@ -162,7 +380,7 @@ void drawPopups() {
// focus cancels the press, so OK would never click. // focus cancels the press, so OK would never click.
const bool refocus = !ImGui::IsAnyMouseDown(); const bool refocus = !ImGui::IsAnyMouseDown();
if (refocus) ImGui::SetNextWindowFocus(); 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::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::Begin("##popupdim", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove); ImGui::Begin("##popupdim", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove);
ImGui::End(); ImGui::End();

View file

@ -1,16 +1,19 @@
#pragma once #pragma once
// Game UI look: fonts from the game folder and a Source-like theme (dark // The UI's look: Niveus (github.com/Nexoniarz/niveus) on top of Dear ImGui. Buttons are keys,
// translucent panels, flat buttons, light text, red highlights) on top of Dear ImGui. // 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 <cstdint> #include <cstdint>
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
struct ImFont; struct ImFont;
struct ImDrawList;
struct ImVec2;
namespace keel::ui { 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. // After the renderer (ImGui context) and the VFS are up.
void init(); void init();
@ -25,10 +28,38 @@ float pixelScale();
// The backends call this between the platform's NewFrame and ImGui::NewFrame. // The backends call this between the platform's NewFrame and ImGui::NewFrame.
void applyPixelScale(); void applyPixelScale();
// The highlight colour: highlights, the logo. // ── Appearance ──
inline constexpr uint8_t kAccent[3] = {227, 34, 45}; 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 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) ── // ── Popups: modal messages over everything (disconnects, errors, questions) ──
struct PopupButton { struct PopupButton {
std::string label; std::string label;

View file

@ -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 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); const ImU32 kHot = IM_COL32(255, 225, 90, 255);
constexpr ImU32 kAccent = IM_COL32(255, 120, 40, 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). // An object as it'll be printed: where it stands, with its finish, checked (kept until it changes).
struct PrintInfo { struct PrintInfo {
@ -229,6 +240,7 @@ keel::Cvar fry_touch("fry_touch", "0", keel::CVAR_ARCHIVE, "Fry3DX: the phone la
#endif #endif
keel::Cvar fry_uiscale("fry_uiscale", "0", 0, "Fry3DX: draw the UI this many times bigger (0: by the screen)"); 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_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)"); 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 { class App : public keel::Game {
@ -752,6 +764,7 @@ private:
bool logoTried_ = false, haveLogo_ = false; bool logoTried_ = false, haveLogo_ = false;
// ── UI ── // ── UI ──
int appliedTheme_ = -1;
bool styled_ = false, layoutDone_ = false, showHelp_ = false, showAbout_ = false; bool styled_ = false, layoutDone_ = false, showHelp_ = false, showAbout_ = false;
std::string iniPath_, message_; std::string iniPath_, message_;
double messageAt_ = -100.0; double messageAt_ = -100.0;
@ -1062,9 +1075,9 @@ private:
ImGui::Spacing(); ImGui::Spacing();
const float bw = 150.0f; const float bw = 150.0f;
const int action = askSave_; 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); const bool save = ImGui::Button("Save", ImVec2(bw, 0)) || ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter);
ImGui::PopStyleColor(); popAccent();
ImGui::SameLine(); ImGui::SameLine();
const bool drop = ImGui::Button(action == 1 ? "Don't save, close" : "Don't save", ImVec2(bw, 0)); const bool drop = ImGui::Button(action == 1 ? "Don't save, close" : "Don't save", ImVec2(bw, 0));
ImGui::SameLine(); ImGui::SameLine();
@ -2466,7 +2479,7 @@ private:
void layoutProperties(float w) { void layoutProperties(float w) {
Object* o = selected_.empty() ? nullptr : find(active_); 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.")); 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 (!o->picture || !o->uvsFit()) {
if (ImGui::Button("Lay it out", ImVec2(w, 0))) readyToPaint(o->id); if (ImGui::Button("Lay it out", ImVec2(w, 0))) readyToPaint(o->id);
return; return;
@ -2683,8 +2696,8 @@ private:
} }
} }
void printProperties(float w) { void printProperties(float w) {
auto good = [](const char* t) { ImGui::TextColored(ImVec4(0.45f, 0.85f, 0.4f, 1), "%s", t); }; auto good = [](const char* t) { ImGui::TextColored(roleColor(keel::ui::Role::SuccessText), "%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 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"); ImGui::SeparatorText("Printer");
int printer = std::clamp(fry_printer.i(), 0, int(std::size(kPrinters)) - 1); int printer = std::clamp(fry_printer.i(), 0, int(std::size(kPrinters)) - 1);
ImGui::SetNextItemWidth(w); ImGui::SetNextItemWidth(w);
@ -2965,7 +2978,7 @@ private:
} }
if (ImGui::IsItemDeactivated()) renamingFolder_ = 0; if (ImGui::IsItemDeactivated()) renamingFolder_ = 0;
} else { } else {
ImGui::PushStyleColor(ImGuiCol_Text, kAccentText); ImGui::PushStyleColor(ImGuiCol_Text, accentText());
const std::string label = std::format("{} ({})", f->name, inside.size()); 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 (ImGui::Selectable(label.c_str(), picked, ImGuiSelectableFlags_AllowDoubleClick)) { // a folder is a group: all of it picked
if (mode_ == Mode::Edit) leaveEdit(); if (mode_ == Mode::Edit) leaveEdit();
@ -3179,9 +3192,9 @@ private:
} }
ImGui::Checkbox("Joined into one object", &c.joined); ImGui::Checkbox("Joined into one object", &c.joined);
ImGui::Spacing(); ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Button, kAccent); pushAccent();
if (ImGui::Button("Make them", ImVec2(w, 0))) makeCopies(), showCopies_ = false; if (ImGui::Button("Make them", ImVec2(w, 0))) makeCopies(), showCopies_ = false;
ImGui::PopStyleColor(); popAccent();
ImGui::End(); ImGui::End();
} }
void thickenActive() { void thickenActive() {
@ -3206,9 +3219,9 @@ private:
ImGui::SetNextItemWidth(w); ImGui::SetNextItemWidth(w);
if (ImGui::Combo("##dir", &out, "Inwards (a closed shape: hollow)\0Outwards\0")) thicknessOut_ = out == 1; 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::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; if (ImGui::Button("Make it thick", ImVec2(w, 0))) thickenActive(), showThicken_ = false;
ImGui::PopStyleColor(); popAccent();
ImGui::End(); ImGui::End();
} }
void cleanFlatFaces() { void cleanFlatFaces() {
@ -3293,7 +3306,7 @@ private:
ImGui::SetNextWindowBgAlpha(0.8f); ImGui::SetNextWindowBgAlpha(0.8f);
ImGui::Begin("##touchbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGui::Begin("##touchbar", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking); 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)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12, 8));
const ImGuiStyle& st = ImGui::GetStyle(); const ImGuiStyle& st = ImGui::GetStyle();
float x = 0; // (where the row's got to) 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; if (x > 0 && x + st.ItemSpacing.x + w <= maxW) ImGui::SameLine(), x += st.ItemSpacing.x;
else x = 0; else x = 0;
x += w; x += w;
if (on) ImGui::PushStyleColor(ImGuiCol_Button, kAccent); if (on) pushAccent();
const bool pressed = ImGui::Button(label); const bool pressed = ImGui::Button(label);
if (on) ImGui::PopStyleColor(); if (on) popAccent();
if (tip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip); if (tip && ImGui::IsItemHovered()) ImGui::SetTooltip("%s", tip);
return pressed; return pressed;
}; };
@ -4872,38 +4885,10 @@ private:
ImGui::GetIO().IniFilename = iniPath_.c_str(); ImGui::GetIO().IniFilename = iniPath_.c_str();
if (stdfs::exists(iniPath_)) ImGui::LoadIniSettingsFromDisk(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; if (styled_) return;
styled_ = true; styled_ = true;
ImGuiStyle& st = ImGui::GetStyle(); ImGui::GetStyle().FontScaleDpi = 0.92f; // (the look itself: keel/ui, Niveus)
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);
} }
void defaultLayout(ImGuiID dock) { void defaultLayout(ImGuiID dock) {
@ -5046,6 +5031,12 @@ private:
ImGui::MenuItem("X-ray (see and pick through)", "Alt+Z", &xray_); ImGui::MenuItem("X-ray (see and pick through)", "Alt+Z", &xray_);
ImGui::MenuItem("Grid", nullptr, &showGrid_); ImGui::MenuItem("Grid", nullptr, &showGrid_);
ImGui::MenuItem("Ground (in Rendered)", nullptr, &scene_.look.ground); 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(); ImGui::Separator();
if (ImGui::MenuItem("Perspective", "Numpad 5", view_ == View::Perspective)) leaveFlatView(); if (ImGui::MenuItem("Perspective", "Numpad 5", view_ == View::Perspective)) leaveFlatView();
if (ImGui::MenuItem("Front", "Numpad 1", view_ == View::Front)) setView(View::Front); if (ImGui::MenuItem("Front", "Numpad 1", view_ == View::Front)) setView(View::Front);
@ -5072,7 +5063,7 @@ private:
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1);
logoImage(h); 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(); if (!touch()) menus();
else if (ImGui::BeginMenu("Menu")) { // (a phone: one menu, the others inside it) else if (ImGui::BeginMenu("Menu")) { // (a phone: one menu, the others inside it)
menus(); menus();
@ -5115,14 +5106,21 @@ private:
if (ImGui::MenuItem("Reference picture...")) dialog(4); 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) { bool iconButton(const char* id, Icon icon, bool on, const char* tip, float size = 38.0f) {
const ImVec2 p = ImGui::GetCursorScreenPos(); const ImVec2 p = ImGui::GetCursorScreenPos();
const bool hit = ImGui::InvisibleButton(id, ImVec2(size, size)); 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(); 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); keel::ui::drawKey(dl, p, ImVec2(p.x + size, p.y + size), 7.0f, k);
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)); 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); if (hov && tip) ImGui::SetTooltip("%s", tip);
return hit; 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) { 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 ImVec2 p = ImGui::GetCursorScreenPos();
const bool hit = ImGui::InvisibleButton(id, ImVec2(width, h)); 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(); 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); keel::ui::drawKey(dl, p, ImVec2(p.x + width, p.y + h), 7.0f, k);
drawIcon(dl, icon, ImVec2(p.x + h * 0.15f, p.y + h * 0.13f), h * 0.74f, IM_COL32(235, 235, 240, 255)); const float sink = k == keel::ui::KeyState::Pressed ? 1.0f : 0.0f;
dl->AddText(ImVec2(p.x + h + 2, p.y + (h - ImGui::GetFontSize()) * 0.5f), IM_COL32(235, 235, 240, 255), label); 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); if (hov && tip) ImGui::SetTooltip("%s", tip);
return hit; return hit;
} }
@ -5209,7 +5209,7 @@ private:
} }
ImGui::Spacing(); ImGui::Spacing();
for (const ToolInfo& t : kTools) 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(); ImGui::End();
} }
@ -5230,7 +5230,7 @@ private:
ImGui::PopID(); ImGui::PopID();
}); });
for (const PaintToolInfo& t : kPaintTools) 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) { void paintOptions(float w) {
@ -5565,7 +5565,7 @@ private:
"or put pictures onto: copy one in any app and press Ctrl+V."); "or put pictures onto: copy one in any app and press Ctrl+V.");
return; return;
} }
ImGui::TextColored(kAccentText, "%s", o->name.c_str()); ImGui::TextColored(accentText(), "%s", o->name.c_str());
if (!o->picture) { if (!o->picture) {
int size = pictureSize_ <= 512 ? 0 : pictureSize_ <= 1024 ? 1 : pictureSize_ <= 2048 ? 2 : 3; int size = pictureSize_ <= 512 ? 0 : pictureSize_ <= 1024 ? 1 : pictureSize_ <= 2048 ? 2 : 3;
ImGui::SetNextItemWidth(w); ImGui::SetNextItemWidth(w);
@ -5992,7 +5992,7 @@ private:
void statusBar() { void statusBar() {
ImGui::Begin("Status", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar); 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) { else if (tab_ == Tab::Layout) {
for (const LayToolInfo& t : kLayTools) for (const LayToolInfo& t : kLayTools)
if (t.tool == layTool_) if (t.tool == layTool_)
@ -6019,7 +6019,7 @@ private:
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking); ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDocking);
static const char* names[] = {"Perspective", "Front", "Back", "Left", "Right", "Top", "Bottom"}; static const char* names[] = {"Perspective", "Front", "Back", "Left", "Right", "Top", "Bottom"};
static const char* shadings[] = {"Solid", "Rendered", "Wireframe"}; static const char* shadings[] = {"Solid", "Rendered", "Wireframe"};
ImGui::TextColored(kAccentText, "%s", names[int(view_)]); ImGui::TextColored(accentText(), "%s", names[int(view_)]);
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextDisabled("%s%s", shadings[int(shading_)], xray_ ? ", X-ray" : ""); ImGui::TextDisabled("%s%s", shadings[int(shading_)], xray_ ? ", X-ray" : "");
for (int i = 1; i < 7; ++i) { for (int i = 1; i < 7; ++i) {
@ -6058,7 +6058,7 @@ private:
for (auto [k, what] : list) { for (auto [k, what] : list) {
ImGui::TableNextRow(); ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::TextColored(kAccentText, "%s", k); ImGui::TextColored(accentText(), "%s", k);
ImGui::TableNextColumn(); ImGui::TableNextColumn();
ImGui::TextWrapped("%s", what); ImGui::TextWrapped("%s", what);
} }
@ -6097,7 +6097,7 @@ private:
logoImage(160); logoImage(160);
const char* title = "Fry3DX — Frying 3D Extreme"; const char* title = "Fry3DX — Frying 3D Extreme";
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize(title).x) * 0.5f); 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::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(); ImGui::End();
} }