From f272fc9391ecd69853854dfe06e13eaf241f7a06 Mon Sep 17 00:00:00 2001 From: JohnGreenCCP Date: Wed, 16 Sep 2026 14:54:50 +0100 Subject: [PATCH 1/5] Added UI Scaling support Added UI Scaling from 1x-3x Tested scaling on custom UI Saving scaling settings into the IMGui Config --- src/viewer/appState.cpp | 1 + src/viewer/appState.h | 3 + src/viewer/application.cpp | 10 ++ .../rendering/ui/uiAnimationPlayback.cpp | 9 +- src/viewer/rendering/ui/uiConsts.cpp | 26 +++ src/viewer/rendering/ui/uiConsts.h | 51 +++++- src/viewer/rendering/ui/uiCustomWidgets.cpp | 12 +- src/viewer/rendering/ui/uiCustomWidgets.h | 4 +- src/viewer/rendering/ui/uiDetailWindow.cpp | 7 +- src/viewer/rendering/ui/uiGeneralWindow.cpp | 6 +- src/viewer/rendering/ui/uiMenubar.cpp | 28 +++ src/viewer/rendering/ui/uiMenubar.h | 6 + src/viewer/rendering/ui/uiRenderer.cpp | 61 +++++-- src/viewer/rendering/ui/uiRenderer.h | 1 + src/viewer/rendering/ui/uiSettings.cpp | 167 ++++++++++++++++++ src/viewer/rendering/ui/uiSettings.h | 14 ++ 16 files changed, 366 insertions(+), 40 deletions(-) create mode 100644 src/viewer/rendering/ui/uiConsts.cpp create mode 100644 src/viewer/rendering/ui/uiSettings.cpp create mode 100644 src/viewer/rendering/ui/uiSettings.h diff --git a/src/viewer/appState.cpp b/src/viewer/appState.cpp index aed1d98..96354ed 100644 --- a/src/viewer/appState.cpp +++ b/src/viewer/appState.cpp @@ -24,6 +24,7 @@ void AppState::CallStateCallbacks() cmfLoadRequest.CallCallbacks( *this ); exitRequested.CallCallbacks( *this ); showUi.CallCallbacks( *this ); + uiScale.CallCallbacks( *this ); modelState.CallCallbacks( *this ); } diff --git a/src/viewer/appState.h b/src/viewer/appState.h index bb748a2..f85c23c 100644 --- a/src/viewer/appState.h +++ b/src/viewer/appState.h @@ -197,6 +197,9 @@ struct AppState State exitRequested{ false }; State showUi{ true }; + /// ui scale factor (see UiConsts::MIN_UI_SCALE / MAX_UI_SCALE) + State uiScale{ 1.0f }; + float defaultUiScale{ 1.0f }; // model ModelState modelState{}; diff --git a/src/viewer/application.cpp b/src/viewer/application.cpp index 8db37e9..bac804f 100644 --- a/src/viewer/application.cpp +++ b/src/viewer/application.cpp @@ -3,6 +3,8 @@ #include "application.h" #include +#include "rendering/ui/uiConsts.h" +#include "rendering/ui/uiSettings.h" #include "rendering/vulkan/shadercache.h" #include "rendering/vulkan/vulkanerrors.h" #include "viewerIcon.h" @@ -41,6 +43,13 @@ void Application::Initialize() } SetCarbonMeshViewerWindowIcon( m_window ); + float contentScaleX = 1.0f; + float contentScaleY = 1.0f; + glfwGetWindowContentScale( m_window, &contentScaleX, &contentScaleY ); + m_appState.defaultUiScale = std::clamp( contentScaleX, UiConsts::MIN_UI_SCALE, UiConsts::MAX_UI_SCALE ); + m_appState.uiScale.SetValueNoCallback( m_appState.defaultUiScale ); + Log::Info( "Monitor content scale is %.2f, initial ui scale %.2fx", contentScaleX, m_appState.defaultUiScale ); + // we may ask for a given size (see above) but it is not guaranteed to be the actual framebuffer size int actualHeight, actualWidth; glfwGetFramebufferSize( m_window, &actualWidth, &actualHeight ); @@ -222,6 +231,7 @@ void Application::Run() } auto logicalDevice = m_renderer->GetDevice()->GetLogicalDevice(); vkDeviceWaitIdle( logicalDevice ); + UiSettings::Save(); m_orientationGizmoRenderer.release(); m_sceneRenderer.release(); diff --git a/src/viewer/rendering/ui/uiAnimationPlayback.cpp b/src/viewer/rendering/ui/uiAnimationPlayback.cpp index 04d4ff2..aa5e0c5 100644 --- a/src/viewer/rendering/ui/uiAnimationPlayback.cpp +++ b/src/viewer/rendering/ui/uiAnimationPlayback.cpp @@ -5,7 +5,7 @@ #include "uiCustomWidgets.h" -const float ANIMATION_SELECTION_WIDTH = 120.0f; +const float BASE_ANIMATION_SELECTION_WIDTH = 120.0f; void UIAnimationPlayback::Render( AppState& appState ) { @@ -13,8 +13,9 @@ void UIAnimationPlayback::Render( AppState& appState ) const auto [width, height] = appState.windowSize.GetValue(); - ImGui::SetNextWindowPos( ImVec2( 0, static_cast( height ) - UiConsts::ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); - ImGui::SetNextWindowSize( ImVec2( static_cast( width ), UiConsts::ANIMATION_PLAYER_HEIGHT ), ImGuiCond_Always ); + const float playerHeight = UiConsts::AnimationPlayerHeight(); + ImGui::SetNextWindowPos( ImVec2( 0, static_cast( height ) - playerHeight ), ImGuiCond_Always ); + ImGui::SetNextWindowSize( ImVec2( static_cast( width ), playerHeight ), ImGuiCond_Always ); if( ImGui::Begin( "##animationTitle", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize ) ) { // animation selection @@ -62,7 +63,7 @@ void UIAnimationPlayback::Render( AppState& appState ) ImGui::BeginDisabled( animationNames.empty() ); - ImGui::PushItemWidth( ANIMATION_SELECTION_WIDTH ); + ImGui::PushItemWidth( UiConsts::Scaled( BASE_ANIMATION_SELECTION_WIDTH ) ); ImGui::ComboBox( "##animation", animationNames, selectedAnimationIndex, [&]( int32_t selectedIndex ) { m_playing = false; m_currentTime = 0.0f; diff --git a/src/viewer/rendering/ui/uiConsts.cpp b/src/viewer/rendering/ui/uiConsts.cpp new file mode 100644 index 0000000..ef02ad6 --- /dev/null +++ b/src/viewer/rendering/ui/uiConsts.cpp @@ -0,0 +1,26 @@ +// Copyright © 2026 CCP ehf. + +#include "uiConsts.h" + +namespace UiConsts +{ +namespace +{ +float g_uiScale = 1.0f; +} + +float GetUiScale() +{ + return g_uiScale; +} + +void SetUiScale( float scale ) +{ + g_uiScale = scale; +} + +float Scaled( float baseSize ) +{ + return baseSize * g_uiScale; +} +} diff --git a/src/viewer/rendering/ui/uiConsts.h b/src/viewer/rendering/ui/uiConsts.h index c6395b1..d1fe0ee 100644 --- a/src/viewer/rendering/ui/uiConsts.h +++ b/src/viewer/rendering/ui/uiConsts.h @@ -5,10 +5,47 @@ namespace UiConsts { -const float MENU_BAR_HEIGHT = 18.0f; -const float ANIMATION_PLAYER_HEIGHT = 36.0f; -const float BUTTON_WIDTH = 19.0f; -const float BUTTON_HEIGHT = 19.0f; -const ImVec2 BUTTON_SIZE = ImVec2( BUTTON_WIDTH, BUTTON_HEIGHT ); -const float FONT_AWESOME_SIZE = 13.0f; -} \ No newline at end of file +const float MIN_UI_SCALE = 1.0f; +const float MAX_UI_SCALE = 3.0f; + +// sizes at 1x, use the scaled accessors below +const float BASE_FONT_SIZE = 13.0f; +const float BASE_FONT_AWESOME_SIZE = 13.0f; +const float BASE_MENU_BAR_HEIGHT = 18.0f; +const float BASE_ANIMATION_PLAYER_HEIGHT = 36.0f; +const float BASE_BUTTON_WIDTH = 19.0f; +const float BASE_BUTTON_HEIGHT = 19.0f; + +float GetUiScale(); +void SetUiScale( float scale ); +float Scaled( float baseSize ); + +inline float FontSize() +{ + return Scaled( BASE_FONT_SIZE ); +} +inline float FontAwesomeSize() +{ + return Scaled( BASE_FONT_AWESOME_SIZE ); +} +inline float MenuBarHeight() +{ + return Scaled( BASE_MENU_BAR_HEIGHT ); +} +inline float AnimationPlayerHeight() +{ + return Scaled( BASE_ANIMATION_PLAYER_HEIGHT ); +} +inline float ButtonWidth() +{ + return Scaled( BASE_BUTTON_WIDTH ); +} +inline float ButtonHeight() +{ + return Scaled( BASE_BUTTON_HEIGHT ); +} +inline ImVec2 ButtonSize() +{ + return ImVec2( ButtonWidth(), ButtonHeight() ); +} +} diff --git a/src/viewer/rendering/ui/uiCustomWidgets.cpp b/src/viewer/rendering/ui/uiCustomWidgets.cpp index 72a5489..7e2cdb6 100644 --- a/src/viewer/rendering/ui/uiCustomWidgets.cpp +++ b/src/viewer/rendering/ui/uiCustomWidgets.cpp @@ -59,8 +59,8 @@ CheckBoxTriStateValue GetCheckedStatus( int64_t checked, int64_t count ) bool FontAwesomeButton( const FaIcon& icon, int id, float width, float height ) { - const float faButtonPadding = 4.0f; - const float glyphWidth = UiConsts::FONT_AWESOME_SIZE * icon.xyRatio; + const float faButtonPadding = UiConsts::Scaled( 4.0f ); + const float glyphWidth = UiConsts::FontAwesomeSize() * icon.xyRatio; const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); ImGui::PushID( id ); @@ -73,8 +73,8 @@ bool FontAwesomeButton( const FaIcon& icon, int id, float width, float height ) void FontAwesomeText( const FaIcon& icon, float width ) { - const float faButtonPadding = -4.0f; - const float glyphWidth = UiConsts::FONT_AWESOME_SIZE * icon.xyRatio; + const float faButtonPadding = UiConsts::Scaled( -4.0f ); + const float glyphWidth = UiConsts::FontAwesomeSize() * icon.xyRatio; const float paddingX = std::max( 0.0f, ( width - glyphWidth ) * 0.5f ); ImGui::PushStyleVar( ImGuiStyleVar_FramePadding, ImVec2( paddingX, faButtonPadding ) ); ImGui::TextUnformatted( icon.text ); @@ -86,12 +86,12 @@ bool FontAwesomeSlashedButton( const FaIcon& icon, int id, float width, float he bool ret = ImGui::FontAwesomeButton( icon, id, width, height ); ImVec2 min = ImGui::GetItemRectMin(); ImVec2 max = ImGui::GetItemRectMax(); - float slashPadding = 1.5f; + const float slashPadding = UiConsts::Scaled( 1.5f ); ImGui::GetWindowDrawList()->AddLine( ImVec2( min.x + slashPadding, max.y - slashPadding ), ImVec2( max.x - slashPadding, min.y + slashPadding ), ImGui::GetColorU32( ImGuiCol_Text ), - 2.0f ); + UiConsts::Scaled( 2.0f ) ); return ret; } diff --git a/src/viewer/rendering/ui/uiCustomWidgets.h b/src/viewer/rendering/ui/uiCustomWidgets.h index 79b8202..5d1569d 100644 --- a/src/viewer/rendering/ui/uiCustomWidgets.h +++ b/src/viewer/rendering/ui/uiCustomWidgets.h @@ -19,8 +19,8 @@ enum class CheckBoxTriStateValue CheckBoxTriStateValue GetCheckedStatus( int64_t checked, int64_t count ); bool CheckBoxTristate( const char* label, CheckBoxTriStateValue* v_tristate ); -bool FontAwesomeButton( const FaIcon& icon, int id = 0, float width = UiConsts::BUTTON_WIDTH, float height = UiConsts::BUTTON_HEIGHT ); -bool FontAwesomeSlashedButton( const FaIcon& icon, int id = 0, float width = UiConsts::BUTTON_WIDTH, float height = UiConsts::BUTTON_HEIGHT ); +bool FontAwesomeButton( const FaIcon& icon, int id = 0, float width = UiConsts::ButtonWidth(), float height = UiConsts::ButtonHeight() ); +bool FontAwesomeSlashedButton( const FaIcon& icon, int id = 0, float width = UiConsts::ButtonWidth(), float height = UiConsts::ButtonHeight() ); void FontAwesomeText( const FaIcon& icon, float width ); const char* OpenCmfFileDialog(); diff --git a/src/viewer/rendering/ui/uiDetailWindow.cpp b/src/viewer/rendering/ui/uiDetailWindow.cpp index b2a0dc7..f00d5fd 100644 --- a/src/viewer/rendering/ui/uiDetailWindow.cpp +++ b/src/viewer/rendering/ui/uiDetailWindow.cpp @@ -1,6 +1,7 @@ // Copyright © 2026 CCP ehf. #include "uiDetailWindow.h" +#include "uiConsts.h" #include "cmf/bufferstreams.h" #include "cmf/declutils.h" #include @@ -642,7 +643,7 @@ void UIDetailWindow::RenderAttributeTable( const uint8_t* vbData, uint32_t verte p, ImVec2( p.x + sz, p.y + sz ), ImColor( color.x, color.y, color.z ) ); - ImGui::Dummy( ImVec2( sz + 4.0f, sz ) ); + ImGui::Dummy( ImVec2( sz + UiConsts::Scaled( 4.0f ), sz ) ); ImGui::SameLine(); } @@ -1217,7 +1218,7 @@ void UIDetailWindow::RenderAudioOccluder( const cmf::AudioOcclusionMesh& audioOc ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit; - const float tableHeight = std::min( (float)vertexCount * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing(), 200.0f ); + const float tableHeight = std::min( (float)vertexCount * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing(), UiConsts::Scaled( 200.0f ) ); if( ImGui::BeginTable( "##aomverts", 4, tableFlags, ImVec2( 0.0f, tableHeight ) ) ) { ImGui::TableSetupScrollFreeze( 1, 1 ); @@ -1261,7 +1262,7 @@ void UIDetailWindow::RenderAudioOccluder( const cmf::AudioOcclusionMesh& audioOc ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingFixedFit; - const float tableHeight = std::min( (float)triCount * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing(), 200.0f ); + const float tableHeight = std::min( (float)triCount * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing(), UiConsts::Scaled( 200.0f ) ); if( ImGui::BeginTable( "##aomtris", 4, tableFlags, ImVec2( 0.0f, tableHeight ) ) ) { ImGui::TableSetupScrollFreeze( 1, 1 ); diff --git a/src/viewer/rendering/ui/uiGeneralWindow.cpp b/src/viewer/rendering/ui/uiGeneralWindow.cpp index 49cd8b7..791cd88 100644 --- a/src/viewer/rendering/ui/uiGeneralWindow.cpp +++ b/src/viewer/rendering/ui/uiGeneralWindow.cpp @@ -547,7 +547,7 @@ void UIGeneralWindow::RenderMorphList( const cmf::Mesh& mesh, MeshState& meshSta { ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed ); ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthStretch ); - ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed, UiConsts::BUTTON_WIDTH ); + ImGui::TableSetupColumn( "", ImGuiTableColumnFlags_WidthFixed, UiConsts::ButtonWidth() ); ImGui::TableNextRow(); ImGui::TableNextColumn(); @@ -626,7 +626,7 @@ void UIGeneralWindow::RenderAnimationOverrideList( AppState& appState ) ImGui::BeginDisabled( disabled ); // button to add an animation owner from a cmf file - if( ImGui::Button( "+", ImVec2( ImGui::GetContentRegionAvail().x, UiConsts::BUTTON_HEIGHT ) ) ) + if( ImGui::Button( "+", ImVec2( ImGui::GetContentRegionAvail().x, UiConsts::ButtonHeight() ) ) ) { auto* path = ImGui::OpenCmfFileDialog(); if( path != nullptr ) @@ -696,7 +696,7 @@ void UIGeneralWindow::RenderAnimationOverrideList( AppState& appState ) if( index != 0 ) { - ImGui::SameLine( ImGui::GetContentRegionAvail().x - UiConsts::BUTTON_WIDTH ); + ImGui::SameLine( ImGui::GetContentRegionAvail().x - UiConsts::ButtonWidth() ); std::string label = "-##" + std::to_string( index ); if( ImGui::Button( label.c_str() ) ) { diff --git a/src/viewer/rendering/ui/uiMenubar.cpp b/src/viewer/rendering/ui/uiMenubar.cpp index 415aacf..3cc0616 100644 --- a/src/viewer/rendering/ui/uiMenubar.cpp +++ b/src/viewer/rendering/ui/uiMenubar.cpp @@ -3,6 +3,7 @@ #include "uiMenubar.h" #include +#include "uiConsts.h" #include "uiCustomWidgets.h" void UiMenubar::Render( AppState& appState ) @@ -112,6 +113,8 @@ void UiMenubar::Render( AppState& appState ) } ImGui::Separator(); + RenderUiScaleMenu( appState ); + bool uiShown = appState.showUi.GetValue(); const char* toggleUiLabel = uiShown ? "Hide UI" : "Show UI"; if( ImGui::MenuItem( toggleUiLabel, "Ctrl+F12" ) ) @@ -124,3 +127,28 @@ void UiMenubar::Render( AppState& appState ) ImGui::EndMainMenuBar(); } } + +void UiMenubar::RenderUiScaleMenu( AppState& appState ) +{ + if( ImGui::BeginMenu( "UI Scale" ) ) + { + if( !m_uiScaleSliderActive ) + { + m_uiScaleSliderValue = appState.uiScale.GetValue(); + } + ImGui::SetNextItemWidth( UiConsts::Scaled( 160.0f ) ); + ImGui::SliderFloat( "##uiScale", &m_uiScaleSliderValue, UiConsts::MIN_UI_SCALE, UiConsts::MAX_UI_SCALE, "%.2fx", ImGuiSliderFlags_AlwaysClamp ); + m_uiScaleSliderActive = ImGui::IsItemActive(); + // rebuilding the fonts is expensive, only apply once the slider is released + if( ImGui::IsItemDeactivatedAfterEdit() ) + { + appState.uiScale.SetValue( m_uiScaleSliderValue ); + } + ImGui::SameLine(); + if( ImGui::SmallButton( "Reset" ) ) + { + appState.uiScale.SetValue( appState.defaultUiScale ); + } + ImGui::EndMenu(); + } +} diff --git a/src/viewer/rendering/ui/uiMenubar.h b/src/viewer/rendering/ui/uiMenubar.h index 458c782..d99cc0f 100644 --- a/src/viewer/rendering/ui/uiMenubar.h +++ b/src/viewer/rendering/ui/uiMenubar.h @@ -8,4 +8,10 @@ class UiMenubar { public: void Render( AppState& appState ); + +private: + void RenderUiScaleMenu( AppState& appState ); + + float m_uiScaleSliderValue{ 1.0f }; + bool m_uiScaleSliderActive{ false }; }; \ No newline at end of file diff --git a/src/viewer/rendering/ui/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp index ce9c47a..1864073 100644 --- a/src/viewer/rendering/ui/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -10,6 +10,7 @@ #include "appState.h" #include "uiConsts.h" #include "uiCustomWidgets.h" +#include "uiSettings.h" #include "../vulkan/vulkanerrors.h" @@ -50,17 +51,9 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) ImGui::CreateContext(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls - io.Fonts->AddFontDefault(); - - static const ImWchar iconRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; - - ImFontConfig config; - config.MergeMode = true; - config.FontDataOwnedByAtlas = false; - // add fontawesome icons - io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast( fa_solid_900_ttf_size ), UiConsts::FONT_AWESOME_SIZE, &config, iconRanges ); - // Setup Dear ImGui style - ImGui::StyleColorsDark(); + // settings must be loaded before the fonts are built + UiSettings::Initialize( state ); + ApplyUiScale( state.uiScale.GetValue() ); Device* device = m_renderer->GetDevice(); const Swapchain* swapchain = m_renderer->GetSwapchain(); @@ -128,11 +121,48 @@ void UIRenderer::Initialize( GLFWwindow* window, AppState& state ) } } ); + state.uiScale.RegisterCallback( [this]( float scale, AppState& appState ) { + ApplyUiScale( scale ); + ImGui_ImplVulkan_CreateFontsTexture(); + UiSettings::MarkDirty(); + } ); + auto [width, height] = state.windowSize.GetValue(); m_graphicsCommandBuffer.SetRenderSize( width, height ); } +void UIRenderer::ApplyUiScale( float scale ) +{ + scale = std::clamp( scale, UiConsts::MIN_UI_SCALE, UiConsts::MAX_UI_SCALE ); + Log::Info( "Applying ui scale %.2fx", scale ); + UiConsts::SetUiScale( scale ); + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); + + ImFontConfig defaultConfig; + defaultConfig.SizePixels = UiConsts::FontSize(); + defaultConfig.OversampleH = 1; + defaultConfig.OversampleV = 1; + defaultConfig.PixelSnapH = true; + io.Fonts->AddFontDefault( &defaultConfig ); + + // add fontawesome icons + static const ImWchar iconRanges[] = { ICON_MIN_FA, ICON_MAX_FA, 0 }; + ImFontConfig iconConfig; + iconConfig.MergeMode = true; + iconConfig.FontDataOwnedByAtlas = false; + io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast( fa_solid_900_ttf_size ), UiConsts::FontAwesomeSize(), &iconConfig, iconRanges ); + io.Fonts->Build(); + + // ScaleAllSizes is cumulative, so always start from a fresh style + ImGuiStyle& style = ImGui::GetStyle(); + style = ImGuiStyle(); + ImGui::StyleColorsDark( &style ); + style.ScaleAllSizes( scale ); +} + void UIRenderer::BeginFrame() { // Start the Dear ImGui frame @@ -156,8 +186,8 @@ void UIRenderer::SetupUi( AppState& appState ) m_menubar.Render( appState ); if( appState.showUi.GetValue() ) { - m_generalWindow.Render( appState, UiConsts::MENU_BAR_HEIGHT, UiConsts::ANIMATION_PLAYER_HEIGHT ); - m_detailWindow.Render( appState, UiConsts::MENU_BAR_HEIGHT, UiConsts::ANIMATION_PLAYER_HEIGHT ); + m_generalWindow.Render( appState, UiConsts::MenuBarHeight(), UiConsts::AnimationPlayerHeight() ); + m_detailWindow.Render( appState, UiConsts::MenuBarHeight(), UiConsts::AnimationPlayerHeight() ); m_animationPlayback.Render( appState ); } SetupPopupWindows( appState ); @@ -182,9 +212,10 @@ void UIRenderer::SetupPopupWindows( AppState& appState ) ImGui::Separator(); ImGui::NewLine(); ImVec2 availableSize = ImGui::GetContentRegionAvail(); - ImGui::SameLine( availableSize.x / 2.0f - 50.0f ); + const ImVec2 buttonSize( UiConsts::Scaled( 100.0f ), UiConsts::Scaled( 24.0f ) ); + ImGui::SameLine( ( availableSize.x - buttonSize.x ) / 2.0f ); - if( ImGui::Button( "Ok", ImVec2( 100.0, 24 ) ) ) + if( ImGui::Button( "Ok", buttonSize ) ) { m_loadStatus = LoadStatus::NOTHING_LOADED; } diff --git a/src/viewer/rendering/ui/uiRenderer.h b/src/viewer/rendering/ui/uiRenderer.h index 6aee38b..ee8efd5 100644 --- a/src/viewer/rendering/ui/uiRenderer.h +++ b/src/viewer/rendering/ui/uiRenderer.h @@ -27,6 +27,7 @@ class UIRenderer private: void SetupPopupWindows( AppState& appState ); void UpdateInputs( AppState& state ); + void ApplyUiScale( float scale ); enum LoadStatus { diff --git a/src/viewer/rendering/ui/uiSettings.cpp b/src/viewer/rendering/ui/uiSettings.cpp new file mode 100644 index 0000000..6ea5c70 --- /dev/null +++ b/src/viewer/rendering/ui/uiSettings.cpp @@ -0,0 +1,167 @@ +// Copyright © 2026 CCP ehf. + +#include "uiSettings.h" + +#include +#include +#include +#include +#include +#include +#include // ImGuiSettingsHandler +#include "log.h" +#include "uiConsts.h" + +#if defined( _WIN32 ) +#include +#define MAKE_DIRECTORY( path ) _mkdir( path ) +#else +#include +#define MAKE_DIRECTORY( path ) mkdir( path, 0755 ) +#endif + +// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) + +namespace +{ +const char* const SETTINGS_TYPE_NAME = "CarbonMeshViewer"; // ini section is [CarbonMeshViewer][Settings] +const char* const SETTINGS_ENTRY_NAME = "Settings"; +const char* const INI_FILE_NAME = "imgui.ini"; + +// io.IniFilename is a raw pointer, so the path has to outlive the ImGui context +std::string g_iniFilePath; + +void CreateDirectories( const std::string& path ) +{ + for( size_t pos = path.find_first_of( "/\\", 1 ); pos != std::string::npos; pos = path.find_first_of( "/\\", pos + 1 ) ) + { + MAKE_DIRECTORY( path.substr( 0, pos ).c_str() ); + } + MAKE_DIRECTORY( path.c_str() ); +} + +std::string GetEnvironmentValue( const char* name ) +{ +#if defined( _MSC_VER ) + char* value = nullptr; + size_t length = 0; + if( _dupenv_s( &value, &length, name ) != 0 || value == nullptr ) + { + return ""; + } + std::string result( value ); + std::free( value ); + return result; +#else + const char* value = std::getenv( name ); + return value != nullptr ? std::string( value ) : ""; +#endif +} + +// empty (= working directory) if the environment does not tell us where the user directory is +std::string GetSettingsDirectory() +{ +#if defined( _WIN32 ) + const std::string base = GetEnvironmentValue( "LOCALAPPDATA" ); + const char* subDirectory = "\\FenrisCreations\\CarbonMeshViewer"; +#elif defined( __APPLE__ ) + const std::string base = GetEnvironmentValue( "HOME" ); + const char* subDirectory = "/Library/Application Support/FenrisCreations/CarbonMeshViewer"; +#else + const std::string base = GetEnvironmentValue( "HOME" ); + const char* subDirectory = "/.config/FenrisCreations/CarbonMeshViewer"; +#endif + if( base.empty() ) + { + return ""; + } + return base + subDirectory; +} + +AppState& GetAppState( ImGuiSettingsHandler* handler ) +{ + return *static_cast( handler->UserData ); +} + +void* ReadOpen( ImGuiContext*, ImGuiSettingsHandler* handler, const char* name ) +{ + return std::strcmp( name, SETTINGS_ENTRY_NAME ) == 0 ? handler->UserData : nullptr; +} + +void ReadLine( ImGuiContext*, ImGuiSettingsHandler* handler, void*, const char* line ) +{ + const char* const key = "UiScale="; + const size_t keyLength = std::strlen( key ); + if( std::strncmp( line, key, keyLength ) == 0 ) + { + char* end = nullptr; + const float uiScale = std::strtof( line + keyLength, &end ); + if( end != line + keyLength ) + { + GetAppState( handler ).uiScale.SetValueNoCallback( std::clamp( uiScale, UiConsts::MIN_UI_SCALE, UiConsts::MAX_UI_SCALE ) ); + } + } +} + +void WriteAll( ImGuiContext*, ImGuiSettingsHandler* handler, ImGuiTextBuffer* outBuffer ) +{ + outBuffer->appendf( "[%s][%s]\n", handler->TypeName, SETTINGS_ENTRY_NAME ); + outBuffer->appendf( "UiScale=%.3f\n", GetAppState( handler ).uiScale.GetValue() ); + outBuffer->append( "\n" ); +} +} + +namespace UiSettings +{ +void Initialize( AppState& appState ) +{ + const std::string directory = GetSettingsDirectory(); + if( directory.empty() ) + { + g_iniFilePath = INI_FILE_NAME; + } + else + { + CreateDirectories( directory ); +#if defined( _WIN32 ) + g_iniFilePath = directory + "\\" + INI_FILE_NAME; +#else + g_iniFilePath = directory + "/" + INI_FILE_NAME; +#endif + } + ImGui::GetIO().IniFilename = g_iniFilePath.c_str(); + Log::Info( "Using settings file %s", g_iniFilePath.c_str() ); + + ImGuiSettingsHandler handler; + handler.TypeName = SETTINGS_TYPE_NAME; + handler.TypeHash = ImHashStr( SETTINGS_TYPE_NAME ); + handler.ReadOpenFn = ReadOpen; + handler.ReadLineFn = ReadLine; + handler.WriteAllFn = WriteAll; + handler.UserData = &appState; + ImGui::AddSettingsHandler( &handler ); + + // load now rather than lazily on the first frame + ImGui::LoadIniSettingsFromDisk( g_iniFilePath.c_str() ); +} + +void MarkDirty() +{ + ImGui::MarkIniSettingsDirty(); +} + +void Save() +{ + if( !g_iniFilePath.empty() ) + { + ImGui::SaveIniSettingsToDisk( g_iniFilePath.c_str() ); + } +} + +const std::string& GetIniFilePath() +{ + return g_iniFilePath; +} +} + +// NOLINTEND(cppcoreguidelines-pro-type-vararg) diff --git a/src/viewer/rendering/ui/uiSettings.h b/src/viewer/rendering/ui/uiSettings.h new file mode 100644 index 0000000..0bc517a --- /dev/null +++ b/src/viewer/rendering/ui/uiSettings.h @@ -0,0 +1,14 @@ +// Copyright © 2026 CCP ehf. + +#pragma once + +#include "../../appState.h" + +namespace UiSettings +{ +/// call after ImGui::CreateContext() and before the fonts are built +void Initialize( AppState& appState ); +void MarkDirty(); +void Save(); +const std::string& GetIniFilePath(); +} From efefe02bb649825900889fb9be18ca705866db56 Mon Sep 17 00:00:00 2001 From: JohnGreenCCP Date: Thu, 17 Sep 2026 12:08:11 +0100 Subject: [PATCH 2/5] Removed duplicate uiScale --- src/viewer/rendering/ui/uiConsts.cpp | 26 -------------------------- src/viewer/rendering/ui/uiConsts.h | 10 ++++------ src/viewer/rendering/ui/uiRenderer.cpp | 5 ++--- 3 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 src/viewer/rendering/ui/uiConsts.cpp diff --git a/src/viewer/rendering/ui/uiConsts.cpp b/src/viewer/rendering/ui/uiConsts.cpp deleted file mode 100644 index ef02ad6..0000000 --- a/src/viewer/rendering/ui/uiConsts.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright © 2026 CCP ehf. - -#include "uiConsts.h" - -namespace UiConsts -{ -namespace -{ -float g_uiScale = 1.0f; -} - -float GetUiScale() -{ - return g_uiScale; -} - -void SetUiScale( float scale ) -{ - g_uiScale = scale; -} - -float Scaled( float baseSize ) -{ - return baseSize * g_uiScale; -} -} diff --git a/src/viewer/rendering/ui/uiConsts.h b/src/viewer/rendering/ui/uiConsts.h index d1fe0ee..e9db17b 100644 --- a/src/viewer/rendering/ui/uiConsts.h +++ b/src/viewer/rendering/ui/uiConsts.h @@ -16,14 +16,12 @@ const float BASE_ANIMATION_PLAYER_HEIGHT = 36.0f; const float BASE_BUTTON_WIDTH = 19.0f; const float BASE_BUTTON_HEIGHT = 19.0f; -float GetUiScale(); -void SetUiScale( float scale ); -float Scaled( float baseSize ); - -inline float FontSize() +// the applied scale is whatever the font atlas was built with, so it is derived from the current font rather than stored +inline float Scaled( float baseSize ) { - return Scaled( BASE_FONT_SIZE ); + return baseSize * ImGui::GetFontSize() / BASE_FONT_SIZE; } + inline float FontAwesomeSize() { return Scaled( BASE_FONT_AWESOME_SIZE ); diff --git a/src/viewer/rendering/ui/uiRenderer.cpp b/src/viewer/rendering/ui/uiRenderer.cpp index 1864073..c31616a 100644 --- a/src/viewer/rendering/ui/uiRenderer.cpp +++ b/src/viewer/rendering/ui/uiRenderer.cpp @@ -136,13 +136,12 @@ void UIRenderer::ApplyUiScale( float scale ) { scale = std::clamp( scale, UiConsts::MIN_UI_SCALE, UiConsts::MAX_UI_SCALE ); Log::Info( "Applying ui scale %.2fx", scale ); - UiConsts::SetUiScale( scale ); ImGuiIO& io = ImGui::GetIO(); io.Fonts->Clear(); ImFontConfig defaultConfig; - defaultConfig.SizePixels = UiConsts::FontSize(); + defaultConfig.SizePixels = UiConsts::BASE_FONT_SIZE * scale; defaultConfig.OversampleH = 1; defaultConfig.OversampleV = 1; defaultConfig.PixelSnapH = true; @@ -153,7 +152,7 @@ void UIRenderer::ApplyUiScale( float scale ) ImFontConfig iconConfig; iconConfig.MergeMode = true; iconConfig.FontDataOwnedByAtlas = false; - io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast( fa_solid_900_ttf_size ), UiConsts::FontAwesomeSize(), &iconConfig, iconRanges ); + io.Fonts->AddFontFromMemoryTTF( (void*)fa_solid_900_ttf_data, static_cast( fa_solid_900_ttf_size ), UiConsts::BASE_FONT_AWESOME_SIZE * scale, &iconConfig, iconRanges ); io.Fonts->Build(); // ScaleAllSizes is cumulative, so always start from a fresh style From 1f40ee7cce9b6c2251e49f3f39139301ddaca1de Mon Sep 17 00:00:00 2001 From: JohnGreenCCP Date: Thu, 17 Sep 2026 14:03:10 +0100 Subject: [PATCH 3/5] Reduced complexity of saving scale setting --- src/viewer/rendering/ui/uiSettings.cpp | 94 ++------------------------ src/viewer/rendering/ui/uiSettings.h | 1 - 2 files changed, 5 insertions(+), 90 deletions(-) diff --git a/src/viewer/rendering/ui/uiSettings.cpp b/src/viewer/rendering/ui/uiSettings.cpp index 6ea5c70..2fec921 100644 --- a/src/viewer/rendering/ui/uiSettings.cpp +++ b/src/viewer/rendering/ui/uiSettings.cpp @@ -2,81 +2,20 @@ #include "uiSettings.h" -#include +#include #include #include -#include -#include #include #include // ImGuiSettingsHandler #include "log.h" #include "uiConsts.h" -#if defined( _WIN32 ) -#include -#define MAKE_DIRECTORY( path ) _mkdir( path ) -#else -#include -#define MAKE_DIRECTORY( path ) mkdir( path, 0755 ) -#endif - // NOLINTBEGIN(cppcoreguidelines-pro-type-vararg) namespace { const char* const SETTINGS_TYPE_NAME = "CarbonMeshViewer"; // ini section is [CarbonMeshViewer][Settings] const char* const SETTINGS_ENTRY_NAME = "Settings"; -const char* const INI_FILE_NAME = "imgui.ini"; - -// io.IniFilename is a raw pointer, so the path has to outlive the ImGui context -std::string g_iniFilePath; - -void CreateDirectories( const std::string& path ) -{ - for( size_t pos = path.find_first_of( "/\\", 1 ); pos != std::string::npos; pos = path.find_first_of( "/\\", pos + 1 ) ) - { - MAKE_DIRECTORY( path.substr( 0, pos ).c_str() ); - } - MAKE_DIRECTORY( path.c_str() ); -} - -std::string GetEnvironmentValue( const char* name ) -{ -#if defined( _MSC_VER ) - char* value = nullptr; - size_t length = 0; - if( _dupenv_s( &value, &length, name ) != 0 || value == nullptr ) - { - return ""; - } - std::string result( value ); - std::free( value ); - return result; -#else - const char* value = std::getenv( name ); - return value != nullptr ? std::string( value ) : ""; -#endif -} - -// empty (= working directory) if the environment does not tell us where the user directory is -std::string GetSettingsDirectory() -{ -#if defined( _WIN32 ) - const std::string base = GetEnvironmentValue( "LOCALAPPDATA" ); - const char* subDirectory = "\\FenrisCreations\\CarbonMeshViewer"; -#elif defined( __APPLE__ ) - const std::string base = GetEnvironmentValue( "HOME" ); - const char* subDirectory = "/Library/Application Support/FenrisCreations/CarbonMeshViewer"; -#else - const std::string base = GetEnvironmentValue( "HOME" ); - const char* subDirectory = "/.config/FenrisCreations/CarbonMeshViewer"; -#endif - if( base.empty() ) - { - return ""; - } - return base + subDirectory; -} AppState& GetAppState( ImGuiSettingsHandler* handler ) { @@ -115,23 +54,6 @@ namespace UiSettings { void Initialize( AppState& appState ) { - const std::string directory = GetSettingsDirectory(); - if( directory.empty() ) - { - g_iniFilePath = INI_FILE_NAME; - } - else - { - CreateDirectories( directory ); -#if defined( _WIN32 ) - g_iniFilePath = directory + "\\" + INI_FILE_NAME; -#else - g_iniFilePath = directory + "/" + INI_FILE_NAME; -#endif - } - ImGui::GetIO().IniFilename = g_iniFilePath.c_str(); - Log::Info( "Using settings file %s", g_iniFilePath.c_str() ); - ImGuiSettingsHandler handler; handler.TypeName = SETTINGS_TYPE_NAME; handler.TypeHash = ImHashStr( SETTINGS_TYPE_NAME ); @@ -142,7 +64,9 @@ void Initialize( AppState& appState ) ImGui::AddSettingsHandler( &handler ); // load now rather than lazily on the first frame - ImGui::LoadIniSettingsFromDisk( g_iniFilePath.c_str() ); + const char* iniFile = ImGui::GetIO().IniFilename; + Log::Info( "Using settings file %s", iniFile ); + ImGui::LoadIniSettingsFromDisk( iniFile ); } void MarkDirty() @@ -152,15 +76,7 @@ void MarkDirty() void Save() { - if( !g_iniFilePath.empty() ) - { - ImGui::SaveIniSettingsToDisk( g_iniFilePath.c_str() ); - } -} - -const std::string& GetIniFilePath() -{ - return g_iniFilePath; + ImGui::SaveIniSettingsToDisk( ImGui::GetIO().IniFilename ); } } diff --git a/src/viewer/rendering/ui/uiSettings.h b/src/viewer/rendering/ui/uiSettings.h index 0bc517a..11a415f 100644 --- a/src/viewer/rendering/ui/uiSettings.h +++ b/src/viewer/rendering/ui/uiSettings.h @@ -10,5 +10,4 @@ namespace UiSettings void Initialize( AppState& appState ); void MarkDirty(); void Save(); -const std::string& GetIniFilePath(); } From 6c47697daa688bdc4c36c3a084ced8115a130a4a Mon Sep 17 00:00:00 2001 From: JohnGreenCCP Date: Thu, 17 Sep 2026 14:33:56 +0100 Subject: [PATCH 4/5] Fixing co-piolet suggestions --- src/viewer/rendering/ui/uiDetailWindow.cpp | 16 ++++++++-------- src/viewer/rendering/ui/uiMenubar.cpp | 4 ++++ src/viewer/rendering/ui/uiSettings.cpp | 3 ++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/viewer/rendering/ui/uiDetailWindow.cpp b/src/viewer/rendering/ui/uiDetailWindow.cpp index f00d5fd..2c0917d 100644 --- a/src/viewer/rendering/ui/uiDetailWindow.cpp +++ b/src/viewer/rendering/ui/uiDetailWindow.cpp @@ -599,7 +599,7 @@ void UIDetailWindow::RenderAttributeTable( const uint8_t* vbData, uint32_t verte } ImGui::TableSetupScrollFreeze( 1, 1 ); - ImGui::TableSetupColumn( "Index", ImGuiTableColumnFlags_WidthFixed, 48.0f ); + ImGui::TableSetupColumn( "Index", ImGuiTableColumnFlags_WidthFixed, UiConsts::Scaled( 48.0f ) ); for( const auto& attr : attributes ) { ImGui::TableSetupColumn( GetUsageFlagLabel( attr.usage, attr.usageIndex ).c_str(), ImGuiTableColumnFlags_WidthStretch ); @@ -714,7 +714,7 @@ void UIDetailWindow::RenderIndexData( const CmfContent& cmfContent, const cmf::M if( ImGui::BeginTable( "##indexdata", areas.empty() ? 4 : 5, tableFlags, outerSize ) ) { ImGui::TableSetupScrollFreeze( 0, 1 ); - ImGui::TableSetupColumn( "Triangle", ImGuiTableColumnFlags_WidthFixed, 72.0f ); + ImGui::TableSetupColumn( "Triangle", ImGuiTableColumnFlags_WidthFixed, UiConsts::Scaled( 72.0f ) ); ImGui::TableSetupColumn( "V0", ImGuiTableColumnFlags_WidthStretch ); ImGui::TableSetupColumn( "V1", ImGuiTableColumnFlags_WidthStretch ); ImGui::TableSetupColumn( "V2", ImGuiTableColumnFlags_WidthStretch ); @@ -809,7 +809,7 @@ void UIDetailWindow::RenderSkeleton( const cmf::Skeleton& skeleton ) } ImGui::TableSetupScrollFreeze( 1, 1 ); - ImGui::TableSetupColumn( "Index", ImGuiTableColumnFlags_WidthFixed, 48.0f ); + ImGui::TableSetupColumn( "Index", ImGuiTableColumnFlags_WidthFixed, UiConsts::Scaled( 48.0f ) ); for( const int ci : activeColIndices ) { ImGui::TableSetupColumn( m_boneColumnFilter[ci].first.c_str(), ImGuiTableColumnFlags_WidthStretch ); @@ -928,7 +928,7 @@ void UIDetailWindow::RenderBoneBindings( const CmfContent& cmfContent, const cmf if( ImGui::BeginTable( "##bonebindingstable", 2, tableFlags ) ) { ImGui::TableSetupScrollFreeze( 0, 1 ); - ImGui::TableSetupColumn( "Index", ImGuiTableColumnFlags_WidthFixed, 48.0f ); + ImGui::TableSetupColumn( "Index", ImGuiTableColumnFlags_WidthFixed, UiConsts::Scaled( 48.0f ) ); ImGui::TableSetupColumn( "Name", ImGuiTableColumnFlags_WidthStretch ); ImGui::TableHeadersRow(); @@ -1135,8 +1135,8 @@ void UIDetailWindow::RenderAnimationCurves( const cmf::AnimationCurve& curve, co if( ImGui::BeginTable( "##curvestable", 3, tableFlags, ImVec2( 0.0f, ImGui::GetContentRegionAvail().y ) ) ) { ImGui::TableSetupScrollFreeze( 1, 1 ); - ImGui::TableSetupColumn( "Knot", ImGuiTableColumnFlags_WidthFixed, 48.0f ); - ImGui::TableSetupColumn( "Time", ImGuiTableColumnFlags_WidthFixed, 80.0f ); + ImGui::TableSetupColumn( "Knot", ImGuiTableColumnFlags_WidthFixed, UiConsts::Scaled( 48.0f ) ); + ImGui::TableSetupColumn( "Time", ImGuiTableColumnFlags_WidthFixed, UiConsts::Scaled( 80.0f ) ); ImGui::TableSetupColumn( "Value", ImGuiTableColumnFlags_WidthFixed ); ImGui::TableHeadersRow(); @@ -1206,8 +1206,8 @@ void UIDetailWindow::RenderAudioOccluder( const cmf::AudioOcclusionMesh& audioOc ImGui::Spacing(); - const float indexColumnWidth = 60.0f; - const float columnWidth = 80.0f; + const float indexColumnWidth = UiConsts::Scaled( 60.0f ); + const float columnWidth = UiConsts::Scaled( 80.0f ); if( ImGui::CollapsingHeader( "Vertices", ImGuiTreeNodeFlags_DefaultOpen ) ) { diff --git a/src/viewer/rendering/ui/uiMenubar.cpp b/src/viewer/rendering/ui/uiMenubar.cpp index 3cc0616..a1e28de 100644 --- a/src/viewer/rendering/ui/uiMenubar.cpp +++ b/src/viewer/rendering/ui/uiMenubar.cpp @@ -151,4 +151,8 @@ void UiMenubar::RenderUiScaleMenu( AppState& appState ) } ImGui::EndMenu(); } + else + { + m_uiScaleSliderActive = false; + } } diff --git a/src/viewer/rendering/ui/uiSettings.cpp b/src/viewer/rendering/ui/uiSettings.cpp index 2fec921..a629fea 100644 --- a/src/viewer/rendering/ui/uiSettings.cpp +++ b/src/viewer/rendering/ui/uiSettings.cpp @@ -3,6 +3,7 @@ #include "uiSettings.h" #include +#include #include #include #include @@ -35,7 +36,7 @@ void ReadLine( ImGuiContext*, ImGuiSettingsHandler* handler, void*, const char* { char* end = nullptr; const float uiScale = std::strtof( line + keyLength, &end ); - if( end != line + keyLength ) + if( end != line + keyLength && std::isfinite( uiScale ) ) { GetAppState( handler ).uiScale.SetValueNoCallback( std::clamp( uiScale, UiConsts::MIN_UI_SCALE, UiConsts::MAX_UI_SCALE ) ); } From c35caeb4ece3a4d565e59784543837dd218c553a Mon Sep 17 00:00:00 2001 From: JohnGreenCCP Date: Thu, 17 Sep 2026 14:43:37 +0100 Subject: [PATCH 5/5] Fixing uninitialised variable --- src/viewer/rendering/ui/uiSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/viewer/rendering/ui/uiSettings.cpp b/src/viewer/rendering/ui/uiSettings.cpp index a629fea..c2fd7ec 100644 --- a/src/viewer/rendering/ui/uiSettings.cpp +++ b/src/viewer/rendering/ui/uiSettings.cpp @@ -55,7 +55,7 @@ namespace UiSettings { void Initialize( AppState& appState ) { - ImGuiSettingsHandler handler; + ImGuiSettingsHandler handler{}; handler.TypeName = SETTINGS_TYPE_NAME; handler.TypeHash = ImHashStr( SETTINGS_TYPE_NAME ); handler.ReadOpenFn = ReadOpen;