Skip to content

Commit 289e7af

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 289e7af

File tree

7 files changed

+192
-17
lines changed

7 files changed

+192
-17
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 AutoSnap] 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 AUTO_SNAP_FIRST] and [constant AUTO_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: 94 additions & 17 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 does
209+
if (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) {
@@ -259,9 +296,9 @@ void SplitContainer::_resort() {
259296

260297
if (!first || !second) { // Only one child.
261298
if (first) {
262-
fit_child_in_rect(first, Rect2(Point2(), get_size()));
299+
_fit_child_in_rect_with_visibility_update(first, Rect2(Point2(), get_size()));
263300
} else if (second) {
264-
fit_child_in_rect(second, Rect2(Point2(), get_size()));
301+
_fit_child_in_rect_with_visibility_update(second, Rect2(Point2(), get_size()));
265302
}
266303
dragging_area_control->hide();
267304
return;
@@ -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,9 @@ 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+
518588
ClassDB::bind_method(D_METHOD("set_vertical", "vertical"), &SplitContainer::set_vertical);
519589
ClassDB::bind_method(D_METHOD("is_vertical"), &SplitContainer::is_vertical);
520590

@@ -538,11 +608,13 @@ void SplitContainer::_bind_methods() {
538608
ADD_SIGNAL(MethodInfo("dragged", PropertyInfo(Variant::INT, "offset")));
539609
ADD_SIGNAL(MethodInfo("drag_started"));
540610
ADD_SIGNAL(MethodInfo("drag_ended"));
611+
ADD_SIGNAL(MethodInfo("snap_state_changed"));
541612

542613
ADD_PROPERTY(PropertyInfo(Variant::INT, "split_offset", PROPERTY_HINT_NONE, "suffix:px"), "set_split_offset", "get_split_offset");
543614
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "collapsed"), "set_collapsed", "is_collapsed");
544615
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dragging_enabled"), "set_dragging_enabled", "is_dragging_enabled");
545616
ADD_PROPERTY(PropertyInfo(Variant::INT, "dragger_visibility", PROPERTY_HINT_ENUM, "Visible,Hidden,Hidden and Collapsed"), "set_dragger_visibility", "get_dragger_visibility");
617+
ADD_PROPERTY(PropertyInfo(Variant::INT, "auto_snap", PROPERTY_HINT_ENUM, "None,First,Second,Both"), "set_auto_snap", "get_auto_snap");
546618
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "vertical"), "set_vertical", "is_vertical");
547619

548620
ADD_GROUP("Drag Area", "drag_area_");
@@ -555,6 +627,11 @@ void SplitContainer::_bind_methods() {
555627
BIND_ENUM_CONSTANT(DRAGGER_HIDDEN);
556628
BIND_ENUM_CONSTANT(DRAGGER_HIDDEN_COLLAPSED);
557629

630+
BIND_ENUM_CONSTANT(SNAP_NONE);
631+
BIND_ENUM_CONSTANT(SNAP_FIRST);
632+
BIND_ENUM_CONSTANT(SNAP_SECOND);
633+
BIND_ENUM_CONSTANT(SNAP_BOTH);
634+
558635
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, separation);
559636
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, minimum_grab_thickness);
560637
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, SplitContainer, autohide);

0 commit comments

Comments
 (0)