Compare commits

...
Author SHA1 Message Date
anthropic-code-agent[bot] 5ad46585b6 Fractional scaling 2026-03-22 19:58:16 +01:00
anthropic-code-agent[bot]andashaduri 7346db784a Add unit tests for fractional DPI scaling
- Create gui/tests directory structure with CMakeLists.txt
- Add test_fractional_scaling.cpp with comprehensive tests for:
  - 125%, 150%, 175%, 225%, 250% fractional scaling calculations
  - Correction ratio computation (system_scale / integer_ui_scale)
  - DPI detection logic from various DPI values
  - Edge cases: 100% and 200% integer scaling (no fractional)
- Tests verify the critical 250% case that was previously broken
- Integrate gui_tests into test_all executable

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
Agent-Logs-Url: https://github.com/ashaduri/gsmartcontrol/sessions/eb9277d5-ef45-4aee-8d6c-383739f37613
2026-03-22 17:06:49 +00:00
anthropic-code-agent[bot]andashaduri 9f62577161 Fix fractional DPI scaling to store full percent and compute correction ratio
- Store full scale percent (e.g., 250 for 250%) in g_windows_fractional_scaling_percent
- Compute correction = system_scale / integer_ui_scale for proper >=200% scaling
- Update API docs to reflect full percent return value (not fractional remainder)

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
Agent-Logs-Url: https://github.com/ashaduri/gsmartcontrol/sessions/d146d9d4-4974-4ee0-bd7b-12a84ce237ea
2026-03-22 17:00:17 +00:00
anthropic-code-agent[bot]andashaduri a79907bd05 Address PR review comments
- Add forward declaration for Gtk::Window in gsc_init.h to fix compilation
- Use std::lround() instead of truncation for proper rounding in scaling
- Update include comments to reference app_apply_fractional_scaling_to_default_size()

Changes:
1. Forward declare Gtk::Window namespace to avoid requiring Gtkmm headers
2. Replace truncating cast with std::lround() for accurate scaling (e.g., 850*1.25 = 1062.5 → 1063 instead of 1062)
3. Fix include comments in gsc_text_window.h and gsc_info_window.cpp

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-06 14:38:18 +00:00
anthropic-code-agent[bot]andashaduri b574294a51 Refactor: Extract fractional scaling logic to separate function
Extract the duplicated fractional scaling logic into a reusable function
`app_apply_fractional_scaling_to_default_size()` in gsc_init.cpp.

This eliminates code duplication across GscMainWindow, GscInfoWindow,
GscExecutorLogWindow, and GscTextWindow constructors.

The new function:
- Takes window pointer and configured sizes as parameters
- Handles all scaling logic in one place
- Makes it easier to maintain and extend

Addresses review feedback from @ashaduri.

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-06 14:26:09 +00:00
anthropic-code-agent[bot]andashaduri f1fd685441 Adjust default window sizes for fractional HiDPI scaling on Windows
When fractional HiDPI scaling is used on Windows (e.g., 125%, 150%, 250%),
GTK3 only applies integer scaling to the UI, but fonts are scaled to match
the actual DPI. This causes windows to appear smaller than they should be.

This fix detects the fractional scaling percentage during app initialization
and applies it to the default window sizes for all major windows:
- Main window (560x450 → scaled by fraction)
- Info window (850x600 → scaled by fraction)
- Executor log window (900x650 → scaled by fraction)
- Text window (800x600 → scaled by fraction)

The scaling is only applied when:
1. Fractional scaling is detected on Windows
2. No custom window size has been configured by the user

This ensures windows scale proportionally with fonts, providing a consistent
user experience on Windows with fractional HiDPI scaling.

Co-authored-by: ashaduri <2302268+ashaduri@users.noreply.github.com>
2026-03-04 21:58:06 +00:00
anthropic-code-agent[bot] bd30bbe3e5 Initial plan 2026-03-04 21:52:33 +00:00
12 changed files with 350 additions and 10 deletions
+55
View File
@@ -11,6 +11,8 @@ Copyright:
#include "gui_utils.h"
#include <cmath>
namespace {
@@ -165,6 +167,59 @@ bool gui_show_text_entry_dialog(const std::string& title, const std::string& mes
namespace {
/// Storage for Windows fractional scaling percentage (0 if no fractional scaling)
int g_windows_fractional_scaling_percent = 0;
}
int app_get_windows_fractional_scaling_percent()
{
return g_windows_fractional_scaling_percent;
}
void app_set_windows_fractional_scaling_percent(int percent)
{
g_windows_fractional_scaling_percent = percent;
}
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h)
{
if (!window) {
return;
}
int size_w = config_size_w;
int size_h = config_size_h;
// Apply fractional scaling adjustment on Windows if no custom size is configured
// This compensates for GTK3's lack of fractional scaling support
const int full_percent = g_windows_fractional_scaling_percent;
if (full_percent > 0 && size_w == 0 && size_h == 0) {
// Get the default size from glade and scale it
int glade_w = 0, glade_h = 0;
window->get_default_size(glade_w, glade_h);
if (glade_w > 0 && glade_h > 0) {
const double system_scale = static_cast<double>(full_percent) / 100.0;
const int integer_ui_scale = full_percent / 100; // GTK3 uses floor (integer) scaling
const double correction = system_scale / static_cast<double>(integer_ui_scale);
size_w = static_cast<int>(std::lround(glade_w * correction));
size_h = static_cast<int>(std::lround(glade_h * correction));
}
}
if (size_w > 0 && size_h > 0) {
window->set_default_size(size_w, size_h);
}
}
+17
View File
@@ -59,6 +59,23 @@ bool gui_show_text_entry_dialog(const std::string& title, const std::string& mes
/// Get the fractional scaling percentage detected on Windows (0 if not detected or integer scale).
/// For example, at 150% scaling, this returns 150; at 125% scaling, this returns 125; at 250% scaling, this returns 250.
/// Returns 0 for exact integer scales (100%, 200%, etc.).
int app_get_windows_fractional_scaling_percent();
/// Set the fractional scaling percentage detected on Windows.
/// This should only be called once during application initialization.
void app_set_windows_fractional_scaling_percent(int percent);
/// Apply fractional scaling to default window size if fractional scaling is detected.
/// This compensates for GTK3's lack of fractional scaling support on Windows.
/// \param window The window to apply scaling to
/// \param config_size_w Configured width (0 if using glade default)
/// \param config_size_h Configured height (0 if using glade default)
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h);
+1
View File
@@ -68,4 +68,5 @@ endif()
add_subdirectory(ui)
add_subdirectory(tests)
+3
View File
@@ -31,6 +31,9 @@ Copyright:
GscExecutorLogWindow::GscExecutorLogWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
: AppBuilderWidget<GscExecutorLogWindow, false>(gtkcobj, std::move(ui))
{
// Apply fractional scaling to default window size
app_apply_fractional_scaling_to_default_size(this, 0, 0);
// Connect callbacks
Gtk::Button* window_close_button = nullptr;
+2 -3
View File
@@ -35,6 +35,7 @@ Copyright:
#include "gsc_info_window.h"
#include "gsc_executor_error_dialog.h"
#include "gsc_startup_settings.h"
#include "gsc_init.h" // app_apply_fractional_scaling_to_default_size()
@@ -166,9 +167,7 @@ GscInfoWindow::GscInfoWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
{
const int def_size_w = rconfig::get_data<int>("gui/info_window/default_size_w");
const int def_size_h = rconfig::get_data<int>("gui/info_window/default_size_h");
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
}
// Create missing widgets
+10 -4
View File
@@ -47,6 +47,7 @@
#include "applib/window_instance_manager.h"
#include "applib/gsc_settings.h"
#include "applib/gui_utils.h" // app_set_windows_fractional_scaling_percent()
#include "gsc_main_window.h"
#include "gsc_executor_log_window.h"
#include "gsc_init.h"
@@ -80,6 +81,7 @@ namespace {
return channel;
}
}
@@ -519,10 +521,14 @@ bool app_init_and_loop(int& argc, char**& argv)
if (h_ppi > 0) {
const double scale = h_ppi / 96.0;
debug_out_info("app", "Windows system DPI: " << h_ppi << ", scale: " << scale << "\n");
const int fraction_percent = static_cast<int>(std::round(scale * 100)) % 100;
if (fraction_percent != 0) { // fractional scaling
// Increase the font size by fraction, but round down the size to match the Windows behavior (?)
debug_out_dump("app", "Fractional scaling detected, increasing font size by " << fraction_percent << "%.\n");
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100; // 250 -> 200, 150 -> 100
if (full_scale_percent != integer_scale_percent) { // fractional scaling detected
// Store the full scale percent for use in window sizing
app_set_windows_fractional_scaling_percent(full_scale_percent);
// Increase the font size by the fractional amount
const int fraction_percent = full_scale_percent - integer_scale_percent;
debug_out_dump("app", "Fractional scaling detected (" << full_scale_percent << "%), increasing font size by " << fraction_percent << "%.\n");
Gtk::Settings::get_default()->property_gtk_font_name()
.set_value("Segoe UI " + hz::number_to_string_nolocale(static_cast<int>(9 * (1. + fraction_percent/100.))));
}
+19
View File
@@ -15,6 +15,11 @@ Copyright:
#include <string>
namespace Gtk {
class Window;
}
/// Initialize the application and run the main loop
bool app_init_and_loop(int& argc, char**& argv);
@@ -28,6 +33,20 @@ void app_quit();
std::string app_get_debug_buffer_str();
/// Get the fractional scaling percentage detected on Windows (0 if not detected or integer scale).
/// For example, at 150% scaling, this returns 150; at 125% scaling, this returns 125; at 250% scaling, this returns 250.
/// Returns 0 for exact integer scales (100%, 200%, etc.).
int app_get_windows_fractional_scaling_percent();
/// Apply fractional scaling to default window size if fractional scaling is detected.
/// This compensates for GTK3's lack of fractional scaling support on Windows.
/// \param window The window to apply scaling to
/// \param config_size_w Configured width (0 if using glade default)
/// \param config_size_h Configured height (0 if using glade default)
void app_apply_fractional_scaling_to_default_size(Gtk::Window* window, int config_size_w, int config_size_h);
#endif
+1 -3
View File
@@ -60,9 +60,7 @@ GscMainWindow::GscMainWindow(BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder>
{
const int def_size_w = rconfig::get_data<int>("gui/main_window/default_size_w");
const int def_size_h = rconfig::get_data<int>("gui/main_window/default_size_h");
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
}
// show the window first, scan later
+4
View File
@@ -24,6 +24,7 @@ Copyright:
#include "applib/app_builder_widget.h"
#include "applib/app_gtkmm_tools.h"
#include "applib/gui_utils.h" // app_apply_fractional_scaling_to_default_size()
@@ -49,6 +50,9 @@ class GscTextWindow : public AppBuilderWidget<GscTextWindow<InstanceSwitch>, Ins
GscTextWindow(typename Gtk::Window::BaseObjectType* gtkcobj, Glib::RefPtr<Gtk::Builder> ui)
: AppBuilderWidget<GscTextWindow<InstanceSwitch>, InstanceSwitch::multi_instance>(gtkcobj, std::move(ui))
{
// Apply fractional scaling to default window size
app_apply_fractional_scaling_to_default_size(this, 0, 0);
// Connect callbacks
Gtk::Button* save_as_button = nullptr;
+21
View File
@@ -0,0 +1,21 @@
###############################################################################
# License: BSD Zero Clause License file
# Copyright:
# (C) 2026 Alexander Shaduri <ashaduri@gmail.com>
###############################################################################
if (NOT APP_BUILD_TESTS)
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL true)
else()
set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL false)
endif()
# Use Object libraries to allow runtime test discovery
add_library(gui_tests OBJECT)
target_sources(gui_tests PRIVATE
test_fractional_scaling.cpp
)
target_link_libraries(gui_tests PRIVATE
Catch2
)
+216
View File
@@ -0,0 +1,216 @@
/******************************************************************************
License: BSD Zero Clause License
Copyright:
(C) 2008 - 2026 Alexander Shaduri <ashaduri@gmail.com>
******************************************************************************/
/// \file
/// \author Alexander Shaduri
/// \ingroup gui_tests
/// \weakgroup gui_tests
/// @{
#include "catch2/catch.hpp"
#include <cmath>
/// Test the fractional scaling calculation logic
/// This tests the mathematical correctness of the scaling correction ratio
TEST_CASE("FractionalScalingCalculation", "[gui][scaling]")
{
// Test the calculation logic that's used in app_apply_fractional_scaling_to_default_size()
// correction = system_scale / integer_ui_scale
SECTION("125% scaling (fractional)")
{
const int full_percent = 125;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.25
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.25);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.25);
// Test window scaling: 800x600 base should become 1000x750
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1000);
REQUIRE(scaled_h == 750);
}
SECTION("150% scaling (fractional)")
{
const int full_percent = 150;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.5
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.5);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.5);
// Test window scaling: 800x600 base should become 1200x900
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1200);
REQUIRE(scaled_h == 900);
}
SECTION("175% scaling (fractional)")
{
const int full_percent = 175;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 1.75
const int integer_ui_scale = full_percent / 100; // 1
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 1.75);
REQUIRE(integer_ui_scale == 1);
REQUIRE(correction == 1.75);
// Test window scaling: 800x600 base should become 1400x1050
const int base_w = 800;
const int base_h = 600;
const int scaled_w = static_cast<int>(std::lround(base_w * correction));
const int scaled_h = static_cast<int>(std::lround(base_h * correction));
REQUIRE(scaled_w == 1400);
REQUIRE(scaled_h == 1050);
}
SECTION("250% scaling (fractional) - Critical test case")
{
// This is the case that was broken before the fix
const int full_percent = 250;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 2.5
const int integer_ui_scale = full_percent / 100; // 2
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 2.5);
REQUIRE(integer_ui_scale == 2);
REQUIRE(correction == 1.25); // NOT 1.5 (the old broken behavior)
// Test window scaling: GTK already applies 2x, we need to apply 1.25x more
// For a base of 800x600, GTK makes it 1600x1200, we should scale to 2000x1500
const int gtk_scaled_w = 1600; // After GTK's 2x integer scaling
const int gtk_scaled_h = 1200;
const int final_w = static_cast<int>(std::lround(gtk_scaled_w * correction));
const int final_h = static_cast<int>(std::lround(gtk_scaled_h * correction));
REQUIRE(final_w == 2000); // 2.5x total = 800 * 2.5
REQUIRE(final_h == 1500); // 2.5x total = 600 * 2.5
}
SECTION("225% scaling (fractional)")
{
const int full_percent = 225;
const double system_scale = static_cast<double>(full_percent) / 100.0; // 2.25
const int integer_ui_scale = full_percent / 100; // 2
const double correction = system_scale / static_cast<double>(integer_ui_scale);
REQUIRE(system_scale == 2.25);
REQUIRE(integer_ui_scale == 2);
REQUIRE(correction == 1.125);
// Test window scaling: 800x600 at 2x GTK = 1600x1200, with correction = 1800x1350
const int gtk_scaled_w = 1600;
const int gtk_scaled_h = 1200;
const int final_w = static_cast<int>(std::lround(gtk_scaled_w * correction));
const int final_h = static_cast<int>(std::lround(gtk_scaled_h * correction));
REQUIRE(final_w == 1800);
REQUIRE(final_h == 1350);
}
SECTION("100% scaling (integer, no fractional)")
{
// Should not trigger fractional scaling at all (full_percent would be 0)
const int full_percent = 100;
const int integer_scale_percent = (full_percent / 100) * 100;
REQUIRE(full_percent == integer_scale_percent); // No fractional component
}
SECTION("200% scaling (integer, no fractional)")
{
// Should not trigger fractional scaling at all (full_percent would be 0)
const int full_percent = 200;
const int integer_scale_percent = (full_percent / 100) * 100;
REQUIRE(full_percent == integer_scale_percent); // No fractional component
}
}
/// Test the DPI detection logic
TEST_CASE("DPIDetectionLogic", "[gui][scaling][dpi]")
{
SECTION("Detect 125% scaling from DPI")
{
const int h_ppi = 120; // 120 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 125);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("Detect 150% scaling from DPI")
{
const int h_ppi = 144; // 144 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 150);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("Detect 250% scaling from DPI")
{
const int h_ppi = 240; // 240 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 250);
REQUIRE(integer_scale_percent == 200);
REQUIRE(full_scale_percent != integer_scale_percent); // Fractional scaling detected
}
SECTION("No fractional scaling at 100% (96 DPI)")
{
const int h_ppi = 96; // 96 DPI (standard)
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 100);
REQUIRE(integer_scale_percent == 100);
REQUIRE(full_scale_percent == integer_scale_percent); // No fractional scaling
}
SECTION("No fractional scaling at 200% (192 DPI)")
{
const int h_ppi = 192; // 192 DPI
const double scale = static_cast<double>(h_ppi) / 96.0;
const int full_scale_percent = static_cast<int>(std::lround(scale * 100.0));
const int integer_scale_percent = (full_scale_percent / 100) * 100;
REQUIRE(full_scale_percent == 200);
REQUIRE(integer_scale_percent == 200);
REQUIRE(full_scale_percent == integer_scale_percent); // No fractional scaling
}
}
/// @}
+1
View File
@@ -22,6 +22,7 @@ target_link_libraries(test_all PRIVATE
libdebug
applib_tests
hz_tests
gui_tests
Catch2
)