Merge pull request #117203 from kitbdev/sc-ed3d

Use SplitContainer for Node3D Editor viewports
This commit is contained in:
Thaddeus Crews 2026-03-09 15:18:20 -05:00
commit fbc00f1f84
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
6 changed files with 130 additions and 309 deletions

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="34"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-opacity=".4" stroke-width="2" d="M32 2V32M2 2h60"/></svg>

Before

Width:  |  Height:  |  Size: 183 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="64"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-opacity=".4" stroke-width="2" d="M32 32H2m30-30v60"/></svg>

Before

Width:  |  Height:  |  Size: 185 B

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><path fill="none" stroke="#fff" stroke-linecap="round" stroke-opacity=".4" stroke-width="2" d="M32 2v60M2 32h60"/></svg>

Before

Width:  |  Height:  |  Size: 184 B

View file

@ -6128,317 +6128,139 @@ Node3DEditorViewport::~Node3DEditorViewport() {
//////////////////////////////////////////////////////////////
void Node3DEditorViewportContainer::gui_input(const Ref<InputEvent> &p_event) {
ERR_FAIL_COND(p_event.is_null());
if (_redirect_freelook_input(p_event)) {
void Node3DEditorViewportContainer::_update_split_drag_margin() {
if (view != VIEW_USE_4_VIEWPORTS && view != VIEW_USE_3_VIEWPORTS) {
return;
}
// Also for 3 viewports view since the first split container is used to remember the offset.
first_split->set_split_offset(second_split->get_split_offset());
Ref<InputEventMouseButton> mb = p_event;
if (mb.is_valid() && mb->get_button_index() == MouseButton::LEFT) {
if (mb->is_pressed()) {
Vector2 size = get_size();
int h_sep = get_theme_constant(SNAME("separation"), SNAME("HSplitContainer"));
int v_sep = get_theme_constant(SNAME("separation"), SNAME("VSplitContainer"));
int mid_w = size.width * ratio_h;
int mid_h = size.height * ratio_v;
dragging_h = mb->get_position().x >= (mid_w - h_sep / 2) && mb->get_position().x < (mid_w + h_sep / 2);
dragging_v = mb->get_position().y >= (mid_h - v_sep / 2) && mb->get_position().y < (mid_h + v_sep / 2);
drag_begin_pos = mb->get_position();
drag_begin_ratio.x = ratio_h;
drag_begin_ratio.y = ratio_v;
switch (view) {
case VIEW_USE_1_VIEWPORT: {
dragging_h = false;
dragging_v = false;
} break;
case VIEW_USE_2_VIEWPORTS: {
dragging_h = false;
} break;
case VIEW_USE_2_VIEWPORTS_ALT: {
dragging_v = false;
} break;
case VIEW_USE_3_VIEWPORTS:
case VIEW_USE_3_VIEWPORTS_ALT:
case VIEW_USE_4_VIEWPORTS: {
// Do nothing.
} break;
}
} else {
dragging_h = false;
dragging_v = false;
}
}
Ref<InputEventMouseMotion> mm = p_event;
if (mm.is_valid()) {
if (view == VIEW_USE_3_VIEWPORTS || view == VIEW_USE_3_VIEWPORTS_ALT || view == VIEW_USE_4_VIEWPORTS) {
Vector2 size = get_size();
int h_sep = get_theme_constant(SNAME("separation"), SNAME("HSplitContainer"));
int v_sep = get_theme_constant(SNAME("separation"), SNAME("VSplitContainer"));
int mid_w = size.width * ratio_h;
int mid_h = size.height * ratio_v;
bool was_hovering_h = hovering_h;
bool was_hovering_v = hovering_v;
hovering_h = mm->get_position().x >= (mid_w - h_sep / 2) && mm->get_position().x < (mid_w + h_sep / 2);
hovering_v = mm->get_position().y >= (mid_h - v_sep / 2) && mm->get_position().y < (mid_h + v_sep / 2);
if (was_hovering_h != hovering_h || was_hovering_v != hovering_v) {
queue_redraw();
}
}
if (dragging_h) {
real_t new_ratio = drag_begin_ratio.x + (mm->get_position().x - drag_begin_pos.x) / get_size().width;
new_ratio = CLAMP(new_ratio, 40 / get_size().width, (get_size().width - 40) / get_size().width);
ratio_h = new_ratio;
queue_sort();
queue_redraw();
}
if (dragging_v) {
real_t new_ratio = drag_begin_ratio.y + (mm->get_position().y - drag_begin_pos.y) / get_size().height;
new_ratio = CLAMP(new_ratio, 40 / get_size().height, (get_size().height - 40) / get_size().height);
ratio_v = new_ratio;
queue_sort();
queue_redraw();
}
if (view == VIEW_USE_4_VIEWPORTS) {
// Extend to cover the first split on top.
second_split->set_drag_area_margin_begin(second_split->get_size().y - get_size().y);
}
}
void Node3DEditorViewportContainer::_notification(int p_what) {
switch (p_what) {
case NOTIFICATION_MOUSE_ENTER:
case NOTIFICATION_MOUSE_EXIT: {
mouseover = (p_what == NOTIFICATION_MOUSE_ENTER);
queue_redraw();
} break;
case NOTIFICATION_DRAW: {
if (mouseover && Input::get_singleton()->get_mouse_mode() != Input::MouseMode::MOUSE_MODE_CAPTURED) {
Ref<Texture2D> h_grabber = get_theme_icon(SNAME("grabber"), SNAME("HSplitContainer"));
Ref<Texture2D> v_grabber = get_theme_icon(SNAME("grabber"), SNAME("VSplitContainer"));
Ref<Texture2D> hdiag_grabber = get_editor_theme_icon(SNAME("GuiViewportHdiagsplitter"));
Ref<Texture2D> vdiag_grabber = get_editor_theme_icon(SNAME("GuiViewportVdiagsplitter"));
Ref<Texture2D> vh_grabber = get_editor_theme_icon(SNAME("GuiViewportVhsplitter"));
Vector2 size = get_size();
int h_sep = get_theme_constant(SNAME("separation"), SNAME("HSplitContainer"));
int v_sep = get_theme_constant(SNAME("separation"), SNAME("VSplitContainer"));
int mid_w = size.width * ratio_h;
int mid_h = size.height * ratio_v;
int size_left = mid_w - h_sep / 2;
int size_bottom = size.height - mid_h - v_sep / 2;
switch (view) {
case VIEW_USE_1_VIEWPORT: {
// Nothing to show.
} break;
case VIEW_USE_2_VIEWPORTS: {
draw_texture(v_grabber, Vector2((size.width - v_grabber->get_width()) / 2, mid_h - v_grabber->get_height() / 2));
set_default_cursor_shape(CURSOR_VSPLIT);
} break;
case VIEW_USE_2_VIEWPORTS_ALT: {
draw_texture(h_grabber, Vector2(mid_w - h_grabber->get_width() / 2, (size.height - h_grabber->get_height()) / 2));
set_default_cursor_shape(CURSOR_HSPLIT);
} break;
case VIEW_USE_3_VIEWPORTS: {
if ((hovering_v && hovering_h && !dragging_v && !dragging_h) || (dragging_v && dragging_h)) {
draw_texture(hdiag_grabber, Vector2(mid_w - hdiag_grabber->get_width() / 2, mid_h - v_grabber->get_height() / 4));
set_default_cursor_shape(CURSOR_DRAG);
} else if ((hovering_v && !dragging_h) || dragging_v) {
draw_texture(v_grabber, Vector2((size.width - v_grabber->get_width()) / 2, mid_h - v_grabber->get_height() / 2));
set_default_cursor_shape(CURSOR_VSPLIT);
} else if (hovering_h || dragging_h) {
draw_texture(h_grabber, Vector2(mid_w - h_grabber->get_width() / 2, mid_h + v_grabber->get_height() / 2 + (size_bottom - h_grabber->get_height()) / 2));
set_default_cursor_shape(CURSOR_HSPLIT);
}
} break;
case VIEW_USE_3_VIEWPORTS_ALT: {
if ((hovering_v && hovering_h && !dragging_v && !dragging_h) || (dragging_v && dragging_h)) {
draw_texture(vdiag_grabber, Vector2(mid_w - vdiag_grabber->get_width() + v_grabber->get_height() / 4, mid_h - vdiag_grabber->get_height() / 2));
set_default_cursor_shape(CURSOR_DRAG);
} else if ((hovering_v && !dragging_h) || dragging_v) {
draw_texture(v_grabber, Vector2((size_left - v_grabber->get_width()) / 2, mid_h - v_grabber->get_height() / 2));
set_default_cursor_shape(CURSOR_VSPLIT);
} else if (hovering_h || dragging_h) {
draw_texture(h_grabber, Vector2(mid_w - h_grabber->get_width() / 2, (size.height - h_grabber->get_height()) / 2));
set_default_cursor_shape(CURSOR_HSPLIT);
}
} break;
case VIEW_USE_4_VIEWPORTS: {
Vector2 half(mid_w, mid_h);
if ((hovering_v && hovering_h && !dragging_v && !dragging_h) || (dragging_v && dragging_h)) {
draw_texture(vh_grabber, half - vh_grabber->get_size() / 2.0);
set_default_cursor_shape(CURSOR_DRAG);
} else if ((hovering_v && !dragging_h) || dragging_v) {
draw_texture(v_grabber, half - v_grabber->get_size() / 2.0);
set_default_cursor_shape(CURSOR_VSPLIT);
} else if (hovering_h || dragging_h) {
draw_texture(h_grabber, half - h_grabber->get_size() / 2.0);
set_default_cursor_shape(CURSOR_HSPLIT);
}
} break;
}
}
} break;
case NOTIFICATION_SORT_CHILDREN: {
Node3DEditorViewport *viewports[4];
int vc = 0;
for (int i = 0; i < get_child_count(); i++) {
viewports[vc] = Object::cast_to<Node3DEditorViewport>(get_child(i));
if (viewports[vc]) {
vc++;
}
}
ERR_FAIL_COND(vc != 4);
Size2 size = get_size();
if (size.x < 10 || size.y < 10) {
for (int i = 0; i < 4; i++) {
viewports[i]->hide();
}
case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: {
if (!EditorSettings::get_singleton()->check_changed_settings_in_group("interface/touchscreen")) {
return;
}
int h_sep = get_theme_constant(SNAME("separation"), SNAME("HSplitContainer"));
int v_sep = get_theme_constant(SNAME("separation"), SNAME("VSplitContainer"));
int mid_w = size.width * ratio_h;
int mid_h = size.height * ratio_v;
int size_left = mid_w - h_sep / 2;
int size_right = size.width - mid_w - h_sep / 2;
int size_top = mid_h - v_sep / 2;
int size_bottom = size.height - mid_h - v_sep / 2;
switch (view) {
case VIEW_USE_1_VIEWPORT: {
viewports[0]->show();
for (int i = 1; i < 4; i++) {
viewports[i]->hide();
}
fit_child_in_rect(viewports[0], Rect2(Vector2(), size));
} break;
case VIEW_USE_2_VIEWPORTS: {
for (int i = 0; i < 4; i++) {
if (i == 1 || i == 3) {
viewports[i]->hide();
} else {
viewports[i]->show();
}
}
fit_child_in_rect(viewports[0], Rect2(Vector2(), Vector2(size.width, size_top)));
fit_child_in_rect(viewports[2], Rect2(Vector2(0, mid_h + v_sep / 2), Vector2(size.width, size_bottom)));
} break;
case VIEW_USE_2_VIEWPORTS_ALT: {
for (int i = 0; i < 4; i++) {
if (i == 1 || i == 3) {
viewports[i]->hide();
} else {
viewports[i]->show();
}
}
fit_child_in_rect(viewports[0], Rect2(Vector2(), Vector2(size_left, size.height)));
fit_child_in_rect(viewports[2], Rect2(Vector2(mid_w + h_sep / 2, 0), Vector2(size_right, size.height)));
} break;
case VIEW_USE_3_VIEWPORTS: {
for (int i = 0; i < 4; i++) {
if (i == 1) {
viewports[i]->hide();
} else {
viewports[i]->show();
}
}
fit_child_in_rect(viewports[0], Rect2(Vector2(), Vector2(size.width, size_top)));
fit_child_in_rect(viewports[2], Rect2(Vector2(0, mid_h + v_sep / 2), Vector2(size_left, size_bottom)));
fit_child_in_rect(viewports[3], Rect2(Vector2(mid_w + h_sep / 2, mid_h + v_sep / 2), Vector2(size_right, size_bottom)));
} break;
case VIEW_USE_3_VIEWPORTS_ALT: {
for (int i = 0; i < 4; i++) {
if (i == 1) {
viewports[i]->hide();
} else {
viewports[i]->show();
}
}
fit_child_in_rect(viewports[0], Rect2(Vector2(), Vector2(size_left, size_top)));
fit_child_in_rect(viewports[2], Rect2(Vector2(0, mid_h + v_sep / 2), Vector2(size_left, size_bottom)));
fit_child_in_rect(viewports[3], Rect2(Vector2(mid_w + h_sep / 2, 0), Vector2(size_right, size.height)));
} break;
case VIEW_USE_4_VIEWPORTS: {
for (int i = 0; i < 4; i++) {
viewports[i]->show();
}
fit_child_in_rect(viewports[0], Rect2(Vector2(), Vector2(size_left, size_top)));
fit_child_in_rect(viewports[1], Rect2(Vector2(mid_w + h_sep / 2, 0), Vector2(size_right, size_top)));
fit_child_in_rect(viewports[2], Rect2(Vector2(0, mid_h + v_sep / 2), Vector2(size_left, size_bottom)));
fit_child_in_rect(viewports[3], Rect2(Vector2(mid_w + h_sep / 2, mid_h + v_sep / 2), Vector2(size_right, size_bottom)));
} break;
}
[[fallthrough]];
}
case NOTIFICATION_READY: {
bool touch_optimizations = EDITOR_GET("interface/touchscreen/enable_touch_optimizations");
main_split->set_touch_dragger_enabled(touch_optimizations);
first_split->set_touch_dragger_enabled(touch_optimizations);
second_split->set_touch_dragger_enabled(touch_optimizations);
} break;
}
}
void Node3DEditorViewportContainer::set_view(View p_view) {
view = p_view;
queue_sort();
Node3DEditorViewport *viewports[4];
for (uint32_t i = 0; i < 4; i++) {
viewports[i] = Node3DEditor::get_singleton()->get_editor_viewport(i);
ERR_FAIL_NULL(viewports[i]);
}
const bool previous_main_vertical = !first_split->is_vertical();
const float horizontal_offset = previous_main_vertical ? first_split->get_split_offset() : main_split->get_split_offset();
const float vertical_offset = previous_main_vertical ? main_split->get_split_offset() : first_split->get_split_offset();
first_split->set_dragging_enabled(true);
second_split->set_drag_area_margin_begin(0);
viewports[0]->show();
switch (view) {
case VIEW_USE_1_VIEWPORT: {
for (int i = 1; i < 4; i++) {
viewports[i]->hide();
}
second_split->hide();
} break;
case VIEW_USE_2_VIEWPORTS:
case VIEW_USE_2_VIEWPORTS_ALT: {
viewports[1]->show();
viewports[2]->hide();
viewports[3]->hide();
second_split->hide();
const bool is_vertical = view == VIEW_USE_2_VIEWPORTS;
if (first_split->is_vertical() != is_vertical) {
first_split->set_vertical(is_vertical);
first_split->set_split_offset(is_vertical ? vertical_offset : horizontal_offset);
main_split->set_split_offset(is_vertical ? horizontal_offset : vertical_offset); // Store the other offset here for later.
}
} break;
case VIEW_USE_3_VIEWPORTS:
case VIEW_USE_3_VIEWPORTS_ALT: {
// Default mode has two on bottom (second_split). Alt mode has two on the left (first_split).
const bool main_vertical = view == VIEW_USE_3_VIEWPORTS;
viewports[1]->set_visible(!main_vertical);
viewports[2]->show();
viewports[3]->set_visible(main_vertical);
second_split->show();
main_split->set_vertical(main_vertical);
main_split->set_split_offset(main_vertical ? vertical_offset : horizontal_offset);
first_split->set_vertical(!main_vertical);
first_split->set_split_offset(main_vertical ? horizontal_offset : vertical_offset);
second_split->set_split_offset(main_vertical ? horizontal_offset : vertical_offset);
} break;
case VIEW_USE_4_VIEWPORTS: {
for (int i = 1; i < 4; i++) {
viewports[i]->show();
}
second_split->show();
main_split->set_vertical(true);
main_split->set_split_offset(vertical_offset);
first_split->set_vertical(false);
first_split->set_split_offset(horizontal_offset);
second_split->set_split_offset(horizontal_offset);
first_split->set_dragging_enabled(false);
_update_split_drag_margin();
} break;
}
}
Node3DEditorViewportContainer::View Node3DEditorViewportContainer::get_view() {
return view;
}
void Node3DEditorViewportContainer::add_viewport(Node3DEditorViewport *p_viewport, int p_index) {
if (p_index <= 1) {
first_split->add_child(p_viewport);
} else {
second_split->add_child(p_viewport);
if (p_index == 3) {
// Connect to the second split's child to update the drag margin immediately whenever the split offset changes.
p_viewport->connect(SceneStringName(resized), callable_mp(this, &Node3DEditorViewportContainer::_update_split_drag_margin));
}
}
}
Node3DEditorViewportContainer::Node3DEditorViewportContainer() {
set_clip_contents(true);
view = VIEW_USE_1_VIEWPORT;
mouseover = false;
ratio_h = 0.5;
ratio_v = 0.5;
hovering_v = false;
hovering_h = false;
dragging_v = false;
dragging_h = false;
main_split = memnew(SplitContainer);
main_split->set_drag_nested_intersections(true);
first_split = memnew(SplitContainer);
first_split->set_h_size_flags(SIZE_EXPAND_FILL);
first_split->set_v_size_flags(SIZE_EXPAND_FILL);
first_split->set_drag_nested_intersections(true);
second_split = memnew(SplitContainer);
second_split->set_h_size_flags(SIZE_EXPAND_FILL);
second_split->set_v_size_flags(SIZE_EXPAND_FILL);
second_split->set_drag_nested_intersections(true);
main_split->add_child(first_split);
main_split->add_child(second_split);
add_child(main_split);
}
///////////////////////////////////////////////////////////////////
@ -9752,7 +9574,10 @@ Node3DEditor::Node3DEditor() {
viewports[i]->connect("toggle_maximize_view", callable_mp(this, &Node3DEditor::_toggle_maximize_view));
viewports[i]->connect("clicked", callable_mp(this, &Node3DEditor::_viewport_clicked).bind(i));
viewports[i]->assign_pending_data_pointers(preview_node, &preview_bounds, accept);
viewport_base->add_child(viewports[i]);
viewports[i]->set_h_size_flags(SIZE_EXPAND_FILL);
viewports[i]->set_v_size_flags(SIZE_EXPAND_FILL);
viewports[i]->set_custom_minimum_size(Size2(39, 39));
viewport_base->add_viewport(viewports[i], i);
}
/* SNAP DIALOG */

View file

@ -37,6 +37,7 @@
#include "scene/debugger/view_3d_controller.h"
#include "scene/gui/box_container.h"
#include "scene/gui/button.h"
#include "scene/gui/margin_container.h"
#include "scene/gui/spin_box.h"
#include "scene/resources/gradient.h"
#include "scene/resources/immediate_mesh.h"
@ -58,6 +59,7 @@ class OptionButton;
class PanelContainer;
class ProceduralSkyMaterial;
class RichTextLabel;
class SplitContainer;
class SubViewport;
class SubViewportContainer;
class VSeparator;
@ -519,8 +521,8 @@ public:
~Node3DEditorSelectedItem();
};
class Node3DEditorViewportContainer : public Container {
GDCLASS(Node3DEditorViewportContainer, Container);
class Node3DEditorViewportContainer : public MarginContainer {
GDCLASS(Node3DEditorViewportContainer, MarginContainer);
public:
enum View {
@ -533,20 +535,12 @@ public:
};
private:
View view;
bool mouseover;
real_t ratio_h;
real_t ratio_v;
View view = VIEW_USE_1_VIEWPORT;
SplitContainer *main_split = nullptr;
SplitContainer *first_split = nullptr;
SplitContainer *second_split = nullptr;
bool hovering_v;
bool hovering_h;
bool dragging_v;
bool dragging_h;
Vector2 drag_begin_pos;
Vector2 drag_begin_ratio;
virtual void gui_input(const Ref<InputEvent> &p_event) override;
void _update_split_drag_margin();
protected:
void _notification(int p_what);
@ -555,6 +549,8 @@ public:
void set_view(View p_view);
View get_view();
void add_viewport(Node3DEditorViewport *p_viewport, int p_index);
Node3DEditorViewportContainer();
};

View file

@ -302,6 +302,7 @@ void SplitContainerDragger::update_touch_dragger() {
touch_dragger->set_texture(sc->_get_touch_dragger_icon());
touch_dragger->set_anchors_and_offsets_preset(Control::PRESET_CENTER);
touch_dragger->set_default_cursor_shape(sc->vertical ? CURSOR_VSPLIT : CURSOR_HSPLIT);
touch_dragger->set_visible(sc->dragging_enabled);
}
void SplitContainerDragger::stop_dragging() {
@ -850,9 +851,6 @@ void SplitContainer::_resort() {
}
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->set_visible(!collapsed);
if (touch_dragger_enabled) {
dragger->touch_dragger->set_visible(dragging_enabled);
}
}
_update_default_dragger_positions();
@ -1483,6 +1481,11 @@ void SplitContainer::set_dragging_enabled(bool p_enabled) {
dragger->stop_dragging();
}
}
if (touch_dragger_enabled) {
for (SplitContainerDragger *dragger : dragging_area_controls) {
dragger->update_touch_dragger();
}
}
if (get_viewport()) {
get_viewport()->update_mouse_cursor_state();
}