Repository: godotengine/godot (language: cpp)

Issue:
Embed Game focus over modal popups
### Tested versions

- Reproductible in 4.4 beta 2 and master

### System information

Godot v4.4.beta2 - Windows 11 (build 26100) - Multi-window, 1 monitor - Vulkan (Forward+) - dedicated NVIDIA GeForce RTX 3060 (NVIDIA; 32.0.15.6636) - 11th Gen Intel(R) Core(TM) i5-11600KF @ 3.90GHz (12 threads)

### Issue description

The embedding is enabled in the editor and the game is running. If a modal popup is visible and the use click on the game window, the game window appears in front of the modal popup. If this popup is small enough, it disappears and the editor is not available (ex: stop button).

https://github.com/user-attachments/assets/9ce74cc1-e7f0-44e8-87b8-fc60c69519e5



### Steps to reproduce

- Enable Embed Game and disable Make Floating
- Run the game
- While inside the Game tab, display a modal popup (ex: Settings, Create node, etc...)
- Click the game window

### Minimal reproduction project (MRP)

n/a


Discussion / hints:

Fix patch:
diff --git a/editor/plugins/embedded_process.cpp b/editor/plugins/embedded_process.cpp
index 1638c437125d..a348607a6e94 100644
--- a/editor/plugins/embedded_process.cpp
+++ b/editor/plugins/embedded_process.cpp
@@ -67,20 +67,10 @@ void EmbeddedProcess::_notification(int p_what) {
 		} break;
 		case NOTIFICATION_APPLICATION_FOCUS_IN: {
 			application_has_focus = true;
-			if (embedded_process_was_focused) {
-				embedded_process_was_focused = false;
-				// Refocus the embedded process if it was focused when the application lost focus,
-				// but do not refocus if the embedded process is currently focused (indicating it just lost focus)
-				// or if the current window is a different popup or secondary window.
-				if (embedding_completed && current_process_id != focused_process_id && window && window->has_focus()) {
-					grab_focus();
-					queue_update_embedded_process();
-				}
-			}
+			last_application_focus_time = OS::get_singleton()->get_ticks_msec();
 		} break;
 		case NOTIFICATION_APPLICATION_FOCUS_OUT: {
 			application_has_focus = false;
-			embedded_process_was_focused = embedding_completed && current_process_id == focused_process_id;
 		} break;
 	}
 }
@@ -286,14 +276,27 @@ void EmbeddedProcess::_check_mouse_over() {
 	// This method checks if the mouse is over the embedded process while the current application is focused.
 	// The goal is to give focus to the embedded process as soon as the mouse hovers over it,
 	// allowing the user to interact with it immediately without needing to click first.
-	if (!is_visible_in_tree() || !embedding_completed || !application_has_focus || !window || !window->has_focus() || Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || Input::get_singleton()->is_mouse_button_pressed(MouseButton::RIGHT)) {
+	if (!embedding_completed || !application_has_focus || !window || has_focus() || !is_visible_in_tree() || !window->has_focus() || Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT) || Input::get_singleton()->is_mouse_button_pressed(MouseButton::RIGHT)) {
+		return;
+	}
+
+	// Before checking whether the mouse is truly inside the embedded process, ensure
+	// the editor has enough time to re-render. When a breakpoint is hit in the script editor,
+	// `_check_mouse_over` may be triggered before the editor hides the game workspace.
+	// This prevents the embedded process from regaining focus immediately after the editor has taken it.
+	if (OS::get_singleton()->get_ticks_msec() - last_application_focus_time < 500) {
 		return;
 	}
 
-	bool focused = has_focus();
+	// Input::is_mouse_button_pressed is not sufficient to detect the mouse button state
+	// while the floating game window is being resized.
+	BitField<MouseButtonMask> mouse_button_mask = DisplayServer::get_singleton()->mouse_get_button_state();
+	if (!mouse_button_mask.is_empty()) {
+		return;
+	}
 
 	// Not stealing focus from a textfield.
-	if (!focused && get_viewport()->gui_get_focus_owner() && get_viewport()->gui_get_focus_owner()->is_text_field()) {
+	if (get_viewport()->gui_get_focus_owner() && get_viewport()->gui_get_focus_owner()->is_text_field()) {
 		return;
 	}
 
@@ -309,14 +312,17 @@ void EmbeddedProcess::_check_mouse_over() {
 		return;
 	}
 
-	// When we already have the focus and the user moves the mouse over the embedded process,
-	// we just need to refocus the process.
-	if (focused) {
-		queue_update_embedded_process();
-	} else {
-		grab_focus();
-		queue_redraw();
+	// When there's a modal window, we don't want to grab the focus to prevent
+	// the game window to go in front of the modal window.
+	if (_get_current_modal_window()) {
+		return;
 	}
+
+	// Force "regrabbing" the game window focus.
+	last_updated_embedded_process_focused = false;
+
+	grab_focus();
+	queue_redraw();
 }
 
 void EmbeddedProcess::_check_focused_process_id() {
@@ -325,17 +331,48 @@ void EmbeddedProcess::_check_focused_process_id() {
 		focused_process_id = process_id;
 		if (focused_process_id == current_process_id) {
 			// The embedded process got the focus.
-			emit_signal(SNAME("embedded_process_focused"));
-			if (has_focus()) {
-				// Redraw to updated the focus style.
-				queue_redraw();
-			} else {
-				grab_focus();
+
+			// Refocus the current model when focusing the embedded process.
+			Window *modal_window = _get_current_modal_window();
+			if (!modal_window) {
+				emit_signal(SNAME("embedded_process_focused"));
+				if (has_focus()) {
+					// Redraw to updated the focus style.
+					queue_redraw();
+				} else {
+					grab_focus();
+				}
 			}
 		} else if (has_focus()) {
 			release_focus();
 		}
 	}
+
+	// Ensure that the opened modal dialog is refocused when the focused process is the embedded process.
+	if (!application_has_focus && focused_process_id == current_process_id) {
+		Window *modal_window = _get_current_modal_window();
+		if (modal_window) {
+			if (modal_window->get_mode() == Window::MODE_MINIMIZED) {
+				modal_window->set_mode(Window::MODE_WINDOWED);
+			}
+			callable_mp(modal_window, &Window::grab_focus).call_deferred();
+		}
+	}
+}
+
+Window *EmbeddedProcess::_get_current_modal_window() {
+	Vector<DisplayServer::WindowID> wl = DisplayServer::get_singleton()->get_window_list();
+	for (const DisplayServer::WindowID &window_id : wl) {
+		Window *w = Window::get_from_id(window_id);
+		if (!w) {
+			continue;
+		}
+
+		if (w->is_exclusive()) {
+			return w;
+		}
+	}
+	return nullptr;
 }
 
 void EmbeddedProcess::_bind_methods() {
diff --git a/editor/plugins/embedded_process.h b/editor/plugins/embedded_process.h
index 3e800923a75d..6f56f255e618 100644
--- a/editor/plugins/embedded_process.h
+++ b/editor/plugins/embedded_process.h
@@ -37,7 +37,7 @@ class EmbeddedProcess : public Control {
 	GDCLASS(EmbeddedProcess, Control);
 
 	bool application_has_focus = true;
-	bool embedded_process_was_focused = false;
+	uint64_t last_application_focus_time = 0;
 	OS::ProcessID focused_process_id = 0;
 	OS::ProcessID current_process_id = 0;
 	bool embedding_grab_focus = false;
@@ -68,6 +68,7 @@ class EmbeddedProcess : public Control {
 	void _check_focused_process_id();
 	bool _is_embedded_process_updatable();
 	Rect2i _get_global_embedded_window_rect();
+	Window *_get_current_modal_window();
 
 protected:
 	static void _bind_methods();
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index d0ddac4b7f9e..1a407bdd4b2f 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -2986,7 +2986,8 @@ Error DisplayServerWindows::embed_process(WindowID p_window, OS::ProcessID p_pid
 	// (e.g., a screen to the left of the main screen).
 	const Rect2i adjusted_rect = Rect2i(p_rect.position + _get_screens_origin(), p_rect.size);
 
-	SetWindowPos(ep->window_handle, nullptr, adjusted_rect.position.x, adjusted_rect.position.y, adjusted_rect.size.x, adjusted_rect.size.y, SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
+	// Use HWND_BOTTOM to prevent reordering of the embedded window over another popup.
+	SetWindowPos(ep->window_handle, HWND_BOTTOM, adjusted_rect.position.x, adjusted_rect.position.y, adjusted_rect.size.x, adjusted_rect.size.y, SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
 
 	if (ep->is_visible != p_visible) {
 		if (p_visible) {


Test patch: