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.h b/src/viewer/rendering/ui/uiConsts.h index c6395b1..e9db17b 100644 --- a/src/viewer/rendering/ui/uiConsts.h +++ b/src/viewer/rendering/ui/uiConsts.h @@ -5,10 +5,45 @@ 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; + +// 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 baseSize * ImGui::GetFontSize() / 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 18f237b..2cbf42b 100644 --- a/src/viewer/rendering/ui/uiCustomWidgets.h +++ b/src/viewer/rendering/ui/uiCustomWidgets.h @@ -20,8 +20,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..2c0917d 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 @@ -598,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 ); @@ -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(); } @@ -713,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 ); @@ -808,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 ); @@ -927,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(); @@ -1134,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(); @@ -1205,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 ) ) { @@ -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..a1e28de 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,32 @@ 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(); + } + else + { + m_uiScaleSliderActive = false; + } +} 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..c31616a 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,47 @@ 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 ); + + ImGuiIO& io = ImGui::GetIO(); + io.Fonts->Clear(); + + ImFontConfig defaultConfig; + defaultConfig.SizePixels = UiConsts::BASE_FONT_SIZE * scale; + 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::BASE_FONT_AWESOME_SIZE * scale, &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 +185,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 +211,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..c2fd7ec --- /dev/null +++ b/src/viewer/rendering/ui/uiSettings.cpp @@ -0,0 +1,84 @@ +// Copyright © 2026 CCP ehf. + +#include "uiSettings.h" + +#include +#include +#include +#include +#include +#include // ImGuiSettingsHandler +#include "log.h" +#include "uiConsts.h" + +// 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"; + +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 && std::isfinite( uiScale ) ) + { + 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 ) +{ + 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 + const char* iniFile = ImGui::GetIO().IniFilename; + Log::Info( "Using settings file %s", iniFile ); + ImGui::LoadIniSettingsFromDisk( iniFile ); +} + +void MarkDirty() +{ + ImGui::MarkIniSettingsDirty(); +} + +void Save() +{ + ImGui::SaveIniSettingsToDisk( ImGui::GetIO().IniFilename ); +} +} + +// 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..11a415f --- /dev/null +++ b/src/viewer/rendering/ui/uiSettings.h @@ -0,0 +1,13 @@ +// 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(); +}