From 101fbbbd1fa195bdf737276c74ecdf40548b9e78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pa=CC=84vels=20Nadtoc=CC=8Cajevs?=
<7645683+bruvzg@users.noreply.github.com>
Date: Sun, 5 Oct 2025 16:53:58 +0300
Subject: [PATCH] [Window] Add unfiltered input handler signal for custom
decorations.
---
doc/classes/Window.xml | 9 +++
editor/gui/editor_title_bar.cpp | 108 ++++++++++++++++++--------------
scene/main/window.cpp | 20 ++++++
scene/main/window.h | 5 ++
scene/scene_string_names.h | 1 +
5 files changed, 96 insertions(+), 47 deletions(-)
diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml
index 85b502544ec..b7baab7b256 100644
--- a/doc/classes/Window.xml
+++ b/doc/classes/Window.xml
@@ -691,6 +691,9 @@
[b]Note:[/b] On Windows, the portion of a window that lies outside the region is not drawn, while on Linux (X11) and macOS it is.
[b]Note:[/b] This property is implemented on Linux (X11), macOS and Windows.
+
+ If set, defines the window's custom decoration area which will receive mouse input, even if normal input to the window is blocked (such as when it has an exclusive child opened). See also [signal nonclient_window_input].
+
If [code]true[/code], the [Window] will be considered a popup. Popups are sub-windows that don't show as separate windows in system's window manager's window list and will send close request when anything is clicked outside of them (unless [member exclusive] is enabled).
@@ -803,6 +806,12 @@
Emitted when the mouse cursor leaves the [Window]'s visible area, that is not occluded behind other [Control]s or windows, provided its [member Viewport.gui_disable_input] is [code]false[/code] and regardless if it's currently focused or not.
+
+
+
+ Emitted when the mouse event is received by the custom decoration area defined by [member nonclient_area], and normal input to the window is blocked (such as when it has an exclusive child opened). [param event]'s position is in the embedder's coordinate system.
+
+
Emitted when the [constant NOTIFICATION_THEME_CHANGED] notification is sent.
diff --git a/editor/gui/editor_title_bar.cpp b/editor/gui/editor_title_bar.cpp
index 19bb2fd7ea8..145d85b3c8d 100644
--- a/editor/gui/editor_title_bar.cpp
+++ b/editor/gui/editor_title_bar.cpp
@@ -89,57 +89,71 @@ Control *EditorTitleBar::get_center_control() const {
}
void EditorTitleBar::_notification(int p_what) {
- if (!center_control || p_what != NOTIFICATION_SORT_CHILDREN) {
- return;
- }
-
- Control *prev = nullptr;
- Control *base = nullptr;
- Control *next = nullptr;
-
- bool rtl = is_layout_rtl();
-
- int start;
- int end;
- int delta;
- if (rtl) {
- start = get_child_count() - 1;
- end = -1;
- delta = -1;
- } else {
- start = 0;
- end = get_child_count();
- delta = +1;
- }
-
- for (int i = start; i != end; i += delta) {
- Control *c = as_sortable_control(get_child(i));
- if (!c) {
- continue;
+ switch (p_what) {
+ case NOTIFICATION_EXIT_TREE: {
+ SceneTree::get_singleton()->get_root()->disconnect(SceneStringName(nonclient_window_input), callable_mp(this, &EditorTitleBar::gui_input));
+ get_window()->set_nonclient_area(Rect2i());
+ } break;
+ case NOTIFICATION_ENTER_TREE: {
+ SceneTree::get_singleton()->get_root()->connect(SceneStringName(nonclient_window_input), callable_mp(this, &EditorTitleBar::gui_input));
+ [[fallthrough]];
}
- if (base) {
- next = c;
- break;
- }
- if (c != center_control) {
- prev = c;
- continue;
- }
- base = c;
- }
- if (base && prev && next) {
- Size2i title_size = get_size();
- Size2i c_size = base->get_combined_minimum_size();
+ case NOTIFICATION_RESIZED: {
+ get_window()->set_nonclient_area(get_global_transform().xform(Rect2i(get_position(), get_size())));
+ } break;
+ case NOTIFICATION_SORT_CHILDREN: {
+ if (!center_control) {
+ break;
+ }
+ Control *prev = nullptr;
+ Control *base = nullptr;
+ Control *next = nullptr;
- int min_offset = prev->get_position().x + prev->get_combined_minimum_size().x;
- int max_offset = next->get_position().x + next->get_size().x - next->get_combined_minimum_size().x - c_size.x;
+ bool rtl = is_layout_rtl();
- int offset = (title_size.width - c_size.width) / 2;
- offset = CLAMP(offset, min_offset, max_offset);
+ int start;
+ int end;
+ int delta;
+ if (rtl) {
+ start = get_child_count() - 1;
+ end = -1;
+ delta = -1;
+ } else {
+ start = 0;
+ end = get_child_count();
+ delta = +1;
+ }
- fit_child_in_rect(prev, Rect2i(prev->get_position().x, 0, offset - prev->get_position().x, title_size.height));
- fit_child_in_rect(base, Rect2i(offset, 0, c_size.width, title_size.height));
- fit_child_in_rect(next, Rect2i(offset + c_size.width, 0, next->get_position().x + next->get_size().x - (offset + c_size.width), title_size.height));
+ for (int i = start; i != end; i += delta) {
+ Control *c = as_sortable_control(get_child(i));
+ if (!c) {
+ continue;
+ }
+ if (base) {
+ next = c;
+ break;
+ }
+ if (c != center_control) {
+ prev = c;
+ continue;
+ }
+ base = c;
+ }
+ if (base && prev && next) {
+ Size2i title_size = get_size();
+ Size2i c_size = base->get_combined_minimum_size();
+
+ int min_offset = prev->get_position().x + prev->get_combined_minimum_size().x;
+ int max_offset = next->get_position().x + next->get_size().x - next->get_combined_minimum_size().x - c_size.x;
+
+ int offset = (title_size.width - c_size.width) / 2;
+ offset = CLAMP(offset, min_offset, max_offset);
+
+ fit_child_in_rect(prev, Rect2i(prev->get_position().x, 0, offset - prev->get_position().x, title_size.height));
+ fit_child_in_rect(base, Rect2i(offset, 0, c_size.width, title_size.height));
+ fit_child_in_rect(next, Rect2i(offset + c_size.width, 0, next->get_position().x + next->get_size().x - (offset + c_size.width), title_size.height));
+ }
+ } break;
}
}
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index 5d1694973cf..6039c1e4d7c 100644
--- a/scene/main/window.cpp
+++ b/scene/main/window.cpp
@@ -1726,6 +1726,15 @@ real_t Window::get_content_scale_factor() const {
return content_scale_factor;
}
+void Window::set_nonclient_area(const Rect2i &p_rect) {
+ ERR_MAIN_THREAD_GUARD;
+ nonclient_area = p_rect;
+}
+
+Rect2i Window::get_nonclient_area() const {
+ return nonclient_area;
+}
+
DisplayServer::WindowID Window::get_window_id() const {
ERR_READ_THREAD_GUARD_V(DisplayServer::INVALID_WINDOW_ID);
if (get_embedder()) {
@@ -1816,6 +1825,12 @@ void Window::_window_input(const Ref &p_ev) {
ERR_MAIN_THREAD_GUARD;
if (exclusive_child != nullptr) {
+ if (nonclient_area.has_area() && is_inside_tree()) {
+ Ref me = p_ev;
+ if (me.is_valid() && nonclient_area.has_point(me->get_position())) {
+ emit_signal(SceneStringName(nonclient_window_input), p_ev);
+ }
+ }
if (!is_embedding_subwindows()) { // Not embedding, no need for event.
return;
}
@@ -3227,6 +3242,9 @@ void Window::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_content_scale_stretch", "stretch"), &Window::set_content_scale_stretch);
ClassDB::bind_method(D_METHOD("get_content_scale_stretch"), &Window::get_content_scale_stretch);
+ ClassDB::bind_method(D_METHOD("set_nonclient_area", "area"), &Window::set_nonclient_area);
+ ClassDB::bind_method(D_METHOD("get_nonclient_area"), &Window::get_nonclient_area);
+
ClassDB::bind_method(D_METHOD("set_keep_title_visible", "title_visible"), &Window::set_keep_title_visible);
ClassDB::bind_method(D_METHOD("get_keep_title_visible"), &Window::get_keep_title_visible);
@@ -3331,6 +3349,7 @@ void Window::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "position", PROPERTY_HINT_NONE, "suffix:px"), "set_position", "get_position");
ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "size", PROPERTY_HINT_NONE, "suffix:px"), "set_size", "get_size");
ADD_PROPERTY(PropertyInfo(Variant::INT, "current_screen", PROPERTY_HINT_RANGE, "0,64,1,or_greater"), "set_current_screen", "get_current_screen");
+ ADD_PROPERTY(PropertyInfo(Variant::RECT2I, "nonclient_area", PROPERTY_HINT_NONE, ""), "set_nonclient_area", "get_nonclient_area");
ADD_PROPERTY(PropertyInfo(Variant::PACKED_VECTOR2_ARRAY, "mouse_passthrough_polygon"), "set_mouse_passthrough_polygon", "get_mouse_passthrough_polygon");
@@ -3380,6 +3399,7 @@ void Window::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::STRING, "theme_type_variation", PROPERTY_HINT_ENUM_SUGGESTION), "set_theme_type_variation", "get_theme_type_variation");
ADD_SIGNAL(MethodInfo("window_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent")));
+ ADD_SIGNAL(MethodInfo("nonclient_window_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent")));
ADD_SIGNAL(MethodInfo("files_dropped", PropertyInfo(Variant::PACKED_STRING_ARRAY, "files")));
ADD_SIGNAL(MethodInfo("mouse_entered"));
ADD_SIGNAL(MethodInfo("mouse_exited"));
diff --git a/scene/main/window.h b/scene/main/window.h
index 25ce600e140..4c25e35a5f4 100644
--- a/scene/main/window.h
+++ b/scene/main/window.h
@@ -171,6 +171,8 @@ private:
Size2i max_size_used;
+ Rect2i nonclient_area;
+
Size2i _clamp_limit_size(const Size2i &p_limit_size);
Size2i _clamp_window_size(const Size2i &p_size);
void _validate_limit_size();
@@ -391,6 +393,9 @@ public:
void set_content_scale_factor(real_t p_factor);
real_t get_content_scale_factor() const;
+ void set_nonclient_area(const Rect2i &p_rect);
+ Rect2i get_nonclient_area() const;
+
void set_mouse_passthrough_polygon(const Vector &p_region);
Vector get_mouse_passthrough_polygon() const;
diff --git a/scene/scene_string_names.h b/scene/scene_string_names.h
index 0743025421a..355e7889f0d 100644
--- a/scene/scene_string_names.h
+++ b/scene/scene_string_names.h
@@ -53,6 +53,7 @@ public:
const StringName input_event = "input_event";
const StringName gui_input = "gui_input";
const StringName window_input = "window_input";
+ const StringName nonclient_window_input = "nonclient_window_input";
const StringName tree_entered = "tree_entered";
const StringName tree_exiting = "tree_exiting";