Merge pull request #90411 from kitbdev/multisplit

Allow SplitContainer to have more than two children
This commit is contained in:
Thaddeus Crews
2025-11-25 07:06:53 -06:00
6 changed files with 2005 additions and 466 deletions

View File

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="SplitContainer" inherits="Container" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
A container that splits two child controls horizontally or vertically and provides a grabber for adjusting the split ratio.
A container that arranges child controls horizontally or vertically and provides grabbers for adjusting the split ratios between them.
</brief_description>
<description>
A container that accepts only two child controls, then arranges them horizontally or vertically and creates a divisor between them. The divisor can be dragged around to change the size relation between the child controls.
A container that arranges child controls horizontally or vertically and creates grabbers between them. The grabbers can be dragged around to change the size relations between the child controls.
</description>
<tutorials>
<link title="Using Containers">$DOCS_URL/tutorials/ui/gui_containers.html</link>
@ -12,11 +12,12 @@
<methods>
<method name="clamp_split_offset">
<return type="void" />
<param index="0" name="priority_index" type="int" default="0" />
<description>
Clamps the [member split_offset] value to not go outside the currently possible minimal and maximum values.
Clamps the [member split_offsets] values to ensure they are within valid ranges and do not overlap with each other. When overlaps occur, this method prioritizes one split offset (at index [param priority_index]) by clamping any overlapping split offsets to it.
</description>
</method>
<method name="get_drag_area_control">
<method name="get_drag_area_control" deprecated="Use the first element of [method get_drag_area_controls] instead.">
<return type="Control" />
<description>
Returns the drag area [Control]. For example, you can move a pre-configured button into the drag area [Control] so that it rides along with the split bar. Try setting the [Button] anchors to [code]center[/code] prior to the [code]reparent()[/code] call.
@ -27,10 +28,21 @@
[b]Warning:[/b] This is a required internal node, removing and freeing it may cause a crash.
</description>
</method>
<method name="get_drag_area_controls">
<return type="Control[]" />
<description>
Returns an [Array] of the drag area [Control]s. These are the interactable [Control] nodes between each child. For example, this can be used to add a pre-configured button to a drag area [Control] so that it rides along with the split bar. Try setting the [Button] anchors to [code]center[/code] prior to the [method Node.reparent] call.
[codeblock]
$BarnacleButton.reparent($SplitContainer.get_drag_area_controls()[0])
[/codeblock]
[b]Note:[/b] The drag area [Control]s are drawn over the [SplitContainer]'s children, so [CanvasItem] draw objects called from a drag area and children added to it will also appear over the [SplitContainer]'s children. Try setting [member Control.mouse_filter] of custom children to [constant Control.MOUSE_FILTER_IGNORE] to prevent blocking the mouse from dragging if desired.
[b]Warning:[/b] These are required internal nodes, removing or freeing them may cause a crash.
</description>
</method>
</methods>
<members>
<member name="collapsed" type="bool" setter="set_collapsed" getter="is_collapsed" default="false">
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].
If [code]true[/code], the draggers will be disabled and the children will be sized as if all [member split_offsets] were [code]0[/code].
</member>
<member name="drag_area_highlight_in_editor" type="bool" setter="set_drag_area_highlight_in_editor" getter="is_drag_area_highlight_in_editor_enabled" default="false">
Highlights the drag area [Rect2] so you can see where it is during development. The drag area is gold if [member dragging_enabled] is [code]true[/code], and red if [code]false[/code].
@ -50,8 +62,13 @@
<member name="dragging_enabled" type="bool" setter="set_dragging_enabled" getter="is_dragging_enabled" default="true">
Enables or disables split dragging.
</member>
<member name="split_offset" type="int" setter="set_split_offset" getter="get_split_offset" default="0">
The initial offset of the splitting between the two [Control]s, with [code]0[/code] being at the end of the first [Control].
<member name="split_offset" type="int" setter="set_split_offset" getter="get_split_offset" default="0" deprecated="Use [member split_offsets] instead. The first element of the array is the split offset between the first two children.">
The first element of [member split_offsets].
</member>
<member name="split_offsets" type="PackedInt32Array" setter="set_split_offsets" getter="get_split_offsets" default="PackedInt32Array(0)">
Offsets for each dragger in pixels. Each one is the offset of the split between the [Control] nodes before and after the dragger, with [code]0[/code] being the default position. The default position is based on the [Control] nodes expand flags and minimum sizes. See [member Control.size_flags_horizontal], [member Control.size_flags_vertical], and [member Control.size_flags_stretch_ratio].
If none of the [Control] nodes before the dragger are expanded, the default position will be at the start of the [SplitContainer]. If none of the [Control] nodes after the dragger are expanded, the default position will be at the end of the [SplitContainer]. If the dragger is in between expanded [Control] nodes, the default position will be in the middle, based on the [member Control.size_flags_stretch_ratio]s and minimum sizes.
[b]Note:[/b] If the split offsets cause [Control] nodes to overlap, the first split will take priority when resolving the positions.
</member>
<member name="touch_dragger_enabled" type="bool" setter="set_touch_dragger_enabled" getter="is_touch_dragger_enabled" default="false">
If [code]true[/code], a touch-friendly drag handle will be enabled for better usability on smaller screens. Unlike the standard grabber, this drag handle overlaps the [SplitContainer]'s children and does not affect their minimum separation. The standard grabber will no longer be drawn when this option is enabled.
@ -75,7 +92,7 @@
<signal name="dragged">
<param index="0" name="offset" type="int" />
<description>
Emitted when the dragger is dragged by user.
Emitted when any dragger is dragged by user.
</description>
</signal>
</signals>
@ -104,20 +121,20 @@
The color of the touch dragger when pressed.
</theme_item>
<theme_item name="autohide" data_type="constant" type="int" default="1">
Boolean value. If [code]1[/code] ([code]true[/code]), the grabber will hide automatically when it isn't under the cursor. If [code]0[/code] ([code]false[/code]), it's always visible. The [member dragger_visibility] must be [constant DRAGGER_VISIBLE].
Boolean value. If [code]1[/code] ([code]true[/code]), the grabbers will hide automatically when they aren't under the cursor. If [code]0[/code] ([code]false[/code]), the grabbers are always visible. The [member dragger_visibility] must be [constant DRAGGER_VISIBLE].
</theme_item>
<theme_item name="minimum_grab_thickness" data_type="constant" type="int" default="6">
The minimum thickness of the area users can click on to grab the split bar. This ensures that the split bar can still be dragged if [theme_item separation] or [theme_item h_grabber] / [theme_item v_grabber]'s size is too narrow to easily select.
The minimum thickness of the area users can click on to grab a split bar. This ensures that the split bar can still be dragged if [theme_item separation] or [theme_item h_grabber] / [theme_item v_grabber]'s size is too narrow to easily select.
</theme_item>
<theme_item name="separation" data_type="constant" type="int" default="12">
The split bar thickness, i.e., the gap between the two children of the container. This is overridden by the size of the grabber icon if [member dragger_visibility] is set to [constant DRAGGER_VISIBLE], or [constant DRAGGER_HIDDEN], and [theme_item separation] is smaller than the size of the grabber icon in the same axis.
The split bar thickness, i.e., the gap between each child of the container. This is overridden by the size of the grabber icon if [member dragger_visibility] is set to [constant DRAGGER_VISIBLE], or [constant DRAGGER_HIDDEN], and [theme_item separation] is smaller than the size of the grabber icon in the same axis.
[b]Note:[/b] To obtain [theme_item separation] values less than the size of the grabber icon, for example a [code]1 px[/code] hairline, set [theme_item h_grabber] or [theme_item v_grabber] to a new [ImageTexture], which effectively sets the grabber icon size to [code]0 px[/code].
</theme_item>
<theme_item name="grabber" data_type="icon" type="Texture2D">
The icon used for the grabber drawn in the middle area. This is only used in [HSplitContainer] and [VSplitContainer]. For [SplitContainer], see [theme_item h_grabber] and [theme_item v_grabber] instead.
The icon used for the grabbers drawn in the separations. This is only used in [HSplitContainer] and [VSplitContainer]. For [SplitContainer], see [theme_item h_grabber] and [theme_item v_grabber] instead.
</theme_item>
<theme_item name="h_grabber" data_type="icon" type="Texture2D">
The icon used for the grabber drawn in the middle area when [member vertical] is [code]false[/code].
The icon used for the grabbers drawn in the separations when [member vertical] is [code]false[/code].
</theme_item>
<theme_item name="h_touch_dragger" data_type="icon" type="Texture2D">
The icon used for the drag handle when [member touch_dragger_enabled] is [code]true[/code] and [member vertical] is [code]false[/code].
@ -126,7 +143,7 @@
The icon used for the drag handle when [member touch_dragger_enabled] is [code]true[/code]. This is only used in [HSplitContainer] and [VSplitContainer]. For [SplitContainer], see [theme_item h_touch_dragger] and [theme_item v_touch_dragger] instead.
</theme_item>
<theme_item name="v_grabber" data_type="icon" type="Texture2D">
The icon used for the grabber drawn in the middle area when [member vertical] is [code]true[/code].
The icon used for the grabbers drawn in the separations when [member vertical] is [code]true[/code].
</theme_item>
<theme_item name="v_touch_dragger" data_type="icon" type="Texture2D">
The icon used for the drag handle when [member touch_dragger_enabled] is [code]true[/code] and [member vertical] is [code]true[/code].

View File

@ -106,3 +106,10 @@ GH-112379
Validate extension JSON: Error: Field 'classes/DisplayServer/methods/tts_speak/arguments/5': meta changed value in new API, from "int32" to "int64".
`utterance_id` argument changed from `int32` to `int64`. No compatibility method needed.
GH-90411
--------
Validate extension JSON: JSON file: Field was added in a way that breaks compatibility 'classes/SplitContainer/methods/clamp_split_offset': arguments
Optional argument added for index. Compatibility method registered.

View File

@ -0,0 +1,41 @@
/**************************************************************************/
/* split_container.compat.inc */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#ifndef DISABLE_DEPRECATED
void SplitContainer::_clamp_split_offset_compat_90411() {
clamp_split_offset(0);
}
void SplitContainer::_bind_compatibility_methods() {
ClassDB::bind_compatibility_method(D_METHOD("clamp_split_offset"), &SplitContainer::_clamp_split_offset_compat_90411);
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,12 @@ class TextureRect;
class SplitContainerDragger : public Control {
GDCLASS(SplitContainerDragger, Control);
friend class SplitContainer;
Rect2 split_bar_rect;
TextureRect *touch_dragger = nullptr;
void _touch_dragger_mouse_exited();
void _touch_dragger_gui_input(const Ref<InputEvent> &p_event);
protected:
void _notification(int p_what);
@ -50,12 +55,18 @@ protected:
private:
bool dragging = false;
int drag_from = 0;
int drag_ofs = 0;
int start_drag_split_offset = 0;
bool mouse_inside = false;
public:
int dragger_index = -1;
virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override;
void set_touch_dragger_enabled(bool p_enabled);
void update_touch_dragger();
bool is_touch_dragger_enabled() const;
SplitContainerDragger();
};
@ -71,21 +82,26 @@ public:
};
private:
int show_drag_area = false;
bool show_drag_area = false;
int drag_area_margin_begin = 0;
int drag_area_margin_end = 0;
int drag_area_offset = 0;
int split_offset = 0;
int computed_split_offset = 0;
PackedInt32Array split_offsets;
LocalVector<int> default_dragger_positions;
LocalVector<int> dragger_positions;
LocalVector<Control *> valid_children;
LocalVector<SplitContainerDragger *> dragging_area_controls;
bool vertical = false;
bool collapsed = false;
DraggerVisibility dragger_visibility = DRAGGER_VISIBLE;
bool dragging_enabled = true;
SplitContainerDragger *dragging_area_control = nullptr;
bool split_offset_pending = false;
bool can_use_desired_sizes = false;
bool initialized = false;
bool touch_dragger_enabled = false;
TextureRect *touch_dragger = nullptr;
struct ThemeCache {
Color touch_dragger_color;
@ -106,24 +122,44 @@ private:
Ref<Texture2D> _get_grabber_icon() const;
Ref<Texture2D> _get_touch_dragger_icon() const;
void _touch_dragger_mouse_exited();
void _touch_dragger_gui_input(const Ref<InputEvent> &p_event);
void _compute_split_offset(bool p_clamp);
Point2i _get_valid_range(int p_dragger_index) const;
PackedInt32Array _get_desired_sizes() const;
void _set_desired_sizes(const PackedInt32Array &p_desired_sizes, int p_priority_index = -1);
void _update_default_dragger_positions();
void _update_dragger_positions(int p_clamp_index = -1);
int _get_separation() const;
void _resort();
Control *_get_sortable_child(int p_idx, SortableVisibilityMode p_visibility_mode = SortableVisibilityMode::VISIBLE_IN_TREE) const;
void _update_draggers();
void _on_child_visibility_changed(Control *p_control);
void _add_valid_child(Control *p_control);
void _remove_valid_child(Control *p_control);
protected:
bool is_fixed = false;
void _notification(int p_what);
void _validate_property(PropertyInfo &p_property) const;
virtual void add_child_notify(Node *p_child) override;
virtual void remove_child_notify(Node *p_child) override;
virtual void move_child_notify(Node *p_child) override;
static void _bind_methods();
#ifndef DISABLE_DEPRECATED
void _clamp_split_offset_compat_90411();
static void _bind_compatibility_methods();
#endif // DISABLE_DEPRECATED
public:
void set_split_offset(int p_offset);
int get_split_offset() const;
void clamp_split_offset();
void set_split_offset(int p_offset, int p_index = 0);
int get_split_offset(int p_index = 0) const;
void set_split_offsets(const PackedInt32Array &p_offsets);
PackedInt32Array get_split_offsets() const;
void clamp_split_offset(int p_priority_index = 0);
void set_collapsed(bool p_collapsed);
bool is_collapsed() const;
@ -154,11 +190,17 @@ public:
void set_show_drag_area_enabled(bool p_enabled);
bool is_show_drag_area_enabled() const;
Control *get_drag_area_control() { return dragging_area_control; }
TypedArray<Control> get_drag_area_controls();
void set_touch_dragger_enabled(bool p_enabled);
bool is_touch_dragger_enabled() const;
#ifndef DISABLE_DEPRECATED
Control *get_drag_area_control() { return dragging_area_controls[0]; }
void _set_split_offset_first(int p_offset) { set_split_offset(p_offset); }
int _get_split_offset_first() const { return get_split_offset(); }
#endif
SplitContainer(bool p_vertical = false);
};

File diff suppressed because it is too large Load Diff