Skip to content

Commit 780a344

Browse files
Implement automatic snapping in split containers so that when the user drags the splitter beyond a threshold after the minimum size of a child, it "snaps" to the appropriate edge (left/right for horizontal arrangement, top/bottom for vertical arrangement), causing the child look as if it disappeared. The splitter can still be dragged back to reveal the child.
Visually this looks similar to collapsing but functionally it differs as it only affects the visual state of the nodes.
1 parent 09fcbb8 commit 780a344

File tree

7 files changed

+194
-15
lines changed

7 files changed

+194
-15
lines changed

doc/classes/SplitContainer.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
</method>
3030
</methods>
3131
<members>
32+
<member name="auto_snap" type="int" setter="set_auto_snap" getter="get_auto_snap" enum="SplitContainer.Snap" default="3">
33+
Determines the edges on which the dragger will auto-snap. See [enum Snap] for details.
34+
</member>
3235
<member name="collapsed" type="bool" setter="set_collapsed" getter="is_collapsed" default="false">
3336
If [code]true[/code], the dragger will be disabled and the children will be sized as if the [member split_offset] was [code]0[/code].
3437
</member>
@@ -50,6 +53,9 @@
5053
<member name="dragging_enabled" type="bool" setter="set_dragging_enabled" getter="is_dragging_enabled" default="true">
5154
Enables or disables split dragging.
5255
</member>
56+
<member name="snap_state" type="int" setter="set_snap_state" getter="get_snap_state" enum="SplitContainer.Snap" default="0">
57+
The current snap state. If [constant SNAP_NONE] the dragger is not snapped. If [constant SNAP_FIRST] or [constant SNAP_SECOND] the dragger is snapped to the edge where the first or second child lies on.
58+
</member>
5359
<member name="split_offset" type="int" setter="set_split_offset" getter="get_split_offset" default="0">
5460
The initial offset of the splitting between the two [Control]s, with [code]0[/code] being at the end of the first [Control].
5561
</member>
@@ -75,6 +81,11 @@
7581
Emitted when the dragger is dragged by user.
7682
</description>
7783
</signal>
84+
<signal name="snap_state_changed">
85+
<description>
86+
Wmitted when the snap state changes.
87+
</description>
88+
</signal>
7889
</signals>
7990
<constants>
8091
<constant name="DRAGGER_VISIBLE" value="0" enum="DraggerVisibility">
@@ -89,6 +100,18 @@
89100
<constant name="DRAGGER_HIDDEN_COLLAPSED" value="2" enum="DraggerVisibility">
90101
The split dragger icon is not visible, and the split bar is collapsed to zero thickness.
91102
</constant>
103+
<constant name="SNAP_NONE" value="0" enum="Snap">
104+
The dragger will never snap automatically to any of the container's edges.
105+
</constant>
106+
<constant name="SNAP_FIRST" value="1" enum="Snap">
107+
The dragger will snap automatically to the edge on which the first child lies when the user attempts to resize the child less than a third of its minimum size. If [member vertical] is [code]true[/code], this is the top edge. Otherwise it is the left edge.
108+
</constant>
109+
<constant name="SNAP_SECOND" value="2" enum="Snap">
110+
The dragger will snap automatically to the edge on which the second child lies when the user attempts to resize the child less than a third of the its minimum size. If [member vertical] is [code]true[/code], this is the bottom edge. Otherwise it is the right edge.
111+
</constant>
112+
<constant name="SNAP_BOTH" value="3" enum="Snap">
113+
Equivalent to setting both [constant SNAP_FIRST] and [constant SNAP_SECOND]. This is the default value for [member auto_snap].
114+
</constant>
92115
</constants>
93116
<theme_items>
94117
<theme_item name="autohide" data_type="constant" type="int" default="1">

editor/editor_dock_manager.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,11 +514,13 @@ void EditorDockManager::save_docks_to_config(Ref<ConfigFile> p_layout, const Str
514514
for (int i = 0; i < vsplits.size(); i++) {
515515
if (vsplits[i]->is_visible_in_tree()) {
516516
p_layout->set_value(p_section, "dock_split_" + itos(i + 1), vsplits[i]->get_split_offset());
517+
p_layout->set_value(p_section, "dock_split_snap_" + itos(i + 1), (int)vsplits[i]->get_snap_state());
517518
}
518519
}
519520

520521
for (int i = 0; i < hsplits.size(); i++) {
521522
p_layout->set_value(p_section, "dock_hsplit_" + itos(i + 1), int(hsplits[i]->get_split_offset() / EDSCALE));
523+
p_layout->set_value(p_section, "dock_hsplit_snap_" + itos(i + 1), (int)hsplits[i]->get_snap_state());
522524
}
523525
}
524526

@@ -607,6 +609,11 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S
607609
}
608610
int ofs = p_layout->get_value(p_section, "dock_split_" + itos(i + 1));
609611
vsplits[i]->set_split_offset(ofs);
612+
if (!p_layout->has_section_key(p_section, "dock_split_snap_" + itos(i + 1))) {
613+
continue;
614+
}
615+
SplitContainer::Snap snap = (SplitContainer::Snap)p_layout->get_value(p_section, "dock_split_snap_" + itos(i + 1));
616+
vsplits[i]->set_snap_state(snap);
610617
}
611618

612619
for (int i = 0; i < hsplits.size(); i++) {
@@ -615,6 +622,11 @@ void EditorDockManager::load_docks_from_config(Ref<ConfigFile> p_layout, const S
615622
}
616623
int ofs = p_layout->get_value(p_section, "dock_hsplit_" + itos(i + 1));
617624
hsplits[i]->set_split_offset(ofs * EDSCALE);
625+
if (!p_layout->has_section_key(p_section, "dock_hsplit_snap_" + itos(i + 1))) {
626+
continue;
627+
}
628+
SplitContainer::Snap snap = (SplitContainer::Snap)p_layout->get_value(p_section, "dock_hsplit_snap_" + itos(i + 1));
629+
hsplits[i]->set_snap_state(snap);
618630
}
619631
update_docks_menu();
620632
}

editor/editor_node.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8131,11 +8131,16 @@ EditorNode::EditorNode() {
81318131
// There are 4 vsplits and 4 hsplits.
81328132
for (int i = 0; i < editor_dock_manager->get_vsplit_count(); i++) {
81338133
default_layout->set_value(docks_section, "dock_split_" + itos(i + 1), 0);
8134+
default_layout->set_value(docks_section, "dock_split_snap_" + itos(i + 1), 0);
81348135
}
81358136
default_layout->set_value(docks_section, "dock_hsplit_1", 0);
81368137
default_layout->set_value(docks_section, "dock_hsplit_2", 270);
81378138
default_layout->set_value(docks_section, "dock_hsplit_3", -270);
81388139
default_layout->set_value(docks_section, "dock_hsplit_4", 0);
8140+
default_layout->set_value(docks_section, "dock_hsplit_snap_1", 0);
8141+
default_layout->set_value(docks_section, "dock_hsplit_snap_2", 0);
8142+
default_layout->set_value(docks_section, "dock_hsplit_snap_3", 0);
8143+
default_layout->set_value(docks_section, "dock_hsplit_snap_4", 0);
81398144

81408145
_update_layouts_menu();
81418146

editor/gui/editor_bottom_panel.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@
4444

4545
void EditorBottomPanel::_notification(int p_what) {
4646
switch (p_what) {
47+
case NOTIFICATION_ENTER_TREE: {
48+
SplitContainer *center_split = Object::cast_to<SplitContainer>(get_parent());
49+
ERR_FAIL_NULL(center_split);
50+
center_split->connect(SNAME("drag_ended"), callable_mp(this, &EditorBottomPanel::_center_split_drag_ended));
51+
} break;
52+
53+
case NOTIFICATION_EXIT_TREE: {
54+
SplitContainer *center_split = Object::cast_to<SplitContainer>(get_parent());
55+
ERR_FAIL_NULL(center_split);
56+
center_split->disconnect(SNAME("drag_ended"), callable_mp(this, &EditorBottomPanel::_center_split_drag_ended));
57+
} break;
58+
4759
case NOTIFICATION_THEME_CHANGED: {
4860
pin_button->set_button_icon(get_editor_theme_icon(SNAME("Pin")));
4961
expand_button->set_button_icon(get_editor_theme_icon(SNAME("ExpandBottomDock")));
@@ -100,6 +112,31 @@ void EditorBottomPanel::_update_disabled_buttons() {
100112
right_button->set_disabled(h_scroll->get_value() + h_scroll->get_page() == h_scroll->get_max());
101113
}
102114

115+
void EditorBottomPanel::_center_split_drag_started() {
116+
SplitContainer *center_split = Object::cast_to<SplitContainer>(get_parent());
117+
ERR_FAIL_NULL(center_split);
118+
119+
// Save the split offset so that it can be restored when snapping to top edge
120+
center_split_start_split_offset = center_split->get_split_offset();
121+
}
122+
123+
void EditorBottomPanel::_center_split_drag_ended() {
124+
SplitContainer *center_split = Object::cast_to<SplitContainer>(get_parent());
125+
ERR_FAIL_NULL(center_split);
126+
127+
// Reset the snap state after the drag ends to either expanding the bottom panel (when the splitter is snapped
128+
// at the top edge) or to collapsing the bottom panel (when the splitter is snapped at the bottom edge)
129+
if (center_split->get_snap_state() == SplitContainer::SNAP_FIRST) {
130+
center_split->set_snap_state(SplitContainer::SNAP_NONE);
131+
center_split->set_split_offset(center_split_start_split_offset);
132+
expand_button->set_pressed_no_signal(true);
133+
EditorNode::get_top_split()->set_visible(false);
134+
} else if (center_split->get_snap_state() == SplitContainer::SNAP_SECOND) {
135+
center_split->set_snap_state(SplitContainer::SNAP_NONE);
136+
_switch_by_control(false, last_opened_control);
137+
}
138+
}
139+
103140
void EditorBottomPanel::_switch_to_item(bool p_visible, int p_idx, bool p_ignore_lock) {
104141
ERR_FAIL_INDEX(p_idx, items.size());
105142

editor/gui/editor_bottom_panel.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class EditorBottomPanel : public PanelContainer {
5050

5151
Vector<BottomPanelItem> items;
5252
bool lock_panel_switching = false;
53+
int center_split_start_split_offset = 0;
5354

5455
VBoxContainer *item_vbox = nullptr;
5556
HBoxContainer *bottom_hbox = nullptr;
@@ -69,6 +70,8 @@ class EditorBottomPanel : public PanelContainer {
6970
void _scroll(bool p_right);
7071
void _update_scroll_buttons();
7172
void _update_disabled_buttons();
73+
void _center_split_drag_started();
74+
void _center_split_drag_ended();
7275

7376
bool _button_drag_hover(const Vector2 &, const Variant &, Button *p_button, Control *p_control);
7477

scene/gui/split_container.cpp

Lines changed: 96 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,7 @@ void SplitContainerDragger::gui_input(const Ref<InputEvent> &p_event) {
5757
drag_from = get_transform().xform(mb->get_position()).x;
5858
}
5959
} else {
60-
dragging = false;
61-
queue_redraw();
62-
sc->emit_signal(SNAME("drag_ended"));
60+
_end_dragging();
6361
}
6462
}
6563
}
@@ -83,6 +81,13 @@ void SplitContainerDragger::gui_input(const Ref<InputEvent> &p_event) {
8381
}
8482
}
8583

84+
void SplitContainerDragger::_end_dragging() {
85+
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
86+
dragging = false;
87+
queue_redraw();
88+
sc->emit_signal(SNAME("drag_ended"));
89+
}
90+
8691
Control::CursorShape SplitContainerDragger::get_cursor_shape(const Point2 &p_pos) const {
8792
SplitContainer *sc = Object::cast_to<SplitContainer>(get_parent());
8893
if (!sc->collapsed && sc->dragging_enabled) {
@@ -196,6 +201,19 @@ Control *SplitContainer::_get_sortable_child(int p_idx, SortableVisibilityMode p
196201
return nullptr;
197202
}
198203

204+
void SplitContainer::_fit_child_in_rect_with_visibility_update(Control *p_child, const Rect2 &p_rect) {
205+
// For very small rects (like when a size is set to 0 via autosnapping) hide the child instead
206+
// of changing its rectangle. The child is scaled to 0 instead of changing visibility to avoid
207+
// affecting any existing visibility state for both the SplitContainer users and for the size
208+
// calculations SplitContainer itself doe
209+
if (snap_state != Snap::SNAP_NONE && (p_rect.size.y < 1.0 || p_rect.size.x < 1.0)) {
210+
p_child->set_scale(Vector2(0, 0));
211+
} else {
212+
// fit_child_in_rect resets scaling to 1
213+
fit_child_in_rect(p_child, p_rect);
214+
}
215+
}
216+
199217
Ref<Texture2D> SplitContainer::_get_grabber_icon() const {
200218
if (is_fixed) {
201219
return theme_cache.grabber_icon;
@@ -245,7 +263,26 @@ void SplitContainer::_compute_split_offset(bool p_clamp) {
245263
// Clamp the split offset to acceptable values.
246264
int first_min_size = first->get_combined_minimum_size()[axis_index];
247265
int second_min_size = second->get_combined_minimum_size()[axis_index];
248-
computed_split_offset = CLAMP(wished_size, first_min_size, size - sep - second_min_size);
266+
267+
// Check autosnapping
268+
if (dragging_area_control->dragging) {
269+
if ((auto_snap & SNAP_FIRST) != 0 && wished_size < first_min_size / 3) {
270+
set_snap_state(Snap::SNAP_FIRST);
271+
} else if ((auto_snap & SNAP_SECOND) != 0 && wished_size > size - sep - second_min_size / 3) {
272+
set_snap_state(Snap::SNAP_SECOND);
273+
} else {
274+
set_snap_state(Snap::SNAP_NONE);
275+
}
276+
}
277+
278+
// Apply snapping
279+
if (!collapsed && (snap_state & SNAP_FIRST) != 0) {
280+
computed_split_offset = 0;
281+
} else if (!collapsed && (snap_state & SNAP_SECOND) != 0) {
282+
computed_split_offset = size;
283+
} else {
284+
computed_split_offset = CLAMP(wished_size, first_min_size, size - sep - second_min_size);
285+
}
249286

250287
// Clamp the split_offset if requested.
251288
if (p_clamp) {
@@ -275,19 +312,19 @@ void SplitContainer::_resort() {
275312

276313
// Move the children.
277314
if (vertical) {
278-
fit_child_in_rect(first, Rect2(Point2(0, 0), Size2(get_size().width, computed_split_offset)));
315+
_fit_child_in_rect_with_visibility_update(first, Rect2(Point2(0, 0), Size2(get_size().width, computed_split_offset)));
279316
int sofs = computed_split_offset + sep;
280-
fit_child_in_rect(second, Rect2(Point2(0, sofs), Size2(get_size().width, get_size().height - sofs)));
317+
_fit_child_in_rect_with_visibility_update(second, Rect2(Point2(0, sofs), Size2(get_size().width, get_size().height - sofs)));
281318
} else {
282319
if (is_rtl) {
283320
computed_split_offset = get_size().width - computed_split_offset - sep;
284-
fit_child_in_rect(second, Rect2(Point2(0, 0), Size2(computed_split_offset, get_size().height)));
321+
_fit_child_in_rect_with_visibility_update(second, Rect2(Point2(0, 0), Size2(computed_split_offset, get_size().height)));
285322
int sofs = computed_split_offset + sep;
286-
fit_child_in_rect(first, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height)));
323+
_fit_child_in_rect_with_visibility_update(first, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height)));
287324
} else {
288-
fit_child_in_rect(first, Rect2(Point2(0, 0), Size2(computed_split_offset, get_size().height)));
325+
_fit_child_in_rect_with_visibility_update(first, Rect2(Point2(0, 0), Size2(computed_split_offset, get_size().height)));
289326
int sofs = computed_split_offset + sep;
290-
fit_child_in_rect(second, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height)));
327+
_fit_child_in_rect_with_visibility_update(second, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height)));
291328
}
292329
}
293330

@@ -385,23 +422,55 @@ void SplitContainer::set_collapsed(bool p_collapsed) {
385422
return;
386423
}
387424
collapsed = p_collapsed;
425+
if (collapsed && dragging_area_control->dragging) {
426+
dragging_area_control->_end_dragging();
427+
}
388428
queue_sort();
389429
}
390430

431+
bool SplitContainer::is_collapsed() const {
432+
return collapsed;
433+
}
434+
391435
void SplitContainer::set_dragger_visibility(DraggerVisibility p_visibility) {
392436
if (dragger_visibility == p_visibility) {
393437
return;
394438
}
395439
dragger_visibility = p_visibility;
440+
if (dragger_visibility != DraggerVisibility::DRAGGER_VISIBLE && dragging_area_control->dragging) {
441+
dragging_area_control->_end_dragging();
442+
}
396443
queue_sort();
397444
}
398445

399446
SplitContainer::DraggerVisibility SplitContainer::get_dragger_visibility() const {
400447
return dragger_visibility;
401448
}
402449

403-
bool SplitContainer::is_collapsed() const {
404-
return collapsed;
450+
void SplitContainer::set_auto_snap(Snap p_auto_snap) {
451+
if (auto_snap == p_auto_snap) {
452+
return;
453+
}
454+
auto_snap = p_auto_snap;
455+
}
456+
457+
SplitContainer::Snap SplitContainer::get_auto_snap() const {
458+
return auto_snap;
459+
}
460+
461+
void SplitContainer::set_snap_state(Snap p_snap_state) {
462+
if (snap_state == p_snap_state) {
463+
return;
464+
}
465+
snap_state = p_snap_state;
466+
if (!collapsed) {
467+
queue_sort();
468+
}
469+
emit_signal(SNAME("snap_state_changed"));
470+
}
471+
472+
SplitContainer::Snap SplitContainer::get_snap_state() const {
473+
return snap_state;
405474
}
406475

407476
void SplitContainer::set_vertical(bool p_vertical) {
@@ -421,9 +490,7 @@ void SplitContainer::set_dragging_enabled(bool p_enabled) {
421490
}
422491
dragging_enabled = p_enabled;
423492
if (!dragging_enabled && dragging_area_control->dragging) {
424-
dragging_area_control->dragging = false;
425-
// queue_redraw() is called by _resort().
426-
emit_signal(SNAME("drag_ended"));
493+
dragging_area_control->_end_dragging();
427494
}
428495
if (get_viewport()) {
429496
get_viewport()->update_mouse_cursor_state();
@@ -515,6 +582,12 @@ void SplitContainer::_bind_methods() {
515582
ClassDB::bind_method(D_METHOD("set_dragger_visibility", "mode"), &SplitContainer::set_dragger_visibility);
516583
ClassDB::bind_method(D_METHOD("get_dragger_visibility"), &SplitContainer::get_dragger_visibility);
517584

585+
ClassDB::bind_method(D_METHOD("set_auto_snap", "auto_snap"), &SplitContainer::set_auto_snap);
586+
ClassDB::bind_method(D_METHOD("get_auto_snap"), &SplitContainer::get_auto_snap);
587+
588+
ClassDB::bind_method(D_METHOD("set_snap_state", "snap_state"), &SplitContainer::set_snap_state);
589+
ClassDB::bind_method(D_METHOD("get_snap_state"), &SplitContainer::get_snap_state);
590+
518591
ClassDB::bind_method(D_METHOD("set_vertical", "vertical"), &SplitContainer::set_vertical);
519592
ClassDB::bind_method(D_METHOD("is_vertical"), &SplitContainer::is_vertical);
520593

@@ -538,11 +611,14 @@ void SplitContainer::_bind_methods() {
538611
ADD_SIGNAL(MethodInfo("dragged", PropertyInfo(Variant::INT, "offset")));
539612
ADD_SIGNAL(MethodInfo("drag_started"));
540613
ADD_SIGNAL(MethodInfo("drag_ended"));
614+
ADD_SIGNAL(MethodInfo("snap_state_changed"));
541615

542616
ADD_PROPERTY(PropertyInfo(Variant::INT, "split_offset", PROPERTY_HINT_NONE, "suffix:px"), "set_split_offset", "get_split_offset");
543617
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "collapsed"), "set_collapsed", "is_collapsed");
544618
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dragging_enabled"), "set_dragging_enabled", "is_dragging_enabled");
545619
ADD_PROPERTY(PropertyInfo(Variant::INT, "dragger_visibility", PROPERTY_HINT_ENUM, "Visible,Hidden,Hidden and Collapsed"), "set_dragger_visibility", "get_dragger_visibility");
620+
ADD_PROPERTY(PropertyInfo(Variant::INT, "auto_snap", PROPERTY_HINT_ENUM, "None,First,Second,Both"), "set_auto_snap", "get_auto_snap");
621+
ADD_PROPERTY(PropertyInfo(Variant::INT, "snap_state", PROPERTY_HINT_ENUM, "None,First,Second"), "set_snap_state", "get_snap_state");
546622
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vertical"), "set_vertical", "is_vertical");
547623

548624
ADD_GROUP("Drag Area", "drag_area_");
@@ -555,6 +631,11 @@ void SplitContainer::_bind_methods() {
555631
BIND_ENUM_CONSTANT(DRAGGER_HIDDEN);
556632
BIND_ENUM_CONSTANT(DRAGGER_HIDDEN_COLLAPSED);
557633

634+
BIND_ENUM_CONSTANT(SNAP_NONE);
635+
BIND_ENUM_CONSTANT(SNAP_FIRST);
636+
BIND_ENUM_CONSTANT(SNAP_SECOND);
637+
BIND_ENUM_CONSTANT(SNAP_BOTH);
638+
558639
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, separation);
559640
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, minimum_grab_thickness);
560641
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, autohide);

0 commit comments

Comments
 (0)