Compare commits

..
14 changed files with 94 additions and 351 deletions
-55
View File
@@ -11,8 +11,6 @@ Copyright:
#include "gui_utils.h"
#include <cmath>
namespace {
@@ -167,59 +165,6 @@ 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,23 +59,6 @@ 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,5 +68,4 @@ endif()
add_subdirectory(ui)
add_subdirectory(tests)
-3
View File
@@ -31,9 +31,6 @@ 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;
+3 -2
View File
@@ -35,7 +35,6 @@ 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()
@@ -167,7 +166,9 @@ 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");
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
}
// Create missing widgets
+30 -10
View File
@@ -47,7 +47,6 @@
#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"
@@ -81,7 +80,6 @@ namespace {
return channel;
}
}
@@ -507,6 +505,32 @@ bool app_init_and_loop(int& argc, char**& argv)
}
*/
// Detect Windows dark mode and set GTK theme preference accordingly
if constexpr(BuildEnv::is_kernel_family_windows()) {
Glib::RefPtr<Gtk::Settings> gtk_settings = Gtk::Settings::get_default();
if (gtk_settings) {
bool use_dark_theme = false;
#ifdef _WIN32
// Check Windows registry for dark mode preference
// HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize
// AppsUseLightTheme = 0 means dark mode, 1 means light mode
DWORD apps_use_light_theme = 1; // Default to light mode
if (hz::win32_get_registry_value_dword(HKEY_CURRENT_USER,
R"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)",
"AppsUseLightTheme", apps_use_light_theme)) {
use_dark_theme = (apps_use_light_theme == 0);
debug_out_dump("app", "Windows theme detected: " << (use_dark_theme ? "dark" : "light") << "\n");
} else {
debug_out_dump("app", "Could not read Windows theme preference, defaulting to light mode.\n");
}
#endif
// Apply the dark theme preference to GTK
gtk_settings->property_gtk_application_prefer_dark_theme().set_value(use_dark_theme);
debug_out_dump("app", "GTK dark theme preference set to: " << (use_dark_theme ? "dark" : "light") << "\n");
}
}
// The application is dpi-aware in Windows.
// However, Gtk3 does not support fractional scaling, so at 250% scaling in system settings, the UI will use 200%.
//
@@ -521,14 +545,10 @@ 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 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");
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");
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,11 +15,6 @@ Copyright:
#include <string>
namespace Gtk {
class Window;
}
/// Initialize the application and run the main loop
bool app_init_and_loop(int& argc, char**& argv);
@@ -33,20 +28,6 @@ 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
+3 -1
View File
@@ -60,7 +60,9 @@ 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");
app_apply_fractional_scaling_to_default_size(this, def_size_w, def_size_h);
if (def_size_w > 0 && def_size_h > 0) {
set_default_size(def_size_w, def_size_h);
}
}
// show the window first, scan later
+7 -1
View File
@@ -120,7 +120,7 @@ bool GscMainWindowIconView::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
return true;
}
if (empty_view_message_ != Message::None && this->num_icons_ == 0) { // no icons
Glib::RefPtr<Pango::Layout> layout = this->create_pango_layout("");
const Glib::RefPtr<Pango::Layout> layout = this->create_pango_layout("");
layout->set_alignment(Pango::ALIGN_CENTER);
layout->set_markup(get_message_string(empty_view_message_));
@@ -131,6 +131,12 @@ bool GscMainWindowIconView::on_draw(const Cairo::RefPtr<Cairo::Context>& cr)
const int pos_y = (get_allocation().get_height() - layout_h) / 2;
cr->move_to(pos_x, pos_y);
// Use the foreground color from the widget's style context so
// the text is visible in both light and dark themes.
const auto style_context = get_style_context();
const Gdk::RGBA fg_color = style_context->get_color(style_context->get_state());
cr->set_source_rgba(fg_color.get_red(), fg_color.get_green(), fg_color.get_blue(), fg_color.get_alpha());
layout->show_in_cairo_context(cr);
return true;
-4
View File
@@ -24,7 +24,6 @@ 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()
@@ -50,9 +49,6 @@ 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
@@ -1,21 +0,0 @@
###############################################################################
# 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
@@ -1,216 +0,0 @@
/******************************************************************************
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
}
}
/// @}
+51
View File
@@ -79,6 +79,14 @@ inline bool win32_set_registry_value_string(HKEY base,
const std::string& keydir, const std::string& key, const std::string& value);
/// Get registry value as a DWORD.
/// Base may be e.g. HKEY_CURRENT_USER.
/// Note that this works only with REG_DWORD types.
/// False is returned for all other types.
inline bool win32_get_registry_value_dword(HKEY base,
const std::string& keydir, const std::string& key, DWORD& put_here);
/// Redirect stdout and stderr to console window (if open). Requires winxp (at compile-time).
/// \param create_if_none if true, create a new console if none was found and attach to it.
/// \return false if failed or unsupported.
@@ -340,6 +348,49 @@ inline bool win32_set_registry_value_string(HKEY base,
// Get registry value as a DWORD.
// Note that this works only with REG_DWORD types.
inline bool win32_get_registry_value_dword(HKEY base,
const std::string& keydir, const std::string& key, DWORD& put_here)
{
std::wstring wkeydir = win32_utf8_to_utf16(keydir);
if (wkeydir.empty())
return false;
HKEY reg_key = nullptr;
bool open_status = (RegOpenKeyExW(base, wkeydir.c_str(), 0, KEY_QUERY_VALUE, &reg_key) == ERROR_SUCCESS);
if (!open_status)
return false;
bool ok = false;
std::wstring wkey = win32_utf8_to_utf16(key, &ok);
if (!ok) { // conversion error. Note that an empty string is not an error.
if (reg_key)
RegCloseKey(reg_key);
return false;
}
DWORD type = 0;
DWORD value = 0;
DWORD nbytes = sizeof(DWORD);
bool status = (RegQueryValueExW(reg_key, wkey.c_str(), nullptr, &type,
reinterpret_cast<BYTE*>(&value), &nbytes) == ERROR_SUCCESS);
if (status && type == REG_DWORD) {
put_here = value;
} else {
status = false;
}
if (reg_key)
RegCloseKey(reg_key);
return status;
}
// Redirect stdout and stderr to console window (if open).
inline bool win32_redirect_stdio_to_console(bool create_if_none)
{
-1
View File
@@ -22,7 +22,6 @@ target_link_libraries(test_all PRIVATE
libdebug
applib_tests
hz_tests
gui_tests
Catch2
)