├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── addons
└── ReorderableContainer
│ ├── Icon
│ ├── reorderable_container_icon.svg
│ ├── reorderable_container_icon.svg.import
│ ├── reorderable_hbox_icon.svg
│ ├── reorderable_hbox_icon.svg.import
│ ├── reorderable_vbox_icon.svg
│ └── reorderable_vbox_icon.svg.import
│ ├── plugin.cfg
│ ├── plugin.gd
│ ├── reorderable_container.gd
│ ├── reorderable_hbox.gd
│ └── reorderable_vbox.gd
├── example.tscn
├── icon.png
├── icon.png.import
└── project.godot
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize EOL for all files that Git considers text files.
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot 4+ specific ignores
2 | .godot/
3 |
4 | # Godot-specific ignores
5 | .import/
6 | export.cfg
7 | export_presets.cfg
8 |
9 | # Imported translations (automatically generated from CSV files)
10 | *.translation
11 |
12 | # Mono-specific ignores
13 | .mono/
14 | data_*/
15 | mono_crash.*.json
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 THEWORLDRUDO
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReorderableContainer
2 | A container similar to BoxContainer but extended with drag-and-drop style reordering functionality, and auto-scroll functionality when placed under ScrollContainer.
3 |
4 |
5 |
6 |
7 |
8 | ## How to use
9 | 1. Click the "+" button to add a new node and select `ReorderableVBox` or `ReorderableHBox`.
10 | 2. Add it under `ScrollContainer` if you want to make "Reorderable list". The container will automatically scroll when the user drag item to a certain point.
11 | **Note:** This addon also works with [SmoothScroll](https://github.com/SpyrexDE/SmoothScroll) by SpyrexDE.
12 | 3. Add child control node under `ReorderableContainer` as many as you like and set `custom_minimum_size` to appropriate value.
13 | 4. Further documentation is provided with the addon but can be troublesome to access due to [this issue](https://github.com/godotengine/godot/issues/67203) and [this](https://godotforums.org/d/33337-custom-class-documentation-not-showing-up)
14 |
15 | ## License
16 | [MIT](https://choosealicense.com/licenses/mit/)
17 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/Icon/reorderable_container_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/Icon/reorderable_container_icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://b4kg0ekxkw2lb"
6 | path="res://.godot/imported/reorderable_container_icon.svg-b137036dec781405c9f5977df10d510b.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/ReorderableContainer/Icon/reorderable_container_icon.svg"
14 | dest_files=["res://.godot/imported/reorderable_container_icon.svg-b137036dec781405c9f5977df10d510b.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/Icon/reorderable_hbox_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/Icon/reorderable_hbox_icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://bc35o8q35l74"
6 | path="res://.godot/imported/reorderable_hbox_icon.svg-b8929c984930c1cf79a5dffb0a9bff85.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/ReorderableContainer/Icon/reorderable_hbox_icon.svg"
14 | dest_files=["res://.godot/imported/reorderable_hbox_icon.svg-b8929c984930c1cf79a5dffb0a9bff85.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/Icon/reorderable_vbox_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
21 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/Icon/reorderable_vbox_icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://c1rxcwal2patu"
6 | path="res://.godot/imported/reorderable_vbox_icon.svg-6c0163d38628f550aff9f4fbed5c8a1d.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/ReorderableContainer/Icon/reorderable_vbox_icon.svg"
14 | dest_files=["res://.godot/imported/reorderable_vbox_icon.svg-6c0163d38628f550aff9f4fbed5c8a1d.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="ReorderableContainer"
4 | description="A container similar to BoxContainer but extended with drag-and-drop style reordering functionality, and auto-scroll functionality when placed under ScrollContainer."
5 | author="FoolLin"
6 | version="1.2.4"
7 | script="plugin.gd"
8 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/plugin.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | extends EditorPlugin
3 |
4 |
5 | func _enter_tree():
6 | add_custom_type("ReorderableContainer", "Container", preload("reorderable_container.gd"), preload("Icon/reorderable_container_icon.svg"))
7 | add_custom_type("ReorderableVBox", "ReorderableContainer", preload("reorderable_vbox.gd"), preload("Icon/reorderable_vbox_icon.svg"))
8 | add_custom_type("ReorderableHBox", "ReorderableContainer", preload("reorderable_hbox.gd"), preload("Icon/reorderable_hbox_icon.svg"))
9 |
10 |
11 | func _exit_tree():
12 | remove_custom_type("ReorderableContainer")
13 | remove_custom_type("ReorderableVBox")
14 | remove_custom_type("ReorderableHBox")
15 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/reorderable_container.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("Icon/reorderable_container_icon.svg")
3 | class_name ReorderableContainer
4 | extends Container
5 | ## A container that allows its child to be reorder and arranges horizontally or vertically.
6 | ##
7 | ## A container similar to [BoxContainer] but extended with drag-and-drop style reordering functionality,
8 | ## and auto-scroll functionality when placed under [ScrollContainer].[br][br]
9 | ## [b]Note:[/b] This addon also works with SmoothScroll by SpyrexDE.
10 | ##
11 | ## @tutorial(SmoothScroll): https://github.com/SpyrexDE/SmoothScroll
12 | ## @tutorial(Using Containers): https://docs.godotengine.org/en/4.1/tutorials/ui/gui_containers.html
13 |
14 | ## Emitted when children have been reordered.
15 | signal reordered(from: int, to: int)
16 |
17 | ## Extend the drop zone length at the start and end of the container.
18 | ## This will ensure that drop input is recognized even outside the container itself.
19 | const DROP_ZONE_EXTEND = 2000
20 |
21 | ## The hold duration time in seconds before the holded child will start being drag.
22 | @export
23 | var hold_duration := 0.5
24 |
25 | ## The overall speed of how fast children will move and arrange.
26 | @export_range(3, 30, 0.01, "or_greater", "or_less")
27 | var speed := 10.0
28 |
29 | ## The space between the container's elements, in pixels.
30 | @export
31 | var separation := 10: set = set_separation
32 | func set_separation(value):
33 | if value == separation or value < 0:
34 | return
35 | separation = value
36 | _on_sort_children()
37 |
38 |
39 | ## if [code]true[/code] the container will arrange its children vertically, rather than horizontally.
40 | @export var is_vertical := false: set = set_vertical
41 | func set_vertical(value):
42 | if value == is_vertical:
43 | return
44 | is_vertical = value
45 | if is_vertical:
46 | custom_minimum_size.x = 0
47 | else:
48 | custom_minimum_size.y = 0
49 | _on_sort_children()
50 |
51 | ## (Optional) [ScrollContainer] refference. Normally, the addon will automatically check
52 | ## its parent node for [ScrollContainer]. If this is not the case, you can manually specify it here.
53 | @export
54 | var scroll_container: ScrollContainer
55 |
56 | ## The maximum speed of auto scroll.
57 | @export
58 | var auto_scroll_speed := 10.0
59 |
60 | ## The pacentage of how much space auto scroll will take in [ScrollContainer][br][br]
61 | ## [b]Example:[/b] If [code]auto_scroll_range[/code] is 30% (0.3) and [ScrollContainer] height is 100 px,
62 | ## upper part will be 0 to 30 px and lower part will be 70 to 100 px.
63 | @export_range(0, 0.5)
64 | var auto_scroll_range := 0.3
65 |
66 | ## The scrolling threshold in pixel. In a nutshell, user will have hard time trying to drag a child if it too low
67 | ## and user will accidentally drag a child when scrolling if it too high.
68 | @export
69 | var scroll_threshold := 30
70 |
71 | ## Uses when debugging
72 | @export
73 | var is_debugging := false
74 |
75 | var _scroll_starting_point := 0
76 | var _is_smooth_scroll := false
77 |
78 | var _drop_zones: Array[Rect2] = []
79 | var _drop_zone_index := -1
80 | var _expect_child_rect: Array[Rect2] = []
81 |
82 | var _focus_child: Control
83 | var _is_press := false
84 | var _is_hold := false
85 | var _current_duration := 0.0
86 | var _is_using_process := false
87 |
88 |
89 | func _ready():
90 | if scroll_container == null and get_parent() is ScrollContainer:
91 | scroll_container = get_parent()
92 |
93 | if scroll_container != null and scroll_container.has_method("handle_overdrag"):
94 | _is_smooth_scroll = true
95 |
96 | process_mode = Node.PROCESS_MODE_PAUSABLE
97 | _adjust_expected_child_rect()
98 | if not sort_children.is_connected(_on_sort_children):
99 | sort_children.connect(_on_sort_children, CONNECT_PERSIST)
100 | if not get_tree().node_added.is_connected(_on_node_added):
101 | get_tree().node_added.connect(_on_node_added, CONNECT_PERSIST)
102 |
103 |
104 | func _gui_input(event):
105 | if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
106 | for _child in get_children():
107 | var child := _child as Control
108 | if child.get_rect().has_point(get_local_mouse_position()) and event.is_pressed():
109 | _focus_child = child
110 | _is_press = true
111 | elif not event.is_pressed():
112 | _is_press = false
113 | _is_hold = false
114 |
115 |
116 | func _process(delta):
117 | if Engine.is_editor_hint(): return
118 |
119 | _handle_input(delta)
120 | if _current_duration >= hold_duration != _is_hold:
121 | _is_hold = _current_duration >= hold_duration
122 | if _is_hold:
123 | _on_start_dragging()
124 |
125 | if _is_hold:
126 | _handle_dragging_child_pos(delta)
127 | if scroll_container != null:
128 | _handle_auto_scroll(delta)
129 | elif not _is_hold and _drop_zone_index != -1:
130 | _on_stop_dragging()
131 |
132 | if _is_using_process :
133 | _on_sort_children(delta)
134 |
135 |
136 | func _handle_input(delta):
137 | if scroll_container != null and _is_press and not _is_hold:
138 | var scroll_point = scroll_container.scroll_vertical if is_vertical else scroll_container.scroll_horizontal
139 | if _current_duration == 0:
140 | _scroll_starting_point = scroll_point
141 | else:
142 | # If user scroll more than scroll_threshold, press is abort.
143 | _is_press = true if abs(scroll_point - _scroll_starting_point) <= scroll_threshold else false
144 | _current_duration = _current_duration + delta if _is_press else 0.0
145 |
146 |
147 | func _on_start_dragging():
148 | # Force _on_sort_children to use process update for linear interpolation
149 | _is_using_process = true
150 | _focus_child.z_index = 1
151 | # Workaround for SmoothScroll addon
152 | if _is_smooth_scroll:
153 | scroll_container.process_mode = Node.PROCESS_MODE_DISABLED
154 | for child in _get_visible_children():
155 | child.propagate_call("set_mouse_filter", [MOUSE_FILTER_IGNORE])
156 |
157 |
158 | func _on_stop_dragging():
159 | _focus_child.z_index = 0
160 | var focus_child_index := _focus_child.get_index()
161 | move_child(_focus_child, _drop_zone_index)
162 | reordered.emit(focus_child_index, _drop_zone_index)
163 | _focus_child = null
164 | _drop_zone_index = -1
165 | if _is_smooth_scroll:
166 | scroll_container.pos = -Vector2(scroll_container.scroll_horizontal, scroll_container.scroll_vertical)
167 | scroll_container.process_mode = Node.PROCESS_MODE_INHERIT
168 | for child in _get_visible_children():
169 | child.propagate_call("set_mouse_filter", [MOUSE_FILTER_PASS])
170 |
171 |
172 | func _on_node_added(node):
173 | if node is Control and not Engine.is_editor_hint():
174 | node.mouse_filter = Control.MOUSE_FILTER_PASS
175 |
176 |
177 | func _handle_dragging_child_pos(delta):
178 | if is_vertical:
179 | var target_pos = get_local_mouse_position().y - (_focus_child.size.y / 2.0)
180 | _focus_child.position.y = lerp(_focus_child.position.y, target_pos, delta * speed)
181 | else:
182 | var target_pos = get_local_mouse_position().x - (_focus_child.size.x / 2.0)
183 | _focus_child.position.x = lerp(_focus_child.position.x, target_pos, delta * speed)
184 |
185 | # Update drop zone index
186 | var child_center_pos: Vector2 = _focus_child.get_rect().get_center()
187 | for i in range(_drop_zones.size()):
188 | var drop_zone = _drop_zones[i]
189 | if drop_zone.has_point(child_center_pos):
190 | _drop_zone_index = i
191 | break
192 | elif i == _drop_zones.size() - 1:
193 | _drop_zone_index = -1
194 |
195 |
196 | func _handle_auto_scroll(delta):
197 | var mouse_g_pos = get_global_mouse_position()
198 | var scroll_g_rect = scroll_container.get_global_rect()
199 | if is_vertical:
200 | var upper = scroll_g_rect.position.y + (scroll_g_rect.size.y * auto_scroll_range)
201 | var lower = scroll_g_rect.position.y + (scroll_g_rect.size.y * (1.0 - auto_scroll_range))
202 |
203 | if upper > mouse_g_pos.y:
204 | var factor = (upper - mouse_g_pos.y) / (upper - scroll_g_rect.position.y)
205 | scroll_container.scroll_vertical -= delta * float(auto_scroll_speed) * 150.0 * factor
206 | elif lower < mouse_g_pos.y:
207 | var factor = (mouse_g_pos.y - lower) / (scroll_g_rect.end.y - lower)
208 | scroll_container.scroll_vertical += delta * float(auto_scroll_speed) * 150.0 * factor
209 | else:
210 | scroll_container.scroll_vertical = scroll_container.scroll_vertical
211 | else:
212 | var left = scroll_g_rect.position.x + (scroll_g_rect.size.x * auto_scroll_range)
213 | var right = scroll_g_rect.position.x + (scroll_g_rect.size.x * (1.0 - auto_scroll_range))
214 |
215 | if left > mouse_g_pos.x:
216 | var factor = (left - mouse_g_pos.x) / (left - scroll_g_rect.position.x)
217 | scroll_container.scroll_horizontal -= delta * float(auto_scroll_speed) * 150.0 * factor
218 | elif right < mouse_g_pos.x:
219 | var factor = (mouse_g_pos.x - right) / (scroll_g_rect.end.x - right)
220 | scroll_container.scroll_horizontal += delta * float(auto_scroll_speed) * 150.0 * factor
221 | else:
222 | scroll_container.scroll_horizontal = scroll_container.scroll_horizontal
223 |
224 |
225 | func _on_sort_children(delta := -1.0):
226 | if _is_using_process and delta == -1.0:
227 | return
228 |
229 | _adjust_expected_child_rect()
230 | _adjust_child_rect(delta)
231 | _adjust_drop_zone_rect()
232 |
233 |
234 | func _adjust_expected_child_rect():
235 | _expect_child_rect.clear()
236 | var children := _get_visible_children()
237 | var end_point = 0.0
238 | for i in range(children.size()):
239 | var child := children[i]
240 | var min_size := child.get_combined_minimum_size()
241 | if is_vertical:
242 | if i == _drop_zone_index:
243 | end_point += _focus_child.size.y + separation
244 |
245 | _expect_child_rect.append(Rect2(Vector2(0, end_point), Vector2(size.x, min_size.y)))
246 | end_point += min_size.y + separation
247 | else:
248 | if i == _drop_zone_index:
249 | end_point += _focus_child.size.x + separation
250 |
251 | _expect_child_rect.append(Rect2(Vector2(end_point, 0), Vector2(min_size.x, size.y)))
252 | end_point += min_size.x + separation
253 |
254 |
255 | func _adjust_child_rect(delta: float = -1.0):
256 | var children := _get_visible_children()
257 | if children.is_empty():
258 | return
259 |
260 | var is_animating := false
261 | var end_point := 0.0
262 | for i in range(children.size()):
263 | var child := children[i]
264 | if child.position == _expect_child_rect[i].position and child.size == _expect_child_rect[i].size:
265 | continue
266 |
267 | if _is_using_process:
268 | is_animating = true
269 | child.position = lerp(child.position, _expect_child_rect[i].position, delta * speed)
270 | child.size = _expect_child_rect[i].size
271 | if (child.position - _expect_child_rect[i].position).length() <= 1.0:
272 | child.position = _expect_child_rect[i].position
273 | else:
274 | child.position = _expect_child_rect[i].position
275 | child.size = _expect_child_rect[i].size
276 |
277 | var last_child := children[-1]
278 | if is_vertical:
279 | if _is_using_process and _drop_zone_index == children.size():
280 | custom_minimum_size.y = _expect_child_rect[-1].end.y + _focus_child.size.y + separation
281 | elif not _is_using_process:
282 | custom_minimum_size.y = last_child.get_rect().end.y
283 | else:
284 | if _is_using_process and _drop_zone_index == children.size():
285 | custom_minimum_size.x = _expect_child_rect[-1].end.x + _focus_child.size.x + separation
286 | elif not _is_using_process:
287 | custom_minimum_size.x = last_child.get_rect().end.x
288 |
289 | # Adjust rect every process frame until child is dropped and finished lerping
290 | # ( return to adjust when sort_children signal is emitted)
291 | if not is_animating and _focus_child == null:
292 | _is_using_process = false
293 |
294 |
295 | func _adjust_drop_zone_rect():
296 | _drop_zones.clear()
297 | var children = _get_visible_children()
298 | for i in range(children.size()):
299 | var drop_zone_rect: Rect2
300 | var child := children[i] as Control
301 | if is_vertical:
302 | if i == 0:
303 | # First child
304 | drop_zone_rect.position = Vector2(child.position.x, child.position.y - DROP_ZONE_EXTEND)
305 | drop_zone_rect.end = Vector2(child.size.x, child.get_rect().get_center().y)
306 | _drop_zones.append(drop_zone_rect)
307 | else:
308 | # In between
309 | var prev_child := children[i - 1] as Control
310 | drop_zone_rect.position = Vector2(prev_child.position.x, prev_child.get_rect().get_center().y)
311 | drop_zone_rect.end = Vector2(child.size.x, child.get_rect().get_center().y)
312 | _drop_zones.append(drop_zone_rect)
313 | if i == children.size() - 1:
314 | # Is also last child
315 | drop_zone_rect.position = Vector2(child.position.x, child.get_rect().get_center().y)
316 | drop_zone_rect.end = Vector2(child.size.x, child.get_rect().end.y + DROP_ZONE_EXTEND)
317 | _drop_zones.append(drop_zone_rect)
318 | else:
319 | if i == 0:
320 | # First child
321 | drop_zone_rect.position = Vector2(child.position.x - DROP_ZONE_EXTEND, child.position.y)
322 | drop_zone_rect.end = Vector2(child.get_rect().get_center().x, child.size.y)
323 | _drop_zones.append(drop_zone_rect)
324 | else:
325 | # In between
326 | var prev_child := children[i - 1] as Control
327 | drop_zone_rect.position = Vector2(prev_child.get_rect().get_center().x, prev_child.position.y)
328 | drop_zone_rect.end = Vector2(child.get_rect().get_center().x, child.size.y)
329 | _drop_zones.append(drop_zone_rect)
330 | if i == children.size() - 1:
331 | # Is also last child
332 | drop_zone_rect.position = Vector2(child.get_rect().get_center().x, child.position.y)
333 | drop_zone_rect.end = Vector2(child.get_rect().end.x + DROP_ZONE_EXTEND, child.size.y)
334 | _drop_zones.append(drop_zone_rect)
335 |
336 |
337 | func _get_visible_children() -> Array[Control]:
338 | var visible_control: Array[Control]
339 | for _child in get_children():
340 | var child := _child as Control
341 | if not child.visible:
342 | continue
343 | if child == _focus_child and _is_hold:
344 | continue
345 |
346 | visible_control.append(child)
347 | return visible_control
348 |
349 |
350 | func _print_debug(val):
351 | if is_debugging:
352 | print(val)
353 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/reorderable_hbox.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("Icon/reorderable_hbox_icon.svg")
3 | class_name ReorderableHBox
4 | extends ReorderableContainer
5 |
6 | func set_vertical(value):
7 | value = false
8 | super.set_vertical(value)
9 |
10 |
11 | func _ready():
12 | is_vertical = false
13 | super._ready()
14 |
--------------------------------------------------------------------------------
/addons/ReorderableContainer/reorderable_vbox.gd:
--------------------------------------------------------------------------------
1 | @tool
2 | @icon("Icon/reorderable_vbox_icon.svg")
3 | class_name ReorderableVBox
4 | extends ReorderableContainer
5 |
6 | func set_vertical(value):
7 | value = true
8 | super.set_vertical(value)
9 |
10 |
11 | func _ready():
12 | is_vertical = true
13 | super._ready()
14 |
--------------------------------------------------------------------------------
/example.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=3 format=3 uid="uid://dfx24nntnppqp"]
2 |
3 | [ext_resource type="Script" path="res://addons/ReorderableContainer/reorderable_vbox.gd" id="2_no7x4"]
4 | [ext_resource type="Script" path="res://addons/ReorderableContainer/reorderable_hbox.gd" id="2_yek3h"]
5 |
6 | [node name="Example" type="ColorRect"]
7 | anchors_preset = 15
8 | anchor_right = 1.0
9 | anchor_bottom = 1.0
10 | grow_horizontal = 2
11 | grow_vertical = 2
12 | color = Color(0.113281, 0.132813, 0.160156, 1)
13 |
14 | [node name="VBoxContainer" type="VBoxContainer" parent="."]
15 | layout_mode = 1
16 | anchors_preset = 15
17 | anchor_right = 1.0
18 | anchor_bottom = 1.0
19 | offset_left = 23.0
20 | offset_top = 25.0
21 | offset_right = -23.0
22 | offset_bottom = -25.0
23 | grow_horizontal = 2
24 | grow_vertical = 2
25 | theme_override_constants/separation = 15
26 |
27 | [node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
28 | layout_mode = 2
29 | size_flags_vertical = 3
30 | theme_override_constants/separation = 10
31 |
32 | [node name="Normal" type="Container" parent="VBoxContainer/HBoxContainer"]
33 | process_mode = 1
34 | custom_minimum_size = Vector2(0, 330)
35 | layout_mode = 2
36 | size_flags_horizontal = 3
37 | script = ExtResource("2_no7x4")
38 | is_vertical = true
39 |
40 | [node name="ColorRect" type="ColorRect" parent="VBoxContainer/HBoxContainer/Normal"]
41 | custom_minimum_size = Vector2(0, 50)
42 | layout_mode = 2
43 | mouse_filter = 1
44 | color = Color(0.835294, 0.203922, 0.203922, 1)
45 |
46 | [node name="ColorRect2" type="ColorRect" parent="VBoxContainer/HBoxContainer/Normal"]
47 | custom_minimum_size = Vector2(0, 125)
48 | layout_mode = 2
49 | mouse_filter = 1
50 | color = Color(0.831373, 0.498039, 0.203922, 1)
51 |
52 | [node name="ColorRect4" type="ColorRect" parent="VBoxContainer/HBoxContainer/Normal"]
53 | custom_minimum_size = Vector2(0, 50)
54 | layout_mode = 2
55 | mouse_filter = 1
56 | color = Color(0.827451, 0.627451, 0.203922, 1)
57 |
58 | [node name="ColorRect3" type="ColorRect" parent="VBoxContainer/HBoxContainer/Normal"]
59 | custom_minimum_size = Vector2(0, 75)
60 | layout_mode = 2
61 | mouse_filter = 1
62 | color = Color(0.792157, 0.823529, 0.203922, 1)
63 |
64 | [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/HBoxContainer"]
65 | layout_mode = 2
66 | size_flags_horizontal = 3
67 |
68 | [node name="Scroll" type="Container" parent="VBoxContainer/HBoxContainer/ScrollContainer" node_paths=PackedStringArray("scroll_container")]
69 | process_mode = 1
70 | custom_minimum_size = Vector2(0, 650)
71 | layout_mode = 2
72 | size_flags_horizontal = 3
73 | script = ExtResource("2_no7x4")
74 | is_vertical = true
75 | scroll_container = NodePath("..")
76 |
77 | [node name="ColorRect" type="ColorRect" parent="VBoxContainer/HBoxContainer/ScrollContainer/Scroll"]
78 | custom_minimum_size = Vector2(0, 100)
79 | layout_mode = 2
80 | mouse_filter = 1
81 | color = Color(0, 0.74902, 0.827451, 1)
82 |
83 | [node name="ColorRect2" type="ColorRect" parent="VBoxContainer/HBoxContainer/ScrollContainer/Scroll"]
84 | custom_minimum_size = Vector2(0, 100)
85 | layout_mode = 2
86 | mouse_filter = 1
87 | color = Color(0, 0.572549, 0.823529, 1)
88 |
89 | [node name="ColorRect4" type="ColorRect" parent="VBoxContainer/HBoxContainer/ScrollContainer/Scroll"]
90 | custom_minimum_size = Vector2(0, 100)
91 | layout_mode = 2
92 | mouse_filter = 1
93 | color = Color(0, 0.376471, 0.819608, 1)
94 |
95 | [node name="ColorRect3" type="ColorRect" parent="VBoxContainer/HBoxContainer/ScrollContainer/Scroll"]
96 | custom_minimum_size = Vector2(0, 100)
97 | layout_mode = 2
98 | mouse_filter = 1
99 | color = Color(0.0705882, 0.196078, 0.686275, 1)
100 |
101 | [node name="ColorRect5" type="ColorRect" parent="VBoxContainer/HBoxContainer/ScrollContainer/Scroll"]
102 | custom_minimum_size = Vector2(0, 100)
103 | layout_mode = 2
104 | mouse_filter = 1
105 | color = Color(0.176471, 0.105882, 0.670588, 1)
106 |
107 | [node name="ColorRect6" type="ColorRect" parent="VBoxContainer/HBoxContainer/ScrollContainer/Scroll"]
108 | custom_minimum_size = Vector2(0, 100)
109 | layout_mode = 2
110 | mouse_filter = 1
111 | color = Color(0.321569, 0.0705882, 0.686275, 1)
112 |
113 | [node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
114 | custom_minimum_size = Vector2(0, 200)
115 | layout_mode = 2
116 | scroll_horizontal = 100
117 | horizontal_scroll_mode = 2
118 | vertical_scroll_mode = 0
119 |
120 | [node name="ReorderableHBox" type="Container" parent="VBoxContainer/ScrollContainer" node_paths=PackedStringArray("scroll_container")]
121 | process_mode = 1
122 | custom_minimum_size = Vector2(2090, 0)
123 | layout_mode = 2
124 | size_flags_horizontal = 3
125 | size_flags_vertical = 3
126 | script = ExtResource("2_yek3h")
127 | scroll_container = NodePath("..")
128 |
129 | [node name="ColorRect" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
130 | custom_minimum_size = Vector2(200, 0)
131 | layout_mode = 2
132 | mouse_filter = 1
133 | color = Color(0, 0.764706, 0.8, 1)
134 |
135 | [node name="ColorRect2" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
136 | custom_minimum_size = Vector2(200, 0)
137 | layout_mode = 2
138 | mouse_filter = 1
139 | color = Color(0, 0.592157, 0.796078, 1)
140 |
141 | [node name="ColorRect3" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
142 | custom_minimum_size = Vector2(200, 0)
143 | layout_mode = 2
144 | mouse_filter = 1
145 | color = Color(0, 0.403922, 0.792157, 1)
146 |
147 | [node name="ColorRect4" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
148 | custom_minimum_size = Vector2(200, 0)
149 | layout_mode = 2
150 | mouse_filter = 1
151 | color = Color(0.0862745, 0.262745, 0.678431, 1)
152 |
153 | [node name="ColorRect5" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
154 | custom_minimum_size = Vector2(200, 0)
155 | layout_mode = 2
156 | mouse_filter = 1
157 | color = Color(0, 0.121569, 0.788235, 1)
158 |
159 | [node name="ColorRect6" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
160 | custom_minimum_size = Vector2(200, 0)
161 | layout_mode = 2
162 | mouse_filter = 1
163 | color = Color(0.247059, 0, 0.784314, 1)
164 |
165 | [node name="ColorRect7" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
166 | custom_minimum_size = Vector2(200, 0)
167 | layout_mode = 2
168 | mouse_filter = 1
169 | color = Color(0.501961, 0, 0.780392, 1)
170 |
171 | [node name="ColorRect8" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
172 | custom_minimum_size = Vector2(200, 0)
173 | layout_mode = 2
174 | mouse_filter = 1
175 | color = Color(0.662745, 0, 0.776471, 1)
176 |
177 | [node name="ColorRect9" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
178 | custom_minimum_size = Vector2(200, 0)
179 | layout_mode = 2
180 | mouse_filter = 1
181 | color = Color(0.772549, 0, 0.705882, 1)
182 |
183 | [node name="ColorRect10" type="ColorRect" parent="VBoxContainer/ScrollContainer/ReorderableHBox"]
184 | custom_minimum_size = Vector2(200, 0)
185 | layout_mode = 2
186 | mouse_filter = 1
187 | color = Color(0.768627, 0, 0.360784, 1)
188 |
189 | [connection signal="sort_children" from="VBoxContainer/HBoxContainer/Normal" to="VBoxContainer/HBoxContainer/Normal" method="_on_sort_children"]
190 | [connection signal="sort_children" from="VBoxContainer/HBoxContainer/ScrollContainer/Scroll" to="VBoxContainer/HBoxContainer/ScrollContainer/Scroll" method="_on_sort_children"]
191 | [connection signal="sort_children" from="VBoxContainer/ScrollContainer/ReorderableHBox" to="VBoxContainer/ScrollContainer/ReorderableHBox" method="_on_sort_children"]
192 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoolLin/ReorderableContainer/f2a8e8fbc5fdf963e1f9835295b19cc1a0700f7f/icon.png
--------------------------------------------------------------------------------
/icon.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://c3kis7t3ahe78"
6 | path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://icon.png"
14 | dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 |
--------------------------------------------------------------------------------
/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=5
10 |
11 | [application]
12 |
13 | config/name="ReorderableContainer"
14 | run/main_scene="res://example.tscn"
15 | config/features=PackedStringArray("4.2", "GL Compatibility")
16 |
17 | [editor_plugins]
18 |
19 | enabled=PackedStringArray("res://addons/ReorderableContainer/plugin.cfg")
20 |
21 | [input_devices]
22 |
23 | pointing/emulate_touch_from_mouse=true
24 |
25 | [rendering]
26 |
27 | renderer/rendering_method="gl_compatibility"
28 | renderer/rendering_method.mobile="gl_compatibility"
29 | textures/vram_compression/import_etc2_astc=true
30 |
--------------------------------------------------------------------------------