From 2c21bd516bd5eabd28e6f66702de25fd88bf9d18 Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Thu, 4 Jun 2026 18:36:22 +0100 Subject: [PATCH] improve Linux runtime behavior --- include/rex/cvar.h | 2 + include/rex/input/mnk/mnk_input_driver.h | 1 + include/rex/ui/imgui_dialog.h | 3 + include/rex/ui/window_gtk.h | 10 ++ src/core/cvar.cpp | 16 +- src/input/input_system.cpp | 14 +- src/input/mnk/mnk_input_driver.cpp | 43 ++++- src/ui/imgui_dialog.cpp | 14 +- src/ui/overlay/simple_settings_overlay.cpp | 105 ++++++++++--- src/ui/rex_app.cpp | 20 ++- src/ui/window_gtk.cpp | 175 ++++++++++++++++++++- 11 files changed, 349 insertions(+), 54 deletions(-) diff --git a/include/rex/cvar.h b/include/rex/cvar.h index 5d16f3d..2299e13 100644 --- a/include/rex/cvar.h +++ b/include/rex/cvar.h @@ -112,6 +112,8 @@ bool IsFinalized(); void SaveConfig(const std::filesystem::path& config_path); void SaveConfigValues(const std::filesystem::path& config_path, std::initializer_list names); +void SaveConfigValues(const std::filesystem::path& config_path, + const std::vector& names); //============================================================================= // Flag Registry diff --git a/include/rex/input/mnk/mnk_input_driver.h b/include/rex/input/mnk/mnk_input_driver.h index 2483b99..80c7b77 100644 --- a/include/rex/input/mnk/mnk_input_driver.h +++ b/include/rex/input/mnk/mnk_input_driver.h @@ -57,6 +57,7 @@ class MnkInputDriver final : public InputDriver, void UpdateMouseCapture(); void SetKeyState(uint16_t vk, bool down); void EnqueueKeystroke(uint16_t vk_pad, bool down); + void ClearStateLocked(); rex::ui::Window* attached_window_ = nullptr; diff --git a/include/rex/ui/imgui_dialog.h b/include/rex/ui/imgui_dialog.h index 1018d5e..bce6d78 100644 --- a/include/rex/ui/imgui_dialog.h +++ b/include/rex/ui/imgui_dialog.h @@ -44,6 +44,8 @@ class ImGuiDialog { ImGuiDrawer* imgui_drawer() const { return imgui_drawer_; } ImGuiIO& GetIO(); + void SetDrawActive(bool active); + bool IsDrawActive() const { return is_draw_active_; } // Closes the dialog and returns to any waiters. void Close(); @@ -54,6 +56,7 @@ class ImGuiDialog { private: ImGuiDrawer* imgui_drawer_ = nullptr; + bool is_draw_active_ = true; bool has_close_pending_ = false; std::vector waiting_fences_; }; diff --git a/include/rex/ui/window_gtk.h b/include/rex/ui/window_gtk.h index 433fda3..2d9d4ba 100644 --- a/include/rex/ui/window_gtk.h +++ b/include/rex/ui/window_gtk.h @@ -38,12 +38,14 @@ class GTKWindow : public Window { GtkWidget* window() const { return window_; } protected: + uint32_t GetLatestDpiImpl() const override; bool OpenImpl() override; void RequestCloseImpl() override; void ApplyNewFullscreen() override; void ApplyNewTitle() override; void ApplyNewMainMenu(MenuItem* old_main_menu) override; + void ApplyNewCursorVisibility(CursorVisibility old_cursor_visibility) override; // Mouse capture seems to happen implicitly compared to Windows. void FocusImpl() override; @@ -51,6 +53,7 @@ class GTKWindow : public Window { void RequestPaintImpl() override; private: + void UpdateDpi(WindowDestructionReceiver* destruction_receiver = nullptr); void HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver); // For updating multiple factors that may influence the window size at once, // without handling the configure event multiple times (that may not only @@ -64,6 +67,9 @@ class GTKWindow : public Window { bool HandleKeyboard(GdkEventKey* event, WindowDestructionReceiver& destruction_receiver); gboolean WindowEventHandler(GdkEvent* event); static gboolean WindowEventHandlerThunk(GtkWidget* widget, GdkEvent* event, gpointer user_data); + void ApplyCursor(); + void SetCursorAutoHideTimer(); + static gboolean AutoHideCursorTimerCallback(gpointer user_data); // Handling events related specifically to the drawing (client) area. gboolean DrawingAreaEventHandler(GdkEvent* event); @@ -75,6 +81,10 @@ class GTKWindow : public Window { GtkWidget* window_ = nullptr; GtkWidget* box_ = nullptr; GtkWidget* drawing_area_ = nullptr; + GdkCursor* blank_cursor_ = nullptr; + guint cursor_auto_hide_timer_ = 0; + bool cursor_currently_auto_hidden_ = false; + uint32_t dpi_ = 96; uint32_t batched_size_update_depth_ = 0; bool batched_size_update_contained_configure_ = false; diff --git a/src/core/cvar.cpp b/src/core/cvar.cpp index f552734..2c8ac3b 100644 --- a/src/core/cvar.cpp +++ b/src/core/cvar.cpp @@ -606,25 +606,16 @@ bool IsFinalized() { void SaveConfig(const std::filesystem::path& config_path) { try { toml::table config; - if (std::filesystem::exists(config_path)) { - config = toml::parse_file(config_path.string()); - } { std::lock_guard lock(GetRegistryMutex()); - bool modified = false; for (const auto& entry : GetRegistryStorage()) { if (entry.getter() == entry.default_value) { continue; } const std::string value = entry.getter(); - modified |= WriteConfigValue(config, entry, value); - } - - if (!modified) { - REXLOG_DEBUG("SaveConfig: no modified flags to save"); - return; + WriteConfigValue(config, entry, value); } } @@ -638,6 +629,11 @@ void SaveConfig(const std::filesystem::path& config_path) { void SaveConfigValues(const std::filesystem::path& config_path, std::initializer_list names) { + SaveConfigValues(config_path, std::vector(names)); +} + +void SaveConfigValues(const std::filesystem::path& config_path, + const std::vector& names) { try { toml::table config; if (std::filesystem::exists(config_path)) { diff --git a/src/input/input_system.cpp b/src/input/input_system.cpp index c822b22..426e022 100644 --- a/src/input/input_system.cpp +++ b/src/input/input_system.cpp @@ -22,8 +22,20 @@ #include #include #include +#include -REXCVAR_DEFINE_STRING(input_backend, "xinput", "Input", "Input backend: sdl, xinput") +namespace { + +constexpr const char* kDefaultInputBackend = +#if REX_PLATFORM_WIN32 + "xinput"; +#else + "sdl"; +#endif + +} // namespace + +REXCVAR_DEFINE_STRING(input_backend, kDefaultInputBackend, "Input", "Input backend: sdl, xinput") .allowed({"sdl", "xinput"}); REXCVAR_DEFINE_BOOL(guide_button, false, "Input", "Enable guide button pass-through"); diff --git a/src/input/mnk/mnk_input_driver.cpp b/src/input/mnk/mnk_input_driver.cpp index 0971217..f853e7a 100644 --- a/src/input/mnk/mnk_input_driver.cpp +++ b/src/input/mnk/mnk_input_driver.cpp @@ -106,6 +106,12 @@ bool MnkInputDriver::IsEnabled() const { return REXCVAR_GET(mnk_mode); } +void MnkInputDriver::ClearStateLocked() { + std::memset(key_down_, 0, sizeof(key_down_)); + mouse_dx_ = 0; + mouse_dy_ = 0; +} + static bool IsBindPressed(const bool (&key_down)[256], const std::string& cvar_val) { VirtualKey vk = rex::ui::ParseVirtualKey(cvar_val); if (vk == VirtualKey::kNone) @@ -138,13 +144,20 @@ X_RESULT MnkInputDriver::GetCapabilities(uint32_t user_index, uint32_t flags, } X_RESULT MnkInputDriver::GetState(uint32_t user_index, X_INPUT_STATE* out_state) { - if (!IsEnabled() || user_index != UserIndex()) { + if (!IsEnabled()) { + std::lock_guard lock(state_mutex_); + ClearStateLocked(); + return X_ERROR_DEVICE_NOT_CONNECTED; + } + if (user_index != UserIndex()) { return X_ERROR_DEVICE_NOT_CONNECTED; } UpdateMouseCapture(); if (!is_active() || !has_focus_) { + std::lock_guard lock(state_mutex_); + ClearStateLocked(); if (out_state) { std::memset(out_state, 0, sizeof(*out_state)); out_state->packet_number = packet_number_; @@ -235,10 +248,25 @@ X_RESULT MnkInputDriver::SetState(uint32_t user_index, X_INPUT_VIBRATION* vibrat X_RESULT MnkInputDriver::GetKeystroke(uint32_t user_index, uint32_t flags, X_INPUT_KEYSTROKE* out_keystroke) { - if (!IsEnabled() || user_index != UserIndex()) { + if (!IsEnabled()) { + std::lock_guard lock(state_mutex_); + ClearStateLocked(); + while (!keystroke_queue_.empty()) { + keystroke_queue_.pop(); + } + return X_ERROR_DEVICE_NOT_CONNECTED; + } + if (user_index != UserIndex()) { return X_ERROR_DEVICE_NOT_CONNECTED; } std::lock_guard lock(state_mutex_); + if (!is_active() || !has_focus_) { + ClearStateLocked(); + while (!keystroke_queue_.empty()) { + keystroke_queue_.pop(); + } + return X_ERROR_EMPTY; + } if (keystroke_queue_.empty()) { return X_ERROR_EMPTY; } @@ -308,8 +336,9 @@ void MnkInputDriver::SetKeyState(uint16_t vk, bool down) { } void MnkInputDriver::OnKeyDown(rex::ui::KeyEvent& e) { - if (!IsEnabled() || !has_focus_) + if (!IsEnabled() || !has_focus_ || !is_active()) { return; + } std::lock_guard lock(state_mutex_); uint16_t vk = static_cast(e.virtual_key()); SetKeyState(vk, true); @@ -324,7 +353,7 @@ void MnkInputDriver::OnKeyUp(rex::ui::KeyEvent& e) { } void MnkInputDriver::OnMouseDown(rex::ui::MouseEvent& e) { - if (!IsEnabled() || !has_focus_) + if (!IsEnabled() || !has_focus_ || !is_active()) return; std::lock_guard lock(state_mutex_); switch (e.button()) { @@ -362,7 +391,7 @@ void MnkInputDriver::OnMouseUp(rex::ui::MouseEvent& e) { } void MnkInputDriver::OnMouseMove(rex::ui::MouseEvent& e) { - if (!IsEnabled() || !has_focus_) + if (!IsEnabled() || !has_focus_ || !is_active()) return; std::lock_guard lock(state_mutex_); int32_t x = e.x(); @@ -376,9 +405,7 @@ void MnkInputDriver::OnMouseMove(rex::ui::MouseEvent& e) { void MnkInputDriver::OnLostFocus(rex::ui::UISetupEvent&) { std::lock_guard lock(state_mutex_); has_focus_ = false; - std::memset(key_down_, 0, sizeof(key_down_)); - mouse_dx_ = 0; - mouse_dy_ = 0; + ClearStateLocked(); if (mouse_captured_ && attached_window_) { mouse_captured_ = false; attached_window_->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible); diff --git a/src/ui/imgui_dialog.cpp b/src/ui/imgui_dialog.cpp index 20cec48..5e62ac7 100644 --- a/src/ui/imgui_dialog.cpp +++ b/src/ui/imgui_dialog.cpp @@ -23,7 +23,7 @@ ImGuiDialog::ImGuiDialog(ImGuiDrawer* imgui_drawer) : imgui_drawer_(imgui_drawer } ImGuiDialog::~ImGuiDialog() { - imgui_drawer_->RemoveDialog(this); + SetDrawActive(false); for (auto fence : waiting_fences_) { fence->Signal(); } @@ -41,6 +41,18 @@ ImGuiIO& ImGuiDialog::GetIO() { return imgui_drawer()->GetIO(); } +void ImGuiDialog::SetDrawActive(bool active) { + if (is_draw_active_ == active) { + return; + } + is_draw_active_ = active; + if (active) { + imgui_drawer_->AddDialog(this); + } else { + imgui_drawer_->RemoveDialog(this); + } +} + void ImGuiDialog::Draw() { // Draw UI. OnDraw(GetIO()); diff --git a/src/ui/overlay/simple_settings_overlay.cpp b/src/ui/overlay/simple_settings_overlay.cpp index dc1af27..4f0661f 100644 --- a/src/ui/overlay/simple_settings_overlay.cpp +++ b/src/ui/overlay/simple_settings_overlay.cpp @@ -23,18 +23,44 @@ constexpr std::array kFrameCapRates = {60.0, 90.0, 120.0, 144.0, 165. constexpr std::array kFrameCapLabels = {"Unlimited", "60 FPS", "90 FPS", "120 FPS", "144 FPS", "165 FPS", "240 FPS"}; -constexpr std::array kSimpleSettingsCvars = { +constexpr std::array kCoreSimpleSettingsCvars = { "resolution_scale", "draw_resolution_scale_x", "draw_resolution_scale_y", - "d3d12_present_frame_limiter", - "d3d12_present_frame_limiter_fps", "fullscreen", "vsync", - "d3d12_allow_variable_refresh_rate_and_tearing", "mnk_mode", "mnk_capture_mouse"}; +bool HasCvar(std::string_view name) { + return rex::cvar::GetFlagInfo(name) != nullptr; +} + +std::vector GetSimpleSettingsCvars() { + std::vector cvars; + cvars.reserve(14); + cvars.insert(cvars.end(), kCoreSimpleSettingsCvars.begin(), kCoreSimpleSettingsCvars.end()); + if (HasCvar("d3d12_present_frame_limiter")) { + cvars.push_back("d3d12_present_frame_limiter"); + } + if (HasCvar("d3d12_present_frame_limiter_fps")) { + cvars.push_back("d3d12_present_frame_limiter_fps"); + } + if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) { + cvars.push_back("d3d12_allow_variable_refresh_rate_and_tearing"); + } + if (HasCvar("vulkan_allow_present_mode_immediate")) { + cvars.push_back("vulkan_allow_present_mode_immediate"); + } + if (HasCvar("vulkan_allow_present_mode_mailbox")) { + cvars.push_back("vulkan_allow_present_mode_mailbox"); + } + if (HasCvar("vulkan_allow_present_mode_fifo_relaxed")) { + cvars.push_back("vulkan_allow_present_mode_fifo_relaxed"); + } + return cvars; +} + int ResolutionIndexFromCvar() { int32_t current = rex::cvar::Query("resolution_scale"); int best = 0; @@ -50,7 +76,14 @@ int ResolutionIndexFromCvar() { return best; } +bool HasFrameCapCvars() { + return HasCvar("d3d12_present_frame_limiter") && HasCvar("d3d12_present_frame_limiter_fps"); +} + int FrameCapIndexFromCvar() { + if (!HasFrameCapCvars()) { + return 0; + } if (!rex::cvar::Query("d3d12_present_frame_limiter")) { return 0; } @@ -77,6 +110,31 @@ void SetBoolCvar(std::string_view name, bool value) { rex::cvar::SetFlagByName(name, value ? "true" : "false"); } +bool TearingFromCvar() { + if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) { + return rex::cvar::Query("d3d12_allow_variable_refresh_rate_and_tearing"); + } + if (HasCvar("vulkan_allow_present_mode_immediate")) { + return rex::cvar::Query("vulkan_allow_present_mode_immediate"); + } + return false; +} + +void SetTearingCvars(bool value) { + if (HasCvar("d3d12_allow_variable_refresh_rate_and_tearing")) { + SetBoolCvar("d3d12_allow_variable_refresh_rate_and_tearing", value); + } + if (HasCvar("vulkan_allow_present_mode_immediate")) { + SetBoolCvar("vulkan_allow_present_mode_immediate", value); + } + if (HasCvar("vulkan_allow_present_mode_mailbox")) { + SetBoolCvar("vulkan_allow_present_mode_mailbox", value); + } + if (HasCvar("vulkan_allow_present_mode_fifo_relaxed")) { + SetBoolCvar("vulkan_allow_present_mode_fifo_relaxed", value); + } +} + void SectionTitle(const char* label) { ImGui::PushFont(nullptr, 20.0f); ImGui::TextUnformatted(label); @@ -103,17 +161,7 @@ bool PrimaryButton(const char* label) { } // namespace void SaveSimpleSettingsConfig(const std::filesystem::path& config_path) { - rex::cvar::SaveConfigValues(config_path, - {"resolution_scale", - "draw_resolution_scale_x", - "draw_resolution_scale_y", - "d3d12_present_frame_limiter", - "d3d12_present_frame_limiter_fps", - "fullscreen", - "vsync", - "d3d12_allow_variable_refresh_rate_and_tearing", - "mnk_mode", - "mnk_capture_mouse"}); + rex::cvar::SaveConfigValues(config_path, GetSimpleSettingsCvars()); } void EnsureSimpleSettingsConfig(const std::filesystem::path& config_path) { @@ -121,7 +169,7 @@ void EnsureSimpleSettingsConfig(const std::filesystem::path& config_path) { if (!should_save) { try { auto config = toml::parse_file(config_path.string()); - for (std::string_view name : kSimpleSettingsCvars) { + for (std::string_view name : GetSimpleSettingsCvars()) { if (!config.contains(name)) { should_save = true; break; @@ -152,12 +200,14 @@ SimpleSettingsDialog::SimpleSettingsDialog(ImGuiDrawer* drawer, std::filesystem: restart_game_(std::move(restart_game)) { ReloadProfiles(); LoadSettingsFromCvars(); + SetDrawActive(false); } SimpleSettingsDialog::~SimpleSettingsDialog() = default; void SimpleSettingsDialog::Show() { visible_ = true; + SetDrawActive(true); ReloadProfiles(); LoadSettingsFromCvars(); } @@ -167,7 +217,7 @@ void SimpleSettingsDialog::LoadSettingsFromCvars() { frame_cap_index_ = FrameCapIndexFromCvar(); fullscreen_ = rex::cvar::Query("fullscreen"); vsync_ = rex::cvar::Query("vsync"); - tearing_ = rex::cvar::Query("d3d12_allow_variable_refresh_rate_and_tearing"); + tearing_ = TearingFromCvar(); mnk_mode_ = rex::cvar::Query("mnk_mode"); mnk_capture_mouse_ = rex::cvar::Query("mnk_capture_mouse"); } @@ -177,7 +227,7 @@ bool SimpleSettingsDialog::HasSettingsChanges() const { frame_cap_index_ != FrameCapIndexFromCvar() || fullscreen_ != rex::cvar::Query("fullscreen") || vsync_ != rex::cvar::Query("vsync") || - tearing_ != rex::cvar::Query("d3d12_allow_variable_refresh_rate_and_tearing") || + tearing_ != TearingFromCvar() || mnk_mode_ != rex::cvar::Query("mnk_mode") || mnk_capture_mouse_ != rex::cvar::Query("mnk_capture_mouse"); } @@ -195,6 +245,7 @@ void SimpleSettingsDialog::Hide() { return; } visible_ = false; + SetDrawActive(false); if (close_settings_) { close_settings_(); } @@ -222,14 +273,16 @@ void SimpleSettingsDialog::SaveVideo() { rex::cvar::SetFlagByName("resolution_scale", scale); rex::cvar::SetFlagByName("draw_resolution_scale_x", scale); rex::cvar::SetFlagByName("draw_resolution_scale_y", scale); - SetBoolCvar("d3d12_present_frame_limiter", frame_cap_index_ != 0); - if (frame_cap_index_ != 0) { + if (HasFrameCapCvars()) { + SetBoolCvar("d3d12_present_frame_limiter", frame_cap_index_ != 0); + } + if (HasFrameCapCvars() && frame_cap_index_ != 0) { rex::cvar::SetFlagByName("d3d12_present_frame_limiter_fps", std::to_string(kFrameCapRates[frame_cap_index_ - 1])); } SetBoolCvar("fullscreen", fullscreen_); SetBoolCvar("vsync", vsync_); - SetBoolCvar("d3d12_allow_variable_refresh_rate_and_tearing", tearing_); + SetTearingCvars(tearing_); SetBoolCvar("mnk_mode", mnk_mode_); SetBoolCvar("mnk_capture_mouse", mnk_capture_mouse_); SaveSimpleSettingsConfig(config_path_); @@ -341,10 +394,12 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) { static_cast(kResolutionLabels.size())); EndFieldRow(); - BeginFieldRow("Framerate Cap"); - ImGui::Combo("##frame_cap", &frame_cap_index_, kFrameCapLabels.data(), - static_cast(kFrameCapLabels.size())); - EndFieldRow(); + if (HasFrameCapCvars()) { + BeginFieldRow("Framerate Cap"); + ImGui::Combo("##frame_cap", &frame_cap_index_, kFrameCapLabels.data(), + static_cast(kFrameCapLabels.size())); + EndFieldRow(); + } ImGui::Separator(); ImGui::Spacing(); diff --git a/src/ui/rex_app.cpp b/src/ui/rex_app.cpp index fba3b9b..9f00545 100644 --- a/src/ui/rex_app.cpp +++ b/src/ui/rex_app.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -50,7 +51,18 @@ namespace rex { -REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, false, "UI/Advanced", +namespace { + +constexpr bool kBlockShaderStorageStartup = +#if REX_PLATFORM_WIN32 + true; +#else + false; +#endif + +} // namespace + +REXCVAR_DEFINE_BOOL(advanced_settings_overlay_enabled, true, "UI/Advanced", "Enable the developer cvar browser on F4"); // --- ReXApp --- @@ -262,7 +274,8 @@ bool ReXApp::ConstructRuntime(const PathConfig& paths) { input_sys->SetActiveCallback([this]() { if (!debug_overlay_ && !console_overlay_ && !settings_overlay_) return true; - return !imgui_drawer_->GetIO().WantCaptureMouse; + const auto& io = imgui_drawer_->GetIO(); + return !io.WantCaptureMouse && !io.WantCaptureKeyboard; }); } } @@ -416,7 +429,8 @@ void ReXApp::LaunchModule() { uint32_t title_id = runtime_->kernel_state()->title_id(); if (title_id != 0) { REXLOG_INFO("Initializing shader storage for title {:08X}...", title_id); - graphics_system->InitializeShaderStorage(runtime_->cache_root(), title_id, true); + graphics_system->InitializeShaderStorage(runtime_->cache_root(), title_id, + kBlockShaderStorageStartup); } } diff --git a/src/ui/window_gtk.cpp b/src/ui/window_gtk.cpp index f729067..6cb9254 100644 --- a/src/ui/window_gtk.cpp +++ b/src/ui/window_gtk.cpp @@ -27,8 +27,50 @@ #include #include +REXCVAR_DEFINE_DOUBLE(gtk_ui_scale, 1.25, "UI/GTK", + "GTK effective UI scale multiplier") + .range(0.75, 2.0) + .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); + namespace { +constexpr uint32_t kDefaultDpi = 96; + +uint32_t CalculateMonitorDpi(GdkMonitor* monitor) { + if (!monitor) { + return kDefaultDpi; + } + // GDK monitor geometry plus physical millimeter size gives EDID physical + // density, not the desktop UI scale. Match Windows DPI behavior by exposing + // effective UI DPI instead. + const int scale_factor = std::max(1, gdk_monitor_get_scale_factor(monitor)); + const auto dpi = + uint32_t(double(kDefaultDpi * uint32_t(scale_factor)) * REXCVAR_GET(gtk_ui_scale) + 0.5); + return std::clamp(dpi, uint32_t(48), uint32_t(384)); +} + +uint32_t QueryWidgetDpi(GtkWidget* widget) { + GdkDisplay* display = widget ? gtk_widget_get_display(widget) : gdk_display_get_default(); + if (!display) { + return kDefaultDpi; + } + + GdkMonitor* monitor = nullptr; + if (widget) { + GdkWindow* gdk_window = gtk_widget_get_window(widget); + if (gdk_window) { + monitor = gdk_display_get_monitor_at_window(display, gdk_window); + } + } + if (!monitor) { + monitor = gdk_display_get_primary_monitor(display); + } + if (!monitor && gdk_display_get_n_monitors(display) > 0) { + monitor = gdk_display_get_monitor(display, 0); + } + return CalculateMonitorDpi(monitor); +} + uint32_t ResolveWindowWidth(uint32_t requested_width) { if (REXCVAR_GET(window_width) > 0) { return uint32_t(REXCVAR_GET(window_width)); @@ -313,10 +355,20 @@ std::unique_ptr Window::Create(WindowedAppContext& app_context, GTKWindow::GTKWindow(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) - : Window(app_context, title, desired_logical_width, desired_logical_height) {} + : Window(app_context, title, desired_logical_width, desired_logical_height) { + dpi_ = QueryWidgetDpi(nullptr); +} GTKWindow::~GTKWindow() { EnterDestructor(); + if (cursor_auto_hide_timer_) { + g_source_remove(cursor_auto_hide_timer_); + cursor_auto_hide_timer_ = 0; + } + if (blank_cursor_) { + g_object_unref(blank_cursor_); + blank_cursor_ = nullptr; + } if (window_) { // Set window_ to null to ignore events from now on since this ui::GTKWindow // is entering an indeterminate state. @@ -329,8 +381,26 @@ GTKWindow::~GTKWindow() { } } +uint32_t GTKWindow::GetLatestDpiImpl() const { + return dpi_; +} + +void GTKWindow::UpdateDpi(WindowDestructionReceiver* destruction_receiver) { + const uint32_t new_dpi = QueryWidgetDpi(drawing_area_ ? drawing_area_ : window_); + if (!new_dpi || new_dpi == dpi_) { + return; + } + + dpi_ = new_dpi; + if (destruction_receiver) { + UISetupEvent e(this); + OnDpiChanged(e, *destruction_receiver); + } +} + bool GTKWindow::OpenImpl() { window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); + blank_cursor_ = gdk_cursor_new_for_display(gtk_widget_get_display(window_), GDK_BLANK_CURSOR); gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str()); @@ -345,6 +415,10 @@ bool GTKWindow::OpenImpl() { if (main_menu_widget) { gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, FALSE, FALSE, 0); } + const bool initial_fullscreen = IsFullscreen(); + if (initial_fullscreen && main_menu_widget) { + gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget); + } // Create the drawing area for creating the surface for, which will be the // client area of the window occupying all the window space not taken by the @@ -371,8 +445,22 @@ bool GTKWindow::OpenImpl() { g_signal_connect(G_OBJECT(drawing_area_), "draw", G_CALLBACK(DrawHandler), reinterpret_cast(this)); + if (initial_fullscreen) { + gtk_window_fullscreen(GTK_WINDOW(window_)); + g_idle_add( + [](gpointer user_data) -> gboolean { + auto* gtk_window = reinterpret_cast(user_data); + if (gtk_window->window_ && gtk_window->IsFullscreen()) { + gtk_window_fullscreen(GTK_WINDOW(gtk_window->window_)); + } + return G_SOURCE_REMOVE; + }, + this); + } + // Finally show all the widgets in the window, including the main menu. gtk_widget_show_all(window_); + UpdateDpi(); // Remove the size request after finishing the initial layout because it makes // it impossible to make the window smaller. @@ -393,12 +481,10 @@ bool GTKWindow::OpenImpl() { // After setting up the initial layout for non-fullscreen, enter fullscreen if // requested. - if (IsFullscreen()) { - if (main_menu_widget) { - gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget); - } + if (initial_fullscreen) { gtk_window_fullscreen(GTK_WINDOW(window_)); } + ApplyNewCursorVisibility(CursorVisibility::kVisible); // Make sure the initial state after opening is reported to the common Window // class no matter how GTK sends the events. @@ -556,6 +642,20 @@ void GTKWindow::ApplyNewMainMenu(MenuItem* old_main_menu) { } } +void GTKWindow::ApplyNewCursorVisibility(CursorVisibility old_cursor_visibility) { + CursorVisibility new_cursor_visibility = GetCursorVisibility(); + if (cursor_auto_hide_timer_) { + g_source_remove(cursor_auto_hide_timer_); + cursor_auto_hide_timer_ = 0; + } + cursor_currently_auto_hidden_ = new_cursor_visibility != CursorVisibility::kVisible; + ApplyCursor(); + if (new_cursor_visibility == CursorVisibility::kAutoHidden && + old_cursor_visibility == CursorVisibility::kAutoHidden) { + SetCursorAutoHideTimer(); + } +} + void GTKWindow::FocusImpl() { gtk_window_activate_focus(GTK_WINDOW(window_)); } @@ -594,10 +694,17 @@ void GTKWindow::HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver return; } - // TODO(Triang3l): Report the desired client area size. + UpdateDpi(&destruction_receiver); + if (destruction_receiver.IsWindowDestroyedOrClosed()) { + return; + } GtkAllocation drawing_area_allocation; gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation); + if (!IsFullscreen()) { + OnDesiredLogicalSizeUpdate(SizeToLogical(uint32_t(drawing_area_allocation.width)), + SizeToLogical(uint32_t(drawing_area_allocation.height))); + } OnActualSizeUpdate(uint32_t(drawing_area_allocation.width), uint32_t(drawing_area_allocation.height), destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { @@ -757,6 +864,14 @@ gboolean GTKWindow::WindowEventHandler(GdkEvent* event) { // at some point in closing anyway. GtkWidget* window = window_; window_ = nullptr; + if (cursor_auto_hide_timer_) { + g_source_remove(cursor_auto_hide_timer_); + cursor_auto_hide_timer_ = 0; + } + if (blank_cursor_) { + g_object_unref(blank_cursor_); + blank_cursor_ = nullptr; + } // Destroying the top-level window also destroys its children. drawing_area_ = nullptr; box_ = nullptr; @@ -773,6 +888,7 @@ gboolean GTKWindow::WindowEventHandler(GdkEvent* event) { if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } + ApplyCursor(); } break; case GDK_KEY_PRESS: @@ -805,12 +921,59 @@ gboolean GTKWindow::WindowEventHandlerThunk(GtkWidget* widget, GdkEvent* event, return window->WindowEventHandler(event); } +void GTKWindow::ApplyCursor() { + if (!drawing_area_) { + return; + } + GdkWindow* drawing_area_window = gtk_widget_get_window(drawing_area_); + if (!drawing_area_window) { + return; + } + GdkCursor* cursor = nullptr; + switch (GetCursorVisibility()) { + case CursorVisibility::kVisible: + cursor = nullptr; + break; + case CursorVisibility::kAutoHidden: + cursor = cursor_currently_auto_hidden_ ? blank_cursor_ : nullptr; + break; + case CursorVisibility::kHidden: + cursor = blank_cursor_; + break; + } + gdk_window_set_cursor(drawing_area_window, cursor); +} + +void GTKWindow::SetCursorAutoHideTimer() { + if (cursor_auto_hide_timer_) { + g_source_remove(cursor_auto_hide_timer_); + cursor_auto_hide_timer_ = 0; + } + cursor_auto_hide_timer_ = + g_timeout_add(kDefaultCursorAutoHideMilliseconds, AutoHideCursorTimerCallback, this); +} + +gboolean GTKWindow::AutoHideCursorTimerCallback(gpointer user_data) { + auto* gtk_window = reinterpret_cast(user_data); + gtk_window->cursor_auto_hide_timer_ = 0; + if (gtk_window->GetCursorVisibility() == CursorVisibility::kAutoHidden) { + gtk_window->cursor_currently_auto_hidden_ = true; + gtk_window->ApplyCursor(); + } + return G_SOURCE_REMOVE; +} + gboolean GTKWindow::DrawingAreaEventHandler(GdkEvent* event) { switch (event->type) { case GDK_MOTION_NOTIFY: case GDK_BUTTON_PRESS: case GDK_BUTTON_RELEASE: case GDK_SCROLL: { + if (GetCursorVisibility() == CursorVisibility::kAutoHidden) { + cursor_currently_auto_hidden_ = false; + ApplyCursor(); + SetCursorAutoHideTimer(); + } WindowDestructionReceiver destruction_receiver(this); HandleMouse(event, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) {