diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index 79064a88ba6c..ff779120ade9 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -1504,6 +1504,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 amd Windows, this always returns (0, 0) since the window buttons are not displayed by the OS when [constant WINDOW_FLAG_EXTEND_TO_TITLE] flag is set. @@ -1866,7 +1867,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 and Windows[/b] Display server supports reading screen pixels. See [method screen_get_pixel]. @@ -2087,7 +2088,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 only 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 Window theme. 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 1de63b4a3997..4df5d36efff5 100644 --- a/doc/classes/EditorSettings.xml +++ b/doc/classes/EditorSettings.xml @@ -658,7 +658,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] Only supported 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 497070fa81de..781c0447b079 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -846,7 +846,8 @@ 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 only on macOS, Linux and Windows and when [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 Window theme. 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..70f376b02434 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 only on macOS 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 only on macOS and Windows. [b]Note:[/b] This flag has no effect in embedded windows. + [b]Note:[/b] On Windows, the window buttons (minimize, maximize and close) are not displayed. All mouse events are passed to the underlying window of the same application. @@ -907,6 +908,18 @@ + + Window button modulation color, when the button is hovered. + [b]Note:[/b] Used only when extend to title is active. + + + Window button modulation color. + [b]Note:[/b] Used only when extend to title is active. + + + Window button modulation color, when the button is pressed. + [b]Note:[/b] Used only when extend to title is active. + The color of the title's text. @@ -916,9 +929,25 @@ Horizontal position offset of the close button. - + Vertical position offset of the close button. + + Horizontal position offset of the maximize/restore button. + [b]Note:[/b] Used only when extend to title is active. + + + Vertical position offset of the maximize/restore button. + [b]Note:[/b] Used only when extend to title is active. + + + Horizontal position offset of the minimize button. + [b]Note:[/b] Used only when extend to title is active. + + + Vertical position offset of the minimize button. + [b]Note:[/b] Used only when extend to title is active. + Defines the outside margin at which the window border can be grabbed with mouse and resized. @@ -940,6 +969,46 @@ The icon for the close button when it's being pressed. + + The icon for the maximize button. + [b]Note:[/b] Used only when extend to title is active. + + + The icon for the maximize button when disabled (window not resizable). + [b]Note:[/b] Used only when extend to title is active. + + + The icon for the maximize button when it's being pressed. + [b]Note:[/b] Used only when extend to title is active. + + + The icon for the minimize button. + [b]Note:[/b] Used only when extend to title is active. + + + The icon for the minimize button when it's being pressed. + [b]Note:[/b] Used only when extend to title is active. + + + The icon for the restore button. + [b]Note:[/b] Used only when extend to title is active. + + + The icon for the restore button when it's being pressed. + [b]Note:[/b] Used only when extend to title is active. + + + The background style used of the window buttons when the mouse pointer is hovering them. + [b]Note:[/b] Used only when extend to title is active. + + + The background style used of the window buttons. + [b]Note:[/b] Used only when extend to title is active. + + + The background style used of the window buttons when they are being pressed. + [b]Note:[/b] Used only when extend to title is active. + 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 6899a35ded84..d6b1c5a4310a 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -1266,18 +1266,23 @@ 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; + } + + 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)); + + 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)); } } @@ -5411,6 +5416,11 @@ void EditorNode::_save_window_settings_to_config(Ref p_layout, const break; } + // Saving the extend to title to set the correct window flag on startup + // otherwise the window size will be corrected to a "normal" window causing + // it to move up. + p_layout->set_value(p_section, "extend_to_title", w->get_flag(Window::FLAG_EXTEND_TO_TITLE)); + p_layout->set_value(p_section, "position", w->get_position()); } } diff --git a/editor/icons/GuiMaximize.svg b/editor/icons/GuiMaximize.svg new file mode 100644 index 000000000000..9f4f377a2b23 --- /dev/null +++ b/editor/icons/GuiMaximize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/GuiMinimize.svg b/editor/icons/GuiMinimize.svg new file mode 100644 index 000000000000..df17ab7e2195 --- /dev/null +++ b/editor/icons/GuiMinimize.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/icons/GuiRestore.svg b/editor/icons/GuiRestore.svg new file mode 100644 index 000000000000..63f8320259d1 --- /dev/null +++ b/editor/icons/GuiRestore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/project_manager.cpp b/editor/project_manager.cpp index 55c361de4b84..616779818eac 100644 --- a/editor/project_manager.cpp +++ b/editor/project_manager.cpp @@ -1020,20 +1020,23 @@ 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; + } + + 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)); + + 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 119508f59b1c..9779f9395601 100644 --- a/editor/themes/editor_theme_manager.cpp +++ b/editor/themes/editor_theme_manager.cpp @@ -1271,19 +1271,49 @@ 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_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); + + p_theme->set_icon("minimize", "Window", p_theme->get_icon(SNAME("GuiMinimize"), EditorStringName(EditorIcons))); + p_theme->set_icon("minimize_pressed", "Window", p_theme->get_icon(SNAME("GuiMinimize"), EditorStringName(EditorIcons))); + p_theme->set_constant("minimize_h_offset", "Window", 108 * EDSCALE); + p_theme->set_constant("minimize_v_offset", "Window", 36 * EDSCALE); + p_theme->set_icon("maximize", "Window", p_theme->get_icon(SNAME("GuiMaximize"), EditorStringName(EditorIcons))); + p_theme->set_icon("maximize_pressed", "Window", p_theme->get_icon(SNAME("GuiMaximize"), EditorStringName(EditorIcons))); + p_theme->set_icon("maximize_disabled", "Window", p_theme->get_icon(SNAME("GuiMaximize"), EditorStringName(EditorIcons))); + p_theme->set_icon("restore", "Window", p_theme->get_icon(SNAME("GuiRestore"), EditorStringName(EditorIcons))); + p_theme->set_icon("restore_pressed", "Window", p_theme->get_icon(SNAME("GuiRestore"), EditorStringName(EditorIcons))); + p_theme->set_constant("maximize_h_offset", "Window", 72 * EDSCALE); + p_theme->set_constant("maximize_v_offset", "Window", 36 * EDSCALE); + 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", 36 * EDSCALE); + p_theme->set_constant("close_v_offset", "Window", 36 * EDSCALE); // AcceptDialog. p_theme->set_stylebox(SceneStringName(panel), "AcceptDialog", p_config.dialog_style); diff --git a/main/main.cpp b/main/main.cpp index 18ffedef1877..267e2af67acc 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -2807,6 +2807,10 @@ Error Main::setup2(bool p_show_boot_logo) { init_use_custom_pos = true; init_custom_pos = config->get_value("EditorWindow", "position", Vector2i(0, 0)); } + + if (config->get_value("EditorWindow", "extend_to_title", false)) { + window_flags |= DisplayServer::WINDOW_FLAG_EXTEND_TO_TITLE_BIT; + } } } diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 840cadace3e6..1f6e4fd689d4 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); @@ -2883,6 +2884,21 @@ 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; + } break; default: { } } @@ -2899,24 +2915,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 +2932,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 +4783,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 +4830,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 +4905,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 +4965,6 @@ void DisplayServerX11::process_events() { break; } - const WindowData &wd = windows[window_id]; bool focused = wd.focused; if (mouse_mode == MOUSE_MODE_CAPTURED) { @@ -5199,6 +5234,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 +5531,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 +5814,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 52dc51bc960d..afb2f5c947ca 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -2365,6 +2365,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 50ebe7077fdb..2cc41e345ee6 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); @@ -1719,7 +1723,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; } @@ -1773,7 +1777,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 { @@ -1893,10 +1897,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; @@ -2007,7 +2013,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; } @@ -2058,7 +2064,7 @@ Size2i DisplayServerWindows::window_get_size_with_decorations(WindowID p_window) return Size2(); } -void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, 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_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 @@ -2070,7 +2076,7 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscre r_style |= WS_VISIBLE; } - 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_maximized) { r_style |= WS_MAXIMIZE; @@ -2081,6 +2087,18 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscre 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. @@ -2101,7 +2119,7 @@ void DisplayServerWindows::_get_window_style(bool p_main_window, bool p_fullscre r_style_ex |= WS_EX_TOPMOST | WS_EX_NOACTIVATE; } - if (!p_borderless && !p_no_activate_focus) { + if ((!p_borderless || !p_extend_to_title) && !p_no_activate_focus) { r_style |= WS_VISIBLE; } @@ -2118,7 +2136,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.fullscreen, wd.multiwindow_fs, wd.borderless, wd.resizable, wd.maximized, wd.maximized_fs, wd.no_focus || wd.is_popup, style, style_ex); + _get_window_style(p_window == MAIN_WINDOW_ID, 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); @@ -2127,6 +2145,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, NULL); + 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) { @@ -2319,6 +2352,10 @@ 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); + } break; default: break; } @@ -2351,6 +2388,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; } @@ -3622,6 +3662,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) { } @@ -4007,9 +4051,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: { @@ -4050,7 +4131,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. @@ -5529,7 +5610,7 @@ DisplayServer::WindowID DisplayServerWindows::_create_window(WindowMode p_mode, DWORD dwExStyle; DWORD dwStyle; - _get_window_style(window_id_counter == MAIN_WINDOW_ID, (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_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, (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; @@ -5569,7 +5650,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); } @@ -5786,7 +5867,7 @@ 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); 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 54e1c9681ddc..2df304af30b5 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; @@ -591,7 +593,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_fullscreen, bool p_multiwindow_fs, bool p_borderless, bool p_resizable, 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_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; @@ -778,6 +780,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 aaa34a4840f4..727b6bf9cf8d 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -37,6 +37,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" @@ -823,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) { @@ -1216,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 +1395,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 +1676,13 @@ void Window::_window_input(const Ref &p_ev) { } } + if (get_flag(Window::FLAG_EXTEND_TO_TITLE)) { + 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. @@ -2799,6 +2815,228 @@ 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()); + + callable_mp(this, &Window::_update_decoration).call_deferred(); +} + +void Window::_update_decoration() { + // 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_flag(Window::FLAG_EXTEND_TO_TITLE) || mode == MODE_FULLSCREEN || MODE_WINDOWED == MODE_EXCLUSIVE_FULLSCREEN || get_flag(Window::FLAG_BORDERLESS)) { + if (decoration_canvas.is_valid()) { + // Not needed anymore. + RenderingServer::get_singleton()->free(decoration_canvas); + } + return; + } + + if (!decoration_canvas.is_valid()) { + _create_decoration_canvas(); + } + + Size2i window_size = get_size(); + RenderingServer::get_singleton()->canvas_item_clear(decoration_canvas); + Vector2i window_buttons_offset = _get_window_buttons_offset(); + + // Calculate each button emplacement. + if (is_layout_rtl()) { + close_button_rect = Rect2i(window_buttons_offset.x, window_buttons_offset.y, theme_cache.close_h_offset, theme_cache.close_v_offset); + maximize_button_rect = Rect2i(theme_cache.close_h_offset + window_buttons_offset.x, window_buttons_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 + window_buttons_offset.x, window_buttons_offset.y, theme_cache.minimize_h_offset - theme_cache.maximize_h_offset, theme_cache.minimize_v_offset); + } else { + minimize_button_rect = Rect2i(window_size.x - theme_cache.minimize_h_offset - window_buttons_offset.x, window_buttons_offset.y, theme_cache.minimize_h_offset - theme_cache.maximize_h_offset, theme_cache.minimize_v_offset); + maximize_button_rect = Rect2i(window_size.x - theme_cache.maximize_h_offset - window_buttons_offset.x, window_buttons_offset.y, theme_cache.maximize_h_offset - theme_cache.close_h_offset, theme_cache.maximize_v_offset); + close_button_rect = Rect2i(window_size.x - theme_cache.close_h_offset - window_buttons_offset.x, window_buttons_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); +} + +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 { + // Window buttons are managed by ourself. + if (is_layout_rtl()) { + 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(); + } + } +} + +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 { + // Window buttons are managed by ourself. + if (is_layout_rtl()) { + return Vector2i(); + } else { + 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(); + } + } +} + 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); @@ -3082,6 +3320,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); @@ -3089,6 +3335,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); @@ -3109,6 +3368,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 33d593711f23..73645be566e6 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,23 @@ 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(); + 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() {} @@ -319,6 +365,7 @@ class Window : public Viewport { void show(); void hide(); + void send_close_request(); void set_transient(bool p_transient); bool is_transient() const; @@ -468,6 +515,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 749d4e35300b..f0295e8b6a6c 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 04f4b0c03d25..2c18654d07da 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;