diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml
index 37bf265d20c4..4b82f67ff530 100644
--- a/doc/classes/DisplayServer.xml
+++ b/doc/classes/DisplayServer.xml
@@ -1511,6 +1511,7 @@
Returns left margins ([code]x[/code]), right margins ([code]y[/code]) and height ([code]z[/code]) of the title that are safe to use (contains no buttons or other elements) when [constant WINDOW_FLAG_EXTEND_TO_TITLE] flag is set.
+ [b]Note:[/b] On Linux and Windows, this method always returns [constant Vector2.ZERO], since the decoration buttons are not displayed by the OS when the [constant WINDOW_FLAG_EXTEND_TO_TITLE] flag is set.
@@ -1873,7 +1874,7 @@
Display server supports text-to-speech. See [code]tts_*[/code] methods. [b]Windows, macOS, Linux (X11/Wayland), Android, iOS, Web[/b]
- Display server supports expanding window content to the title. See [constant WINDOW_FLAG_EXTEND_TO_TITLE]. [b]macOS[/b]
+ Display server supports expanding window content to the title. See [constant WINDOW_FLAG_EXTEND_TO_TITLE]. [b]macOS, Linux, Windows[/b]
Display server supports reading screen pixels. See [method screen_get_pixel].
@@ -2094,7 +2095,8 @@
Window content is expanded to the full size of the window. Unlike borderless window, the frame is left intact and can be used to resize the window, title bar is transparent, but have minimize/maximize/close buttons.
Use [method window_set_window_buttons_offset] to adjust minimize/maximize/close buttons offset.
Use [method window_get_safe_title_margins] to determine area under the title bar that is not covered by decorations.
- [b]Note:[/b] This flag is implemented only on macOS.
+ [b]Note:[/b] This flag is implemented on macOS, Linux, and Windows.
+ [b]Note:[/b] On Linux and Windows, the decoration buttons (Minimize, Maximize and Close) are rendered by Godot and use the theme of the Window.
All mouse events are passed to the underlying window of the same application.
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index a5097521dc45..791eb538015a 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -782,7 +782,7 @@
Expanding main editor window content to the title, if supported by [DisplayServer]. See [constant DisplayServer.WINDOW_FLAG_EXTEND_TO_TITLE].
- Specific to the macOS platform.
+ [b]Note:[/b] This setting is implemented on macOS, Linux, and Windows.
If set to [code]true[/code], MSDF font rendering will be used for the visual shader graph editor. You may need to set this to [code]false[/code] when using a custom main font, as some fonts will look broken due to the use of self-intersecting outlines in their font data. Downloading the font from the font maker's official website as opposed to a service like Google Fonts can help resolve this issue.
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 4266bab2a175..34e3da4be251 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -846,7 +846,9 @@
Main window content is expanded to the full size of the window. Unlike a borderless window, the frame is left intact and can be used to resize the window, and the title bar is transparent, but has minimize/maximize/close buttons.
- [b]Note:[/b] This setting is implemented only on macOS.
+ [b]Note:[/b] This setting is implemented on macOS, Linux, and Windows.
+ [b]Note:[/b] This setting only works if [member display/window/subwindows/embed_subwindows] is [code]false[/code].
+ [b]Note:[/b] On Linux and Windows, the decoration buttons (Minimize, Maximize and Close) are rendered by Godot and use the theme of the Window.
Main window initial position (in virtual desktop coordinates), this setting is used only if [member display/window/size/initial_position_type] is set to "Absolute" ([code]0[/code]).
diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml
index ca155881c8ed..7050b19081f7 100644
--- a/doc/classes/Window.xml
+++ b/doc/classes/Window.xml
@@ -592,7 +592,7 @@
If [code]true[/code], the [Window] contents is expanded to the full size of the window, window title bar is transparent.
- [b]Note:[/b] This property is implemented only on macOS.
+ [b]Note:[/b] This property is implemented on macOS, Linux, and Windows.
[b]Note:[/b] This property only works with native windows.
@@ -835,8 +835,9 @@
Window content is expanded to the full size of the window. Unlike borderless window, the frame is left intact and can be used to resize the window, title bar is transparent, but have minimize/maximize/close buttons. Set with [member extend_to_title].
- [b]Note:[/b] This flag is implemented only on macOS.
+ [b]Note:[/b] This flag is implemented on macOS, Linux, and Windows.
[b]Note:[/b] This flag has no effect in embedded windows.
+ [b]Note:[/b] On Linux and Windows, the decoration buttons (Minimize, Maximize and Close) are rendered by Godot and use the Window theme.
All mouse events are passed to the underlying window of the same application.
@@ -907,6 +908,15 @@
+
+ Window decoration buttons modulation color, when the buttons are hovered, [member extend_to_title] is true and the buttons are rendered by Godot.
+
+
+ Window decoration buttons modulation color, when [member extend_to_title] is true and the buttons are rendered by Godot.
+
+
+ Window decoration buttons modulation color, when the buttons are pressed, [member extend_to_title] is true and the buttons are rendered by Godot.
+
The color of the title's text.
@@ -916,9 +926,21 @@
Horizontal position offset of the close button.
-
+
Vertical position offset of the close button.
+
+ Horizontal position offset of the maximize/restore decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ Vertical position offset of the maximize/restore decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ Horizontal position offset of the minimize decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ Vertical position offset of the minimize decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
Defines the outside margin at which the window border can be grabbed with mouse and resized.
@@ -940,6 +962,36 @@
The icon for the close button when it's being pressed.
+
+ The icon for the maximize decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The icon for the maximize decoration button when disabled (window not resizable), [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The icon for the maximize decoration button when it's being pressed, [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The icon for the minimize decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The icon for the minimize decoration button when it's being pressed, [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The icon for the restore decoration button, when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The icon for the restore decoration button when it's being pressed, [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The background style used of the window decoration buttons when the mouse pointer is hovering them, [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The background style used of the window decoration buttons when [member extend_to_title] is true and the button is rendered by Godot.
+
+
+ The background style used of the window decoration buttons when they are being pressed, [member extend_to_title] is true and the button is rendered by Godot.
+
The background style used when the [Window] is embedded. Note that this is drawn only under the window's content, excluding the title. For proper borders and title bar style, you can use [code]expand_margin_*[/code] properties of [StyleBoxFlat].
[b]Note:[/b] The content background will not be visible unless [member transparent] is enabled.
diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp
index 2853ebc4994c..e387f7d3aaf2 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -544,6 +544,7 @@ void EditorNode::_update_theme(bool p_skip_creation) {
}
_update_renderer_color();
+ _titlebar_resized();
}
editor_dock_manager->update_tab_styles();
@@ -650,7 +651,7 @@ void EditorNode::_notification(int p_what) {
Window *window = get_window();
if (window) {
- // Handle macOS fullscreen and extend-to-title changes.
+ // Handle fullscreen and extend-to-title changes.
window->connect("titlebar_changed", callable_mp(this, &EditorNode::_titlebar_resized));
}
@@ -1239,18 +1240,27 @@ void EditorNode::_viewport_resized() {
}
void EditorNode::_titlebar_resized() {
- DisplayServer::get_singleton()->window_set_window_buttons_offset(Vector2i(title_bar->get_global_position().y + title_bar->get_size().y / 2, title_bar->get_global_position().y + title_bar->get_size().y / 2), DisplayServer::MAIN_WINDOW_ID);
- const Vector3i &margin = DisplayServer::get_singleton()->window_get_safe_title_margins(DisplayServer::MAIN_WINDOW_ID);
+ Window *w = get_window();
+ if (!w) {
+ return;
+ }
+
+ // On Windows and Linux, we will let the window button in the corner, same as the
+ // default OS buttons.
+#ifdef MACOS_ENABLED
+ w->set_window_buttons_offset(Vector2i(title_bar->get_global_position().y + title_bar->get_size().y / 2, title_bar->get_global_position().y + title_bar->get_size().y / 2));
+#endif
+
+ const Vector2i &left_margin = w->get_safe_title_margins_left();
+ const Vector2i &right_margin = w->get_safe_title_margins_right();
if (left_menu_spacer) {
- int w = (gui_base->is_layout_rtl()) ? margin.y : margin.x;
- left_menu_spacer->set_custom_minimum_size(Size2(w, 0));
+ left_menu_spacer->set_custom_minimum_size(Size2(left_margin.x, 0));
}
if (right_menu_spacer) {
- int w = (gui_base->is_layout_rtl()) ? margin.x : margin.y;
- right_menu_spacer->set_custom_minimum_size(Size2(w, 0));
+ right_menu_spacer->set_custom_minimum_size(Size2(right_margin.x, 0));
}
if (title_bar) {
- title_bar->set_custom_minimum_size(Size2(0, margin.z - title_bar->get_global_position().y));
+ title_bar->set_custom_minimum_size(Size2(0, MAX(left_margin.y, right_margin.y) - title_bar->get_global_position().y));
}
}
diff --git a/editor/icons/WindowClose.svg b/editor/icons/WindowClose.svg
new file mode 100644
index 000000000000..5b11ab0b7b4c
--- /dev/null
+++ b/editor/icons/WindowClose.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/WindowMaximize.svg b/editor/icons/WindowMaximize.svg
new file mode 100644
index 000000000000..778e05c8b247
--- /dev/null
+++ b/editor/icons/WindowMaximize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/WindowMinimize.svg b/editor/icons/WindowMinimize.svg
new file mode 100644
index 000000000000..610b69d69362
--- /dev/null
+++ b/editor/icons/WindowMinimize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/WindowRestore.svg b/editor/icons/WindowRestore.svg
new file mode 100644
index 000000000000..230717a87fe7
--- /dev/null
+++ b/editor/icons/WindowRestore.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp
index 30878a248809..4ad79b0fa56c 100644
--- a/editor/project_manager.cpp
+++ b/editor/project_manager.cpp
@@ -1053,20 +1053,27 @@ void ProjectManager::_files_dropped(PackedStringArray p_files) {
}
void ProjectManager::_titlebar_resized() {
- DisplayServer::get_singleton()->window_set_window_buttons_offset(Vector2i(title_bar->get_global_position().y + title_bar->get_size().y / 2, title_bar->get_global_position().y + title_bar->get_size().y / 2), DisplayServer::MAIN_WINDOW_ID);
- const Vector3i &margin = DisplayServer::get_singleton()->window_get_safe_title_margins(DisplayServer::MAIN_WINDOW_ID);
+ Window *w = get_window();
+ if (!w) {
+ return;
+ }
+
+ // On Windows and Linux, we will let the window button in the corner, same as the
+ // default OS buttons.
+#ifdef MACOS_ENABLED
+ w->set_window_buttons_offset(Vector2i(title_bar->get_global_position().y + title_bar->get_size().y / 2, title_bar->get_global_position().y + title_bar->get_size().y / 2));
+#endif
+
+ const Vector2i &left_margin = w->get_safe_title_margins_left();
+ const Vector2i &right_margin = w->get_safe_title_margins_right();
if (left_menu_spacer) {
- int w = (root_container->is_layout_rtl()) ? margin.y : margin.x;
- left_menu_spacer->set_custom_minimum_size(Size2(w, 0));
- right_spacer->set_custom_minimum_size(Size2(w, 0));
+ left_menu_spacer->set_custom_minimum_size(Size2(left_margin.x, 0));
}
if (right_menu_spacer) {
- int w = (root_container->is_layout_rtl()) ? margin.x : margin.y;
- right_menu_spacer->set_custom_minimum_size(Size2(w, 0));
- left_spacer->set_custom_minimum_size(Size2(w, 0));
+ right_menu_spacer->set_custom_minimum_size(Size2(right_margin.x, 0));
}
if (title_bar) {
- title_bar->set_custom_minimum_size(Size2(0, margin.z - title_bar->get_global_position().y));
+ title_bar->set_custom_minimum_size(Size2(0, MAX(left_margin.y, right_margin.y) - title_bar->get_global_position().y));
}
}
diff --git a/editor/themes/editor_theme_manager.cpp b/editor/themes/editor_theme_manager.cpp
index 17bcbacfc204..2513bd55b50f 100644
--- a/editor/themes/editor_theme_manager.cpp
+++ b/editor/themes/editor_theme_manager.cpp
@@ -1267,19 +1267,52 @@ void EditorThemeManager::_populate_standard_styles(const Ref &p_the
// Window and dialogs.
{
// Window.
-
p_theme->set_stylebox("embedded_border", "Window", p_config.window_style);
p_theme->set_stylebox("embedded_unfocused_border", "Window", p_config.window_style);
+ p_theme->set_font("title_font", "Window", p_theme->get_font(SNAME("title"), EditorStringName(EditorFonts)));
p_theme->set_color("title_color", "Window", p_config.font_color);
- p_theme->set_icon("close", "Window", p_theme->get_icon(SNAME("GuiClose"), EditorStringName(EditorIcons)));
- p_theme->set_icon("close_pressed", "Window", p_theme->get_icon(SNAME("GuiClose"), EditorStringName(EditorIcons)));
- p_theme->set_constant("close_h_offset", "Window", 22 * EDSCALE);
- p_theme->set_constant("close_v_offset", "Window", 20 * EDSCALE);
p_theme->set_constant("title_height", "Window", 24 * EDSCALE);
- p_theme->set_constant("resize_margin", "Window", 4 * EDSCALE);
- p_theme->set_font("title_font", "Window", p_theme->get_font(SNAME("title"), EditorStringName(EditorFonts)));
p_theme->set_font_size("title_font_size", "Window", p_theme->get_font_size(SNAME("title_size"), EditorStringName(EditorFonts)));
+ p_theme->set_constant("resize_margin", "Window", 4 * EDSCALE);
+
+ Ref decoration_button_normal = p_config.button_style->duplicate();
+ decoration_button_normal->set_bg_color(Color(1, 1, 1, 0));
+ decoration_button_normal->set_content_margin_all(0);
+ decoration_button_normal->set_corner_radius_all(0);
+ p_theme->set_stylebox("decoration_button_normal", "Window", decoration_button_normal);
+
+ Ref decoration_button_hover = p_config.button_style_hover->duplicate();
+ decoration_button_hover->set_content_margin_all(0);
+ decoration_button_hover->set_corner_radius_all(0);
+ p_theme->set_stylebox("decoration_button_hover", "Window", decoration_button_hover);
+
+ Ref decoration_button_pressed = p_config.button_style_pressed->duplicate();
+ decoration_button_pressed->set_content_margin_all(0);
+ decoration_button_pressed->set_corner_radius_all(0);
+ p_theme->set_stylebox("decoration_button_pressed", "Window", decoration_button_pressed);
+
+ p_theme->set_color("decoration_button_normal_modulate", "Window", p_config.font_color);
+ p_theme->set_color("decoration_button_hover_modulate", "Window", p_config.font_hover_color);
+ p_theme->set_color("decoration_button_pressed_modulate", "Window", p_config.font_pressed_color);
+
+ float window_button_width = MIN(20 + (p_config.widget_margin.x * 2), 32);
+ float window_button_height = MIN(20 + (p_config.widget_margin.y * 2), 32);
+ p_theme->set_icon("minimize", "Window", p_theme->get_icon(SNAME("WindowMinimize"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("minimize_pressed", "Window", p_theme->get_icon(SNAME("WindowMinimize"), EditorStringName(EditorIcons)));
+ p_theme->set_constant("minimize_h_offset", "Window", window_button_width * 3);
+ p_theme->set_constant("minimize_v_offset", "Window", window_button_height);
+ p_theme->set_icon("maximize", "Window", p_theme->get_icon(SNAME("WindowMaximize"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("maximize_pressed", "Window", p_theme->get_icon(SNAME("WindowMaximize"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("maximize_disabled", "Window", p_theme->get_icon(SNAME("WindowMaximize"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("restore", "Window", p_theme->get_icon(SNAME("WindowRestore"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("restore_pressed", "Window", p_theme->get_icon(SNAME("WindowRestore"), EditorStringName(EditorIcons)));
+ p_theme->set_constant("maximize_h_offset", "Window", window_button_width * 2);
+ p_theme->set_constant("maximize_v_offset", "Window", window_button_height);
+ p_theme->set_icon("close", "Window", p_theme->get_icon(SNAME("WindowClose"), EditorStringName(EditorIcons)));
+ p_theme->set_icon("close_pressed", "Window", p_theme->get_icon(SNAME("WindowClose"), EditorStringName(EditorIcons)));
+ p_theme->set_constant("close_h_offset", "Window", window_button_width);
+ p_theme->set_constant("close_v_offset", "Window", window_button_height);
// AcceptDialog.
p_theme->set_stylebox(SceneStringName(panel), "AcceptDialog", p_config.dialog_style);
diff --git a/main/main.cpp b/main/main.cpp
index 5206e9b84c57..12ef32792e0c 100644
--- a/main/main.cpp
+++ b/main/main.cpp
@@ -2735,6 +2735,9 @@ Error Main::setup2(bool p_show_boot_logo) {
EditorPaths::create();
+ // The default value of the editor setting 'interface/editor/expand_to_title' is true.
+ bool expand_to_title = true;
+
// Editor setting class is not available, load config directly.
if (!init_use_custom_screen && (editor || project_manager) && EditorPaths::get_singleton()->are_paths_valid()) {
ERR_FAIL_COND_V(!DirAccess::dir_exists_absolute(EditorPaths::get_singleton()->get_config_dir()), FAILED);
@@ -2763,6 +2766,7 @@ Error Main::setup2(bool p_show_boot_logo) {
bool prefer_wayland_found = false;
bool prefer_wayland = false;
+ bool extend_to_title_found = false;
if (editor) {
screen_property = "interface/editor/editor_screen";
@@ -2778,7 +2782,7 @@ Error Main::setup2(bool p_show_boot_logo) {
prefer_wayland_found = true;
}
- while (!screen_found || !prefer_wayland_found) {
+ while (!screen_found || !prefer_wayland_found || !extend_to_title_found) {
assign = Variant();
next_tag.fields.clear();
next_tag.name = String();
@@ -2802,6 +2806,10 @@ Error Main::setup2(bool p_show_boot_logo) {
prefer_wayland = value;
prefer_wayland_found = true;
}
+
+ if (!extend_to_title_found && assign == "interface/editor/expand_to_title") {
+ expand_to_title = value;
+ }
}
}
@@ -2816,6 +2824,10 @@ Error Main::setup2(bool p_show_boot_logo) {
}
}
+ if (expand_to_title) {
+ window_flags |= DisplayServer::WINDOW_FLAG_EXTEND_TO_TITLE_BIT;
+ }
+
if (found_project && EditorPaths::get_singleton()->is_self_contained()) {
if (ProjectSettings::get_singleton()->get_resource_path() == OS::get_singleton()->get_executable_path().get_base_dir()) {
ERR_PRINT("You are trying to run a self-contained editor at the same location as a project. This is not allowed, since editor files will mix with project files.");
diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp
index 293623e594f3..78153a6de098 100644
--- a/platform/linuxbsd/x11/display_server_x11.cpp
+++ b/platform/linuxbsd/x11/display_server_x11.cpp
@@ -139,6 +139,7 @@ bool DisplayServerX11::has_feature(Feature p_feature) const {
#endif
case FEATURE_CLIPBOARD_PRIMARY:
case FEATURE_TEXT_TO_SPEECH:
+ case FEATURE_EXTEND_TO_TITLE:
return true;
case FEATURE_SCREEN_CAPTURE:
return !xwayland;
@@ -2214,7 +2215,7 @@ void DisplayServerX11::window_set_position(const Point2i &p_position, WindowID p
int x = 0;
int y = 0;
- if (!window_get_flag(WINDOW_FLAG_BORDERLESS, p_window)) {
+ if (!window_get_flag(WINDOW_FLAG_BORDERLESS, p_window) && !window_get_flag(WINDOW_FLAG_EXTEND_TO_TITLE, p_window)) {
//exclude window decorations
XSync(x11_display, False);
Atom prop = XInternAtom(x11_display, "_NET_FRAME_EXTENTS", True);
@@ -2640,7 +2641,7 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled, boo
ERR_FAIL_COND(!windows.has(p_window));
WindowData &wd = windows[p_window];
- if (p_enabled && !window_get_flag(WINDOW_FLAG_BORDERLESS, p_window)) {
+ if (p_enabled && !window_get_flag(WINDOW_FLAG_BORDERLESS, p_window) && !window_get_flag(WINDOW_FLAG_EXTEND_TO_TITLE, p_window)) {
// remove decorations if the window is not already borderless
Hints hints;
Atom property;
@@ -2697,7 +2698,7 @@ void DisplayServerX11::_set_wm_fullscreen(WindowID p_window, bool p_enabled, boo
Hints hints;
Atom property;
hints.flags = 2;
- hints.decorations = wd.borderless ? 0 : 1;
+ hints.decorations = wd.borderless || wd.extend_to_title ? 0 : 1;
property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True);
if (property != None) {
XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5);
@@ -2775,6 +2776,9 @@ void DisplayServerX11::window_set_mode(WindowMode p_mode, WindowID p_window) {
_set_wm_maximized(p_window, true);
} break;
}
+
+ // Notify a possible change in the titlebar.
+ _send_window_event(wd, DisplayServerX11::WINDOW_EVENT_TITLEBAR_CHANGE);
}
DisplayServer::WindowMode DisplayServerX11::window_get_mode(WindowID p_window) const {
@@ -2883,6 +2887,24 @@ void DisplayServerX11::window_set_flag(WindowFlags p_flag, bool p_enabled, Windo
ERR_FAIL_COND_MSG((xwa.map_state == IsViewable) && (wd.is_popup != p_enabled), "Popup flag can't changed while window is opened.");
wd.is_popup = p_enabled;
} break;
+ case WINDOW_FLAG_EXTEND_TO_TITLE: {
+ Hints hints;
+ Atom property;
+ hints.flags = 2;
+ hints.decorations = p_enabled ? 0 : 1;
+ property = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True);
+ if (property != None) {
+ XChangeProperty(x11_display, wd.x11_window, property, property, 32, PropModeReplace, (unsigned char *)&hints, 5);
+ }
+
+ // Preserve window size
+ window_set_size(window_get_size(p_window), p_window);
+
+ wd.extend_to_title = p_enabled;
+
+ // Notify a possible change in the titlebar.
+ _send_window_event(wd, DisplayServerX11::WINDOW_EVENT_TITLEBAR_CHANGE);
+ } break;
default: {
}
}
@@ -2899,24 +2921,7 @@ bool DisplayServerX11::window_get_flag(WindowFlags p_flag, WindowID p_window) co
return wd.resize_disabled;
} break;
case WINDOW_FLAG_BORDERLESS: {
- bool borderless = wd.borderless;
- Atom prop = XInternAtom(x11_display, "_MOTIF_WM_HINTS", True);
- if (prop != None) {
- Atom type;
- int format;
- unsigned long len;
- unsigned long remaining;
- unsigned char *data = nullptr;
- if (XGetWindowProperty(x11_display, wd.x11_window, prop, 0, sizeof(Hints), False, AnyPropertyType, &type, &format, &len, &remaining, &data) == Success) {
- if (data && (format == 32) && (len >= 5)) {
- borderless = !(reinterpret_cast(data)->decorations);
- }
- if (data) {
- XFree(data);
- }
- }
- }
- return borderless;
+ return wd.borderless;
} break;
case WINDOW_FLAG_ALWAYS_ON_TOP: {
return wd.on_top;
@@ -2933,6 +2938,10 @@ bool DisplayServerX11::window_get_flag(WindowFlags p_flag, WindowID p_window) co
case WINDOW_FLAG_POPUP: {
return wd.is_popup;
} break;
+ case WINDOW_FLAG_EXTEND_TO_TITLE: {
+ return wd.extend_to_title;
+ } break;
+
default: {
}
}
@@ -4780,6 +4789,28 @@ void DisplayServerX11::process_events() {
event.xbutton.y = last_mouse_pos.y;
}
+ WindowData &wd = windows[window_id];
+
+ // Handle left mouse button press and release to resize the window in extend_to_title mode.
+ if (wd.extend_to_title && !wd.maximized && !wd.minimized && !wd.resize_disabled && mouse_mode == MOUSE_MODE_VISIBLE && (MouseButton)event.xbutton.button == MouseButton::LEFT) {
+ if (event.type == ButtonPress) {
+ int resize_edge = _detect_resize_edge(event.xbutton.x, event.xbutton.y, wd);
+ if (resize_edge != RESIZE_EDGE_NONE) {
+ wd.resize_edge = resize_edge;
+ wd.resize_origin_mouse_x = event.xbutton.x_root;
+ wd.resize_origin_mouse_y = event.xbutton.y_root;
+ wd.resize_origin_width = wd.size.x;
+ wd.resize_origin_height = wd.size.y;
+ wd.resize_origin_position_x = wd.position.x;
+ wd.resize_origin_position_y = wd.position.y;
+ break;
+ }
+ } else if (wd.resize_edge != RESIZE_EDGE_NONE) {
+ wd.resize_edge = RESIZE_EDGE_NONE;
+ break;
+ }
+ }
+
Ref mb;
mb.instantiate();
@@ -4805,8 +4836,6 @@ void DisplayServerX11::process_events() {
mb->set_button_mask(mouse_get_button_state());
}
- const WindowData &wd = windows[window_id];
-
if (event.type == ButtonPress) {
DEBUG_LOG_X11("[%u] ButtonPress window=%lu (%u), button_index=%u \n", frame, event.xbutton.window, window_id, mb->get_button_index());
@@ -4882,6 +4911,19 @@ void DisplayServerX11::process_events() {
if (ime_window_event || ignore_events) {
break;
}
+
+ WindowData &wd = windows[window_id];
+ if (wd.extend_to_title && !wd.maximized && !wd.minimized && !wd.resize_disabled && mouse_mode == MOUSE_MODE_VISIBLE) {
+ if (wd.resize_edge != RESIZE_EDGE_NONE) {
+ _handle_resize(&event, wd);
+ break;
+ } else {
+ if (_handle_border_motion(&event, wd)) {
+ break;
+ }
+ }
+ }
+
// The X11 API requires filtering one-by-one through the motion
// notify events, in order to figure out which event is the one
// generated by warping the mouse pointer.
@@ -4929,7 +4971,6 @@ void DisplayServerX11::process_events() {
break;
}
- const WindowData &wd = windows[window_id];
bool focused = wd.focused;
if (mouse_mode == MOUSE_MODE_CAPTURED) {
@@ -5199,6 +5240,90 @@ void DisplayServerX11::process_events() {
Input::get_singleton()->flush_buffered_events();
}
+int DisplayServerX11::_detect_resize_edge(int p_mouse_x, int p_mouse_y, const WindowData &p_wd) {
+ int edge = RESIZE_EDGE_NONE;
+
+ if (p_mouse_x < RESIZE_BORDER) {
+ edge |= RESIZE_EDGE_LEFT;
+ }
+ if (p_mouse_x > p_wd.size.x - RESIZE_BORDER) {
+ edge |= RESIZE_EDGE_RIGHT;
+ }
+ if (p_mouse_y < RESIZE_BORDER) {
+ edge |= RESIZE_EDGE_TOP;
+ }
+ if (p_mouse_y > p_wd.size.y - RESIZE_BORDER) {
+ edge |= RESIZE_EDGE_BOTTOM;
+ }
+
+ return edge;
+}
+
+void DisplayServerX11::_handle_resize(XEvent *p_event, WindowData &p_wd) {
+ int new_width = p_wd.resize_origin_width;
+ int new_height = p_wd.resize_origin_height;
+ int new_position_x = p_wd.resize_origin_position_x;
+ int new_position_y = p_wd.resize_origin_position_y;
+ int delta_x = p_event->xmotion.x_root - p_wd.resize_origin_mouse_x;
+ int delta_y = p_event->xmotion.y_root - p_wd.resize_origin_mouse_y;
+
+ if (p_wd.resize_edge & RESIZE_EDGE_RIGHT) {
+ new_width = std::max(100, new_width + delta_x);
+ } else if (p_wd.resize_edge & RESIZE_EDGE_LEFT) {
+ new_width = std::max(100, new_width - delta_x);
+ new_position_x += delta_x;
+ }
+
+ if (p_wd.resize_edge & RESIZE_EDGE_BOTTOM) {
+ new_height = std::max(100, new_height + delta_y);
+ } else if (p_wd.resize_edge & RESIZE_EDGE_TOP) {
+ new_height = std::max(100, new_height - delta_y);
+ new_position_y += delta_y;
+ }
+
+ if (p_wd.position.x != new_position_x || p_wd.position.y != new_position_y) {
+ p_wd.position = Size2i(new_position_x, new_position_y);
+ XMoveWindow(x11_display, p_wd.x11_window, new_position_x, new_position_y);
+ }
+
+ if (p_wd.size.x != new_width || p_wd.size.y != new_height) {
+ p_wd.size = Size2i(new_width, new_height);
+ XResizeWindow(x11_display, p_wd.x11_window, new_width, new_height);
+ }
+}
+
+bool DisplayServerX11::_handle_border_motion(XEvent *event, WindowData &p_wd) {
+ int edge = _detect_resize_edge(event->xmotion.x, event->xmotion.y, p_wd);
+
+ Cursor cursor = None;
+ if (edge & RESIZE_EDGE_LEFT || edge & RESIZE_EDGE_RIGHT) {
+ cursor = cursors[DisplayServer::CursorShape::CURSOR_HSIZE];
+ }
+ if (edge & RESIZE_EDGE_TOP || edge & RESIZE_EDGE_BOTTOM) {
+ cursor = cursors[DisplayServer::CursorShape::CURSOR_VSIZE];
+ }
+ if ((edge & RESIZE_EDGE_TOP && edge & RESIZE_EDGE_LEFT) || (edge & RESIZE_EDGE_BOTTOM && edge & RESIZE_EDGE_RIGHT)) {
+ cursor = cursors[DisplayServer::CursorShape::CURSOR_FDIAGSIZE];
+ }
+ if ((edge & RESIZE_EDGE_TOP && edge & RESIZE_EDGE_RIGHT) || (edge & RESIZE_EDGE_BOTTOM && edge & RESIZE_EDGE_LEFT)) {
+ cursor = cursors[DisplayServer::CursorShape::CURSOR_BDIAGSIZE];
+ }
+
+ if (cursor != None) {
+ if (p_wd.resize_border_cursor != cursor) {
+ XDefineCursor(x11_display, p_wd.x11_window, cursor);
+ p_wd.resize_border_cursor = cursor;
+ }
+ return true;
+ } else if (p_wd.resize_border_cursor != None) {
+ // Reset previous cursor, not resizing anymore.
+ XDefineCursor(x11_display, p_wd.x11_window, current_cursor);
+ p_wd.resize_border_cursor = None;
+ }
+
+ return false;
+}
+
void DisplayServerX11::release_rendering_thread() {
#if defined(GLES3_ENABLED)
if (gl_manager) {
@@ -5412,6 +5537,10 @@ DisplayServer::VSyncMode DisplayServerX11::window_get_vsync_mode(WindowID p_wind
return DisplayServer::VSYNC_ENABLED;
}
+bool DisplayServerX11::window_maximize_on_title_dbl_click() const {
+ return true;
+}
+
Vector DisplayServerX11::get_rendering_drivers_func() {
Vector drivers;
@@ -5691,7 +5820,7 @@ DisplayServerX11::WindowID DisplayServerX11::_create_window(WindowMode p_mode, V
_update_context(wd);
- if (p_flags & WINDOW_FLAG_BORDERLESS_BIT) {
+ if (p_flags & WINDOW_FLAG_BORDERLESS_BIT || p_flags & WINDOW_FLAG_EXTEND_TO_TITLE_BIT) {
Hints hints;
Atom property;
hints.flags = 2;
diff --git a/platform/linuxbsd/x11/display_server_x11.h b/platform/linuxbsd/x11/display_server_x11.h
index 0cbfbe51ef1f..a5a4eb189448 100644
--- a/platform/linuxbsd/x11/display_server_x11.h
+++ b/platform/linuxbsd/x11/display_server_x11.h
@@ -207,10 +207,31 @@ class DisplayServerX11 : public DisplayServer {
bool is_popup = false;
bool layered_window = false;
bool mpass = false;
+ bool extend_to_title = false;
Rect2i parent_safe_rect;
unsigned int focus_order = 0;
+
+ // Variables to track the resizing state
+ Cursor resize_border_cursor = None;
+ int resize_edge = 0;
+ int resize_origin_mouse_x, resize_origin_mouse_y = 0;
+ int resize_origin_width, resize_origin_height = 0;
+ int resize_origin_position_x, resize_origin_position_y = 0;
+ };
+
+ const int RESIZE_BORDER = 5;
+ enum ResizeEdges {
+ RESIZE_EDGE_NONE = 0,
+ RESIZE_EDGE_LEFT = 1,
+ RESIZE_EDGE_RIGHT = 1 << 1,
+ RESIZE_EDGE_TOP = 1 << 2,
+ RESIZE_EDGE_BOTTOM = 1 << 3,
+ RESIZE_EDGE_TOP_LEFT = RESIZE_EDGE_TOP | RESIZE_EDGE_LEFT,
+ RESIZE_EDGE_TOP_RIGHT = RESIZE_EDGE_TOP | RESIZE_EDGE_RIGHT,
+ RESIZE_EDGE_BOTTOM_LEFT = RESIZE_EDGE_BOTTOM | RESIZE_EDGE_LEFT,
+ RESIZE_EDGE_BOTTOM_RIGHT = RESIZE_EDGE_BOTTOM | RESIZE_EDGE_RIGHT
};
Point2i im_selection;
@@ -375,6 +396,10 @@ class DisplayServerX11 : public DisplayServer {
static Bool _predicate_clipboard_incr(Display *display, XEvent *event, XPointer arg);
static Bool _predicate_clipboard_save_targets(Display *display, XEvent *event, XPointer arg);
+ int _detect_resize_edge(int p_mouse_x, int p_mouse_y, const WindowData &p_wd);
+ void _handle_resize(XEvent *p_event, WindowData &p_wd);
+ bool _handle_border_motion(XEvent *p_event, WindowData &p_wd);
+
protected:
void _window_changed(XEvent *event);
@@ -510,6 +535,8 @@ class DisplayServerX11 : public DisplayServer {
virtual void window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override;
virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override;
+ virtual bool window_maximize_on_title_dbl_click() const override;
+
virtual void cursor_set_shape(CursorShape p_shape) override;
virtual CursorShape cursor_get_shape() const override;
virtual void cursor_set_custom_image(const Ref &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) override;
diff --git a/platform/macos/display_server_macos.h b/platform/macos/display_server_macos.h
index 97af6d0a5a25..bfbd7cc02ce0 100644
--- a/platform/macos/display_server_macos.h
+++ b/platform/macos/display_server_macos.h
@@ -404,6 +404,7 @@ class DisplayServerMacOS : public DisplayServer {
virtual void window_set_window_buttons_offset(const Vector2i &p_offset, WindowID p_window = MAIN_WINDOW_ID) override;
virtual Vector3i window_get_safe_title_margins(WindowID p_window = MAIN_WINDOW_ID) const override;
+ virtual bool window_is_extend_to_title_show_window_buttons() const override;
virtual Point2i ime_get_selection() const override;
virtual String ime_get_text() const override;
diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm
index 48cc7bbba3ef..19e9cd88000b 100644
--- a/platform/macos/display_server_macos.mm
+++ b/platform/macos/display_server_macos.mm
@@ -2375,6 +2375,10 @@
}
}
+bool DisplayServerMacOS::window_is_extend_to_title_show_window_buttons() const {
+ return true;
+}
+
void DisplayServerMacOS::window_set_custom_window_buttons(WindowData &p_wd, bool p_enabled) {
if (p_wd.window_button_view) {
[p_wd.window_button_view removeFromSuperview];
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index ffa3840181b4..4157353153d7 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -119,6 +119,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const {
case FEATURE_NATIVE_DIALOG_FILE:
case FEATURE_SWAP_BUFFERS:
case FEATURE_KEEP_SCREEN_ON:
+ case FEATURE_EXTEND_TO_TITLE:
case FEATURE_TEXT_TO_SPEECH:
case FEATURE_SCREEN_CAPTURE:
case FEATURE_STATUS_INDICATOR:
@@ -1505,6 +1506,9 @@ DisplayServer::WindowID DisplayServerWindows::create_sub_window(WindowMode p_mod
wd.layered_window = true;
}
+ if (p_flags & WINDOW_FLAG_EXTEND_TO_TITLE_BIT) {
+ wd.extend_to_title = true;
+ }
// Inherit icons from MAIN_WINDOW for all sub windows.
HICON mainwindow_icon = (HICON)SendMessage(windows[MAIN_WINDOW_ID].hWnd, WM_GETICON, ICON_SMALL, 0);
@@ -1720,7 +1724,7 @@ Size2i DisplayServerWindows::window_get_title_size(const String &p_title, Window
ERR_FAIL_COND_V(!windows.has(p_window), size);
const WindowData &wd = windows[p_window];
- if (wd.fullscreen || wd.minimized || wd.borderless) {
+ if (wd.fullscreen || wd.minimized || wd.borderless || wd.extend_to_title) {
return size;
}
@@ -1774,7 +1778,7 @@ void DisplayServerWindows::_update_window_mouse_passthrough(WindowID p_window) {
} else {
POINT *points = (POINT *)memalloc(sizeof(POINT) * windows[p_window].mpath.size());
for (int i = 0; i < windows[p_window].mpath.size(); i++) {
- if (windows[p_window].borderless) {
+ if (windows[p_window].borderless || windows[p_window].extend_to_title) {
points[i].x = windows[p_window].mpath[i].x;
points[i].y = windows[p_window].mpath[i].y;
} else {
@@ -1901,10 +1905,12 @@ void DisplayServerWindows::window_set_position(const Point2i &p_position, Window
rc.bottom = p_position.y + wd.height + offset.y;
rc.top = p_position.y + offset.y;
- const DWORD style = GetWindowLongPtr(wd.hWnd, GWL_STYLE);
- const DWORD exStyle = GetWindowLongPtr(wd.hWnd, GWL_EXSTYLE);
+ if (!wd.extend_to_title) {
+ const DWORD style = GetWindowLongPtr(wd.hWnd, GWL_STYLE);
+ const DWORD exStyle = GetWindowLongPtr(wd.hWnd, GWL_EXSTYLE);
- AdjustWindowRectEx(&rc, style, false, exStyle);
+ AdjustWindowRectEx(&rc, style, false, exStyle);
+ }
MoveWindow(wd.hWnd, rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, TRUE);
wd.last_pos = p_position;
@@ -2015,7 +2021,7 @@ void DisplayServerWindows::window_set_size(const Size2i p_size, WindowID p_windo
ERR_FAIL_COND(!windows.has(p_window));
WindowData &wd = windows[p_window];
- if (wd.fullscreen || wd.maximized) {
+ if (wd.fullscreen || wd.maximized || IsIconic(wd.hWnd)) {
return;
}
@@ -2066,7 +2072,7 @@ Size2i DisplayServerWindows::window_get_size_with_decorations(WindowID p_window)
return Size2();
}
-void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_initialized, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_minimized, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex) {
+void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_initialized, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_minimized, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, bool p_extend_to_title, DWORD &r_style, DWORD &r_style_ex) {
// Windows docs for window styles:
// https://docs.microsoft.com/en-us/windows/win32/winmsg/window-styles
// https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles
@@ -2080,7 +2086,7 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_initiali
}
}
- if (p_fullscreen || p_borderless) {
+ if (p_fullscreen || p_borderless || p_extend_to_title) {
r_style |= WS_POPUP; // p_borderless was WS_EX_TOOLWINDOW in the past.
if (p_minimized) {
r_style |= WS_MINIMIZE;
@@ -2093,6 +2099,18 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_initiali
if (p_resizable) {
r_style |= WS_MAXIMIZEBOX;
}
+ if (!p_borderless && !p_fullscreen) {
+ // Extend to title mode.
+ if (!p_maximized && !p_maximized_fs) {
+ // Needs a border to resize the window without WS_SYSMENU.
+ r_style |= WS_SIZEBOX;
+ }
+ // Without this, the Project Manager preview when minimized is blank in Windows Task Bar
+ // and restoring from minimized state is not possible.
+ if (p_minimized) {
+ r_style |= WS_MINIMIZE;
+ }
+ }
}
if ((p_fullscreen && p_multiwindow_fs) || p_maximized_fs) {
r_style |= WS_BORDER; // Allows child windows to be displayed on top of full screen.
@@ -2119,7 +2137,7 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_initiali
r_style_ex |= WS_EX_TOPMOST | WS_EX_NOACTIVATE;
}
- if (!p_borderless && !p_no_activate_focus && p_initialized) {
+ if ((!p_borderless || !p_extend_to_title) && !p_no_activate_focus && p_initialized) {
r_style |= WS_VISIBLE;
}
@@ -2136,7 +2154,7 @@ void DisplayServerWindows::_update_window_style(WindowID p_window, bool p_repain
DWORD style = 0;
DWORD style_ex = 0;
- _get_window_style(p_window == MAIN_WINDOW_ID, wd.initialized, wd.fullscreen, wd.multiwindow_fs, wd.borderless, wd.resizable, wd.minimized, wd.maximized, wd.maximized_fs, wd.no_focus || wd.is_popup, style, style_ex);
+ _get_window_style(p_window == MAIN_WINDOW_ID, wd.initialized, wd.fullscreen, wd.multiwindow_fs, wd.borderless, wd.resizable, wd.minimized, wd.maximized, wd.maximized_fs, wd.no_focus || wd.is_popup, wd.extend_to_title, style, style_ex);
SetWindowLongPtr(wd.hWnd, GWL_STYLE, style);
SetWindowLongPtr(wd.hWnd, GWL_EXSTYLE, style_ex);
@@ -2145,6 +2163,21 @@ void DisplayServerWindows::_update_window_style(WindowID p_window, bool p_repain
set_icon(icon);
}
+ if (wd.extend_to_title) {
+ // We need to keep the border thickness so we could use it in Non Client Hit Test (WM_NCHITTEST).
+ // The simple way to get the border thickness is to call AdjustWindowRectEx on a window of size (0, 0).
+ SetRectEmpty(&wd.border_thickness);
+ AdjustWindowRectEx(&wd.border_thickness, style, FALSE, style_ex);
+ wd.border_thickness.left *= -1;
+ wd.border_thickness.top *= -1;
+
+ // We need to set the bottom margin to keep the rounded corner and the drop shadow.
+ MARGINS margins = { 0, 0, 0, 1 };
+ ::DwmExtendFrameIntoClientArea(wd.hWnd, &margins);
+ } else {
+ SetRectEmpty(&wd.border_thickness);
+ }
+
SetWindowPos(wd.hWnd, wd.always_on_top ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | ((wd.no_focus || wd.is_popup) ? SWP_NOACTIVATE : 0));
if (p_repaint) {
@@ -2160,6 +2193,11 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window)
ERR_FAIL_COND(!windows.has(p_window));
WindowData &wd = windows[p_window];
+ WindowMode old_mode = window_get_mode(p_window);
+ if (old_mode == p_mode) {
+ return; // Do nothing.
+ }
+
if (wd.fullscreen && p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) {
RECT rect;
@@ -2243,6 +2281,9 @@ void DisplayServerWindows::window_set_mode(WindowMode p_mode, WindowID p_window)
SystemParametersInfoA(SPI_SETMOUSETRAILS, 0, nullptr, 0);
}
}
+
+ // Notify a possible change in the titlebar.
+ _send_window_event(wd, DisplayServerWindows::WINDOW_EVENT_TITLEBAR_CHANGE);
}
DisplayServer::WindowMode DisplayServerWindows::window_get_mode(WindowID p_window) const {
@@ -2337,6 +2378,12 @@ void DisplayServerWindows::window_set_flag(WindowFlags p_flag, bool p_enabled, W
ERR_FAIL_COND_MSG(IsWindowVisible(wd.hWnd) && (wd.is_popup != p_enabled), "Popup flag can't changed while window is opened.");
wd.is_popup = p_enabled;
} break;
+ case WINDOW_FLAG_EXTEND_TO_TITLE: {
+ wd.extend_to_title = p_enabled;
+ _update_window_style(p_window);
+ // Notify a possible change in the titlebar.
+ _send_window_event(wd, DisplayServerWindows::WINDOW_EVENT_TITLEBAR_CHANGE);
+ } break;
default:
break;
}
@@ -2369,6 +2416,9 @@ bool DisplayServerWindows::window_get_flag(WindowFlags p_flag, WindowID p_window
case WINDOW_FLAG_POPUP: {
return wd.is_popup;
} break;
+ case WINDOW_FLAG_EXTEND_TO_TITLE: {
+ return wd.extend_to_title;
+ } break;
default:
break;
}
@@ -3640,6 +3690,10 @@ DisplayServer::VSyncMode DisplayServerWindows::window_get_vsync_mode(WindowID p_
return DisplayServer::VSYNC_ENABLED;
}
+bool DisplayServerWindows::window_maximize_on_title_dbl_click() const {
+ return true;
+}
+
void DisplayServerWindows::set_context(Context p_context) {
}
@@ -4025,9 +4079,46 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
return 0;
}
} break;
+ case WM_NCCALCSIZE:
+ if (windows[window_id].extend_to_title) {
+ if (lParam) {
+ // Returning zero here tells Windows to keep the received area in lParam the same
+ // which should be the hole window area.
+ // Without that, on Windows 7/10, we have a white bar on top of the window.
+ return 0;
+ }
+ }
+ break;
case WM_NCHITTEST: {
if (windows[window_id].mpass) {
return HTTRANSPARENT;
+ } else if (windows[window_id].extend_to_title) {
+ if (windows[window_id].borderless || windows[window_id].fullscreen || windows[window_id].maximized || !windows[window_id].resizable) {
+ return HTCLIENT;
+ } else {
+ // We disabled the Non Client Area in (WM_NCCALCSIZE) so we need to do the hit test
+ // by ourself. Unfornunately, the hit needs to be inside the window since we don't have
+ // a client area anymore.
+ POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
+ ScreenToClient(windows[window_id].hWnd, &pt);
+ RECT rc;
+ GetClientRect(windows[window_id].hWnd, &rc);
+ bool hitLeft = (pt.x < windows[window_id].border_thickness.left);
+ bool hitRight = (pt.x > rc.right - windows[window_id].border_thickness.right);
+ bool hitTop = (pt.y < windows[window_id].border_thickness.top);
+ bool hitBottom = (pt.y > rc.bottom - windows[window_id].border_thickness.bottom);
+
+ if (hitLeft) {
+ return (hitTop ? HTTOPLEFT : (hitBottom ? HTBOTTOMLEFT : HTLEFT));
+ } else if (hitRight) {
+ return (hitTop ? HTTOPRIGHT : (hitBottom ? HTBOTTOMRIGHT : HTRIGHT));
+ } else if (hitTop) {
+ return HTTOP;
+ } else if (hitBottom) {
+ return HTBOTTOM;
+ }
+ return HTCLIENT;
+ }
}
} break;
case WM_MOUSEACTIVATE: {
@@ -4068,7 +4159,7 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
min_max_info->ptMaxTrackSize.x = windows[window_id].max_size.x + decor.x;
min_max_info->ptMaxTrackSize.y = windows[window_id].max_size.y + decor.y;
}
- if (windows[window_id].borderless) {
+ if (windows[window_id].borderless || windows[window_id].extend_to_title) {
Rect2i screen_rect = screen_get_usable_rect(window_get_current_screen(window_id));
// Set the size of (borderless) maximized mode to exclude taskbar (or any other panel) if present.
@@ -4076,6 +4167,13 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA
min_max_info->ptMaxPosition.y = screen_rect.position.y;
min_max_info->ptMaxSize.x = screen_rect.size.x;
min_max_info->ptMaxSize.y = screen_rect.size.y;
+
+ // When DwmExtendFrameIntoClientArea is called, we set a margin of 1 at the bottom to retain the drop shadow.
+ // This has the side effect of leaving an empty pixel at the bottom when maximized.
+ // Adding a pixel at the bottom seems to fix the issue.
+ if (windows[window_id].extend_to_title) {
+ min_max_info->ptMaxSize.y += 1;
+ }
}
return 0;
}
@@ -5547,7 +5645,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
DWORD dwExStyle;
DWORD dwStyle;
- _get_window_style(window_id_counter == MAIN_WINDOW_ID, false, (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MINIMIZED, p_mode == WINDOW_MODE_MAXIMIZED, false, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) | (p_flags & WINDOW_FLAG_POPUP), dwStyle, dwExStyle);
+ _get_window_style(window_id_counter == MAIN_WINDOW_ID, false, (p_mode == WINDOW_MODE_FULLSCREEN || p_mode == WINDOW_MODE_EXCLUSIVE_FULLSCREEN), p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN, p_flags & WINDOW_FLAG_BORDERLESS_BIT, !(p_flags & WINDOW_FLAG_RESIZE_DISABLED_BIT), p_mode == WINDOW_MODE_MINIMIZED, p_mode == WINDOW_MODE_MAXIMIZED, false, (p_flags & WINDOW_FLAG_NO_FOCUS_BIT) | (p_flags & WINDOW_FLAG_POPUP), p_flags & WINDOW_FLAG_EXTEND_TO_TITLE_BIT, dwStyle, dwExStyle);
RECT WindowRect;
@@ -5587,7 +5685,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
WindowRect.top += offset.y;
WindowRect.bottom += offset.y;
- if (p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN) {
+ if (p_mode != WINDOW_MODE_FULLSCREEN && p_mode != WINDOW_MODE_EXCLUSIVE_FULLSCREEN && !(p_flags & WINDOW_FLAG_EXTEND_TO_TITLE_BIT)) {
AdjustWindowRectEx(&WindowRect, dwStyle, FALSE, dwExStyle);
}
@@ -5804,8 +5902,14 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode,
}
// Set size of maximized borderless window (by default it covers the entire screen).
- if (p_mode == WINDOW_MODE_MAXIMIZED && (p_flags & WINDOW_FLAG_BORDERLESS_BIT)) {
+ if (p_mode == WINDOW_MODE_MAXIMIZED && ((p_flags & WINDOW_FLAG_BORDERLESS_BIT) || (p_flags & WINDOW_FLAG_EXTEND_TO_TITLE_BIT))) {
Rect2i srect = screen_get_usable_rect(rq_screen);
+ // When DwmExtendFrameIntoClientArea is called, we set a margin of 1 at the bottom to retain the drop shadow.
+ // This has the side effect of leaving an empty pixel at the bottom when maximized.
+ // Adding a pixel at the bottom seems to fix the issue.
+ if (p_flags & WINDOW_FLAG_EXTEND_TO_TITLE_BIT) {
+ srect.size.height += 1;
+ }
SetWindowPos(wd.hWnd, HWND_TOP, srect.position.x, srect.position.y, srect.size.width, srect.size.height, SWP_NOZORDER | SWP_NOACTIVATE);
}
diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h
index 7d6a3e96a6bc..a8979d10fc50 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -473,6 +473,7 @@ class DisplayServerWindows : public DisplayServer {
bool exclusive = false;
bool context_created = false;
bool mpass = false;
+ bool extend_to_title = false;
// Used to transfer data between events using timer.
WPARAM saved_wparam;
@@ -501,6 +502,7 @@ class DisplayServerWindows : public DisplayServer {
Size2 window_rect;
Point2 last_pos;
+ RECT border_thickness;
ObjectID instance_id;
@@ -593,7 +595,7 @@ class DisplayServerWindows : public DisplayServer {
HashMap pointer_last_pos;
void _send_window_event(const WindowData &wd, WindowEvent p_event);
- void _get_window_style(bool p_main_window, bool p_initialized, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_minimized, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, DWORD &r_style, DWORD &r_style_ex);
+ void _get_window_style(bool p_main_window, bool p_initialized, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, bool p_minimized, bool p_maximized, bool p_maximized_fs, bool p_no_activate_focus, bool p_extend_to_title, DWORD &r_style, DWORD &r_style_ex);
MouseMode mouse_mode;
int restore_mouse_trails = 0;
@@ -780,6 +782,8 @@ class DisplayServerWindows : public DisplayServer {
virtual void window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override;
virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override;
+ virtual bool window_maximize_on_title_dbl_click() const override;
+
virtual void cursor_set_shape(CursorShape p_shape) override;
virtual CursorShape cursor_get_shape() const override;
virtual void cursor_set_custom_image(const Ref &p_cursor, CursorShape p_shape = CURSOR_ARROW, const Vector2 &p_hotspot = Vector2()) override;
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index 803ce89bc9e1..6b21927afc4a 100644
--- a/scene/main/window.cpp
+++ b/scene/main/window.cpp
@@ -36,6 +36,7 @@
#include "core/string/translation_server.h"
#include "core/variant/variant_parser.h"
#include "scene/gui/control.h"
+#include "scene/resources/world_2d.h"
#include "scene/theme/theme_db.h"
#include "scene/theme/theme_owner.h"
@@ -790,6 +791,7 @@ void Window::_event_callback(DisplayServer::WindowEvent p_event) {
emit_signal(SNAME("dpi_changed"));
} break;
case DisplayServer::WINDOW_EVENT_TITLEBAR_CHANGE: {
+ _update_decoration();
emit_signal(SNAME("titlebar_changed"));
} break;
}
@@ -822,6 +824,11 @@ void Window::hide() {
set_visible(false);
}
+void Window::send_close_request() {
+ _propagate_window_notification(this, NOTIFICATION_WM_CLOSE_REQUEST);
+ emit_signal(SNAME("close_requested"));
+}
+
void Window::set_visible(bool p_visible) {
ERR_MAIN_THREAD_GUARD;
if (visible == p_visible) {
@@ -1215,6 +1222,8 @@ void Window::_update_viewport_size() {
RenderingServer::get_singleton()->viewport_attach_to_screen(get_viewport_rid(), Rect2i(), DisplayServer::INVALID_WINDOW_ID);
}
+ _update_decoration();
+
if (window_id == DisplayServer::MAIN_WINDOW_ID) {
if (!use_font_oversampling) {
font_oversampling = 1.0;
@@ -1387,6 +1396,7 @@ void Window::_notification(int p_what) {
emit_signal(SceneStringName(theme_changed));
_invalidate_theme_cache();
_update_theme_item_cache();
+ _update_decoration();
} break;
case NOTIFICATION_TRANSLATION_CHANGED: {
@@ -1667,6 +1677,13 @@ void Window::_window_input(const Ref &p_ev) {
}
}
+ if (_get_decoration_visible()) {
+ if (_handle_window_buttons(p_ev)) {
+ set_input_as_handled();
+ return;
+ }
+ }
+
// If the event needs to be handled in a Window-derived class, then it should overwrite
// `_input_from_window` instead of subscribing to the `window_input` signal, because the signal
// filters out internal events.
@@ -2803,6 +2820,250 @@ void Window::_mouse_leave_viewport() {
}
}
+void Window::_create_decoration_canvas() {
+ decoration_canvas = RenderingServer::get_singleton()->canvas_item_create();
+ RenderingServer::get_singleton()->canvas_item_set_visible(decoration_canvas, true);
+ RenderingServer::get_singleton()->canvas_item_set_parent(decoration_canvas, find_world_2d()->get_canvas());
+ RenderingServer::get_singleton()->canvas_item_set_draw_index(decoration_canvas, std::numeric_limits::max());
+
+ // That should force to draw the decoration the first time.
+ callable_mp(this, &Window::_update_decoration).call_deferred();
+}
+
+bool Window::_get_decoration_visible() const {
+ // Without the extend to title flag, we don't need the decoration canvas.
+ // And in borderless mode or fullscreen, there's not decorations.
+ return get_flag(Window::FLAG_EXTEND_TO_TITLE) && mode != MODE_FULLSCREEN && MODE_WINDOWED != MODE_EXCLUSIVE_FULLSCREEN && !get_flag(Window::FLAG_BORDERLESS);
+}
+
+void Window::_update_decoration() {
+ if (window_id == DisplayServer::INVALID_WINDOW_ID) {
+ return;
+ }
+
+ // Without the extend to title flag, we don't need the decoration canvas.
+ // And in borderless mode or fullscreen, there's not decorations.
+ if (!_get_decoration_visible()) {
+ if (decoration_canvas.is_valid()) {
+ // Not needed anymore.
+ RenderingServer::get_singleton()->free(decoration_canvas);
+ decoration_canvas = RID();
+ }
+ return;
+ }
+
+ if (!decoration_canvas.is_valid()) {
+ _create_decoration_canvas();
+ }
+
+ Size2i window_size = get_size();
+ Size2i viewport_size = get_visible_rect().size;
+ RenderingServer::get_singleton()->canvas_item_clear(decoration_canvas);
+ Vector2i offset = _get_window_buttons_offset();
+
+ // Calculate each button emplacement.
+ if (is_layout_rtl()) {
+ close_button_rect = Rect2i(offset.x, offset.y, theme_cache.close_h_offset, theme_cache.close_v_offset);
+ maximize_button_rect = Rect2i(theme_cache.close_h_offset + offset.x, offset.y, theme_cache.maximize_h_offset - theme_cache.close_h_offset, theme_cache.maximize_v_offset);
+ minimize_button_rect = Rect2i(theme_cache.maximize_h_offset + offset.x, offset.y, theme_cache.minimize_h_offset - theme_cache.maximize_h_offset, theme_cache.minimize_v_offset);
+ } else {
+ minimize_button_rect = Rect2i(viewport_size.x - theme_cache.minimize_h_offset - offset.x, offset.y, theme_cache.minimize_h_offset - theme_cache.maximize_h_offset, theme_cache.minimize_v_offset);
+ maximize_button_rect = Rect2i(viewport_size.x - theme_cache.maximize_h_offset - offset.x, offset.y, theme_cache.maximize_h_offset - theme_cache.close_h_offset, theme_cache.maximize_v_offset);
+ close_button_rect = Rect2i(viewport_size.x - theme_cache.close_h_offset - offset.x, offset.y, theme_cache.close_h_offset, theme_cache.close_v_offset);
+ }
+
+ // Drawing buttons.
+ _draw_window_button_decoration(minimize_button_rect, WINDOW_BUTTON_MINIMIZE);
+ _draw_window_button_decoration(maximize_button_rect, WINDOW_BUTTON_MAXIMIZE);
+ _draw_window_button_decoration(close_button_rect, WINDOW_BUTTON_CLOSE);
+
+ // We need to translate the coords from the viewport to the real window coords
+ // when the stretch mode is enabled.
+ if (window_size != viewport_size) {
+ Transform2D transform = get_final_transform();
+ minimize_button_rect = _translate_decoration_rect(minimize_button_rect, transform);
+ maximize_button_rect = _translate_decoration_rect(maximize_button_rect, transform);
+ close_button_rect = _translate_decoration_rect(close_button_rect, transform);
+ }
+}
+
+Rect2 Window::_translate_decoration_rect(const Rect2 &p_rect, const Transform2D &p_window_transform) {
+ Vector2 new_position = p_window_transform.translated_local(p_rect.position).get_origin();
+ Vector2 new_end_position = p_window_transform.translated_local(p_rect.get_end()).get_origin();
+
+ return Rect2(new_position, new_end_position - new_position);
+}
+
+void Window::_draw_window_button_decoration(const Rect2 &p_button_rect, WindowButton p_window_button) {
+ Ref style_box = _get_decoration_button_style_box(p_window_button);
+ if (style_box.is_valid()) {
+ style_box->draw(decoration_canvas, p_button_rect);
+ }
+
+ Ref icon = _get_decoration_button_icon(p_window_button);
+ if (icon.is_valid()) {
+ Size2 icon_size = icon->get_size();
+ int ofs_x = (p_button_rect.size.x - icon_size.x) / 2.0;
+ int ofs_y = (p_button_rect.size.y - icon_size.y) / 2.0;
+ icon->draw_rect(decoration_canvas, Rect2(p_button_rect.position.x + ofs_x, p_button_rect.position.y + ofs_y, icon_size.x, icon_size.y), false, _get_decoration_button_modulate(p_window_button));
+ }
+}
+
+Ref Window::_get_decoration_button_icon(WindowButton p_window_button) {
+ switch (p_window_button) {
+ case WINDOW_BUTTON_MINIMIZE:
+ return window_button_hover == WINDOW_BUTTON_MINIMIZE ? theme_cache.minimize_pressed : theme_cache.minimize;
+ case WINDOW_BUTTON_MAXIMIZE:
+ if (get_flag(FLAG_RESIZE_DISABLED)) {
+ return theme_cache.maximize_disabled;
+ } else if (get_mode() == MODE_WINDOWED) {
+ return window_button_hover == WINDOW_BUTTON_MAXIMIZE ? theme_cache.maximize_pressed : theme_cache.maximize;
+ } else {
+ return window_button_hover == WINDOW_BUTTON_MAXIMIZE ? theme_cache.restore_pressed : theme_cache.restore;
+ }
+ default:
+ return window_button_hover == WINDOW_BUTTON_CLOSE ? theme_cache.close_pressed : theme_cache.close;
+ }
+}
+
+Ref Window::_get_decoration_button_style_box(WindowButton p_window_button) {
+ if (p_window_button == window_button_pressed) {
+ return theme_cache.decoration_button_pressed;
+ }
+
+ if (p_window_button == window_button_hover) {
+ return theme_cache.decoration_button_hover;
+ }
+
+ return theme_cache.decoration_button_normal;
+}
+
+Color Window::_get_decoration_button_modulate(WindowButton p_window_button) {
+ if (p_window_button == window_button_pressed) {
+ return theme_cache.decoration_button_normal_modulate;
+ }
+
+ if (p_window_button == window_button_hover) {
+ return theme_cache.decoration_button_hover_modulate;
+ }
+
+ return theme_cache.decoration_button_normal_modulate;
+}
+
+bool Window::_handle_window_buttons(const Ref &p_event) {
+ Ref mm = p_event;
+ if (mm.is_valid()) {
+ WindowButton new_button_state = WINDOW_BUTTON_NONE;
+ Vector2 mouse_position = mm->get_position();
+ if (minimize_button_rect.has_point(mouse_position)) {
+ new_button_state = WINDOW_BUTTON_MINIMIZE;
+ } else if (maximize_button_rect.has_point(mouse_position)) {
+ if (!get_flag(FLAG_RESIZE_DISABLED)) {
+ new_button_state = WINDOW_BUTTON_MAXIMIZE;
+ }
+ } else if (close_button_rect.has_point(mouse_position)) {
+ new_button_state = WINDOW_BUTTON_CLOSE;
+ }
+
+ if (new_button_state != window_button_hover) {
+ window_button_hover = new_button_state;
+ window_button_pressed = WINDOW_BUTTON_NONE;
+ callable_mp(this, &Window::_update_decoration).call_deferred();
+ }
+
+ if (window_button_hover != WINDOW_BUTTON_NONE) {
+ return true;
+ }
+ }
+
+ if (window_button_hover == WINDOW_BUTTON_NONE) {
+ return false;
+ }
+
+ Ref mb = p_event;
+ if (mb.is_valid()) {
+ if (mb->get_button_index() == MouseButton::LEFT) {
+ if (mb->is_pressed()) {
+ // Note the pressed button put take action only on release.
+ window_button_pressed = window_button_hover;
+ } else if (window_button_pressed == window_button_hover) {
+ // Released on the same button.
+ switch (window_button_pressed) {
+ case WINDOW_BUTTON_MINIMIZE: {
+ set_mode(MODE_MINIMIZED);
+ } break;
+ case WINDOW_BUTTON_MAXIMIZE: {
+ if (get_mode() == MODE_WINDOWED) {
+ set_mode(MODE_MAXIMIZED);
+ } else {
+ set_mode(MODE_WINDOWED);
+ }
+ } break;
+ case WINDOW_BUTTON_CLOSE: {
+ send_close_request();
+ } break;
+ case WINDOW_BUTTON_NONE: {
+ // Just to pass the CI validations.
+ } break;
+ }
+ window_button_pressed = WINDOW_BUTTON_NONE;
+ }
+ callable_mp(this, &Window::_update_decoration).call_deferred();
+ }
+ // Even if we don't use this event, it was over on of the window button, return true
+ // to prevent propagation of the event.
+ return true;
+ }
+
+ return false;
+}
+
+Vector2i Window::_get_window_buttons_offset() const {
+ // The method set_window_buttons_offset receives the center of the first button.
+ // This method calculates the offset to the left and right of the window buttons.
+ if (!window_buttons_offset_customized) {
+ return Vector2i();
+ }
+ return Vector2i(window_buttons_offset.x - (theme_cache.close_h_offset / 2), window_buttons_offset.y - (theme_cache.close_v_offset / 2));
+}
+
+void Window::set_window_buttons_offset(const Vector2i &p_offset) {
+ window_buttons_offset = p_offset;
+ window_buttons_offset_customized = true;
+
+ if (DisplayServer::get_singleton()->window_is_extend_to_title_show_window_buttons()) {
+ // Window buttons are managed by the OS.
+ DisplayServer::get_singleton()->window_set_window_buttons_offset(p_offset, window_id);
+ }
+}
+
+Vector2i Window::get_window_buttons_offset() const {
+ return window_buttons_offset;
+}
+
+Vector2i Window::get_safe_title_margins_left() const {
+ if (DisplayServer::get_singleton()->window_is_extend_to_title_show_window_buttons()) {
+ // Window buttons are managed by the OS.
+ const Vector3i &margin = DisplayServer::get_singleton()->window_get_safe_title_margins(window_id);
+ return Vector2i(is_layout_rtl() ? margin.y : margin.x, margin.z);
+ } else {
+ return Vector2i();
+ }
+}
+
+Vector2i Window::get_safe_title_margins_right() const {
+ if (DisplayServer::get_singleton()->window_is_extend_to_title_show_window_buttons()) {
+ // Window buttons are managed by the OS.
+ const Vector3i &margin = DisplayServer::get_singleton()->window_get_safe_title_margins(window_id);
+ return Vector2i(is_layout_rtl() ? margin.x : margin.y, margin.z);
+ } else if (_get_decoration_visible()) {
+ // Window buttons are managed by ourself.
+ return Vector2i(theme_cache.minimize_h_offset, MAX(MAX(theme_cache.minimize_v_offset, theme_cache.maximize_v_offset), theme_cache.close_v_offset)) + _get_window_buttons_offset();
+ } else {
+ return Vector2i();
+ }
+}
+
void Window::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_title", "title"), &Window::set_title);
ClassDB::bind_method(D_METHOD("get_title"), &Window::get_title);
@@ -3086,6 +3347,14 @@ void Window::_bind_methods() {
BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Window, embedded_border);
BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Window, embedded_unfocused_border);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Window, decoration_button_normal);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Window, decoration_button_hover);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_STYLEBOX, Window, decoration_button_pressed);
+
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Window, decoration_button_normal_modulate);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Window, decoration_button_hover_modulate);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Window, decoration_button_pressed_modulate);
+
BIND_THEME_ITEM(Theme::DATA_TYPE_FONT, Window, title_font);
BIND_THEME_ITEM(Theme::DATA_TYPE_FONT_SIZE, Window, title_font_size);
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Window, title_color);
@@ -3093,6 +3362,19 @@ void Window::_bind_methods() {
BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, Window, title_outline_modulate);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Window, title_outline_size);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, minimize);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, minimize_pressed);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Window, minimize_h_offset);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Window, minimize_v_offset);
+
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, maximize);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, maximize_pressed);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, maximize_disabled);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, restore);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, restore_pressed);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Window, maximize_h_offset);
+ BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Window, maximize_v_offset);
+
BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, close);
BIND_THEME_ITEM(Theme::DATA_TYPE_ICON, Window, close_pressed);
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, Window, close_h_offset);
@@ -3113,6 +3395,10 @@ Window::Window() {
}
Window::~Window() {
+ if (decoration_canvas.is_valid() && RenderingServer::get_singleton()) {
+ RenderingServer::get_singleton()->free(decoration_canvas);
+ }
+
memdelete(theme_owner);
// Resources need to be disconnected.
diff --git a/scene/main/window.h b/scene/main/window.h
index 47aaf7372870..fe4dacfdd70b 100644
--- a/scene/main/window.h
+++ b/scene/main/window.h
@@ -31,6 +31,7 @@
#ifndef WINDOW_H
#define WINDOW_H
+#include "scene/gui/panel.h"
#include "scene/main/viewport.h"
#include "scene/resources/theme.h"
@@ -105,6 +106,13 @@ class Window : public Viewport {
WINDOW_INITIAL_POSITION_CENTER_SCREEN_WITH_KEYBOARD_FOCUS,
};
+ enum WindowButton {
+ WINDOW_BUTTON_NONE,
+ WINDOW_BUTTON_MINIMIZE,
+ WINDOW_BUTTON_MAXIMIZE,
+ WINDOW_BUTTON_CLOSE,
+ };
+
private:
DisplayServer::WindowID window_id = DisplayServer::INVALID_WINDOW_ID;
bool initialized = false;
@@ -206,6 +214,27 @@ class Window : public Viewport {
Color title_outline_modulate;
int title_outline_size = 0;
+ Ref decoration_button_normal;
+ Ref decoration_button_hover;
+ Ref decoration_button_pressed;
+
+ Color decoration_button_normal_modulate;
+ Color decoration_button_hover_modulate;
+ Color decoration_button_pressed_modulate;
+
+ Ref minimize;
+ Ref minimize_pressed;
+ int minimize_h_offset = 0;
+ int minimize_v_offset = 0;
+
+ Ref maximize;
+ Ref maximize_pressed;
+ Ref maximize_disabled;
+ Ref restore;
+ Ref restore_pressed;
+ int maximize_h_offset = 0;
+ int maximize_v_offset = 0;
+
Ref close;
Ref close_pressed;
int close_h_offset = 0;
@@ -237,6 +266,25 @@ class Window : public Viewport {
static int root_layout_direction;
+ RID decoration_canvas;
+ WindowButton window_button_hover = WINDOW_BUTTON_NONE;
+ WindowButton window_button_pressed = WINDOW_BUTTON_NONE;
+ Rect2 minimize_button_rect;
+ Rect2 maximize_button_rect;
+ Rect2 close_button_rect;
+ bool window_buttons_offset_customized = false;
+ Vector2i window_buttons_offset;
+ void _create_decoration_canvas();
+ void _update_decoration();
+ bool _get_decoration_visible() const;
+ Rect2 _translate_decoration_rect(const Rect2 &p_rect, const Transform2D &p_window_transform);
+ void _draw_window_button_decoration(const Rect2 &p_button_rect, WindowButton p_window_button);
+ Ref _get_decoration_button_icon(WindowButton p_window_button);
+ Ref _get_decoration_button_style_box(WindowButton p_window_button);
+ Color _get_decoration_button_modulate(WindowButton p_window_button);
+ bool _handle_window_buttons(const Ref &p_event);
+ Vector2i _get_window_buttons_offset() const;
+
protected:
virtual Rect2i _popup_adjust_rect() const { return Rect2i(); }
virtual void _post_popup() {}
@@ -315,6 +363,7 @@ class Window : public Viewport {
void show();
void hide();
+ void send_close_request();
void set_transient(bool p_transient);
bool is_transient() const;
@@ -464,6 +513,11 @@ class Window : public Viewport {
Ref get_theme_default_font() const;
int get_theme_default_font_size() const;
+ void set_window_buttons_offset(const Vector2i &p_offset);
+ Vector2i get_window_buttons_offset() const;
+ Vector2i get_safe_title_margins_left() const;
+ Vector2i get_safe_title_margins_right() const;
+
//
virtual Transform2D get_final_transform() const override;
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index caf44ac392f6..c17bcf3a41b1 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -671,10 +671,39 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const
theme->set_constant("title_height", "Window", 36 * scale);
theme->set_constant("resize_margin", "Window", Math::round(4 * scale));
+ Ref style_decoration_button_normal = make_flat_stylebox(style_normal_color, 0, 0, 0, 0);
+ style_decoration_button_normal->set_corner_radius_all(0);
+ theme->set_stylebox("decoration_button_normal", "Window", style_decoration_button_normal);
+
+ Ref style_decoration_button_hover = make_flat_stylebox(style_hover_color, 0, 0, 0, 0);
+ style_decoration_button_hover->set_corner_radius_all(0);
+ theme->set_stylebox("decoration_button_hover", "Window", style_decoration_button_hover);
+
+ Ref style_decoration_button_pressed = make_flat_stylebox(style_pressed_color, 0, 0, 0, 0);
+ style_decoration_button_pressed->set_corner_radius_all(0);
+ theme->set_stylebox("decoration_button_pressed", "Window", style_decoration_button_pressed);
+
+ theme->set_color("decoration_button_normal_modulate", "Window", control_font_color);
+ theme->set_color("decoration_button_hover_modulate", "Window", control_font_hover_color);
+ theme->set_color("decoration_button_pressed_modulate", "Window", control_font_pressed_color);
+
+ theme->set_icon("minimize", "Window", icons["minimize"]);
+ theme->set_icon("minimize_pressed", "Window", icons["minimize_hl"]);
+ theme->set_constant("minimize_h_offset", "Window", 54 * scale);
+ theme->set_constant("minimize_v_offset", "Window", 20 * scale);
+
+ theme->set_icon("maximize", "Window", icons["maximize"]);
+ theme->set_icon("maximize_pressed", "Window", icons["maximize_hl"]);
+ theme->set_icon("maximize_disabled", "Window", icons["maximize_disabled"]);
+ theme->set_icon("restore", "Window", icons["restore"]);
+ theme->set_icon("restore_pressed", "Window", icons["restore_hl"]);
+ theme->set_constant("maximize_h_offset", "Window", 36 * scale);
+ theme->set_constant("maximize_v_offset", "Window", 20 * scale);
+
theme->set_icon("close", "Window", icons["close"]);
theme->set_icon("close_pressed", "Window", icons["close_hl"]);
theme->set_constant("close_h_offset", "Window", 18 * scale);
- theme->set_constant("close_v_offset", "Window", 24 * scale);
+ theme->set_constant("close_v_offset", "Window", 20 * scale);
// Dialogs
diff --git a/scene/theme/icons/maximize.svg b/scene/theme/icons/maximize.svg
new file mode 100644
index 000000000000..10d8afa325c7
--- /dev/null
+++ b/scene/theme/icons/maximize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/theme/icons/maximize_disabled.svg b/scene/theme/icons/maximize_disabled.svg
new file mode 100644
index 000000000000..6aad8c224b97
--- /dev/null
+++ b/scene/theme/icons/maximize_disabled.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/theme/icons/maximize_hl.svg b/scene/theme/icons/maximize_hl.svg
new file mode 100644
index 000000000000..109b86b3976e
--- /dev/null
+++ b/scene/theme/icons/maximize_hl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/theme/icons/minimize.svg b/scene/theme/icons/minimize.svg
new file mode 100644
index 000000000000..6152aba9b933
--- /dev/null
+++ b/scene/theme/icons/minimize.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/theme/icons/minimize_hl.svg b/scene/theme/icons/minimize_hl.svg
new file mode 100644
index 000000000000..7c2d50800ecc
--- /dev/null
+++ b/scene/theme/icons/minimize_hl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/theme/icons/restore.svg b/scene/theme/icons/restore.svg
new file mode 100644
index 000000000000..28a3d3700d3e
--- /dev/null
+++ b/scene/theme/icons/restore.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/theme/icons/restore_hl.svg b/scene/theme/icons/restore_hl.svg
new file mode 100644
index 000000000000..a24d0f9f453e
--- /dev/null
+++ b/scene/theme/icons/restore_hl.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/servers/display_server.h b/servers/display_server.h
index 36798bd011cd..992337feba7c 100644
--- a/servers/display_server.h
+++ b/servers/display_server.h
@@ -473,6 +473,7 @@ class DisplayServer : public Object {
virtual void window_set_window_buttons_offset(const Vector2i &p_offset, WindowID p_window = MAIN_WINDOW_ID) {}
virtual Vector3i window_get_safe_title_margins(WindowID p_window = MAIN_WINDOW_ID) const { return Vector3i(); }
+ virtual bool window_is_extend_to_title_show_window_buttons() const { return false; }
virtual bool window_can_draw(WindowID p_window = MAIN_WINDOW_ID) const = 0;