├── .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 | # drawing 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 | animated 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 | 5 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 21 | 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 | 5 | 8 | 9 | 11 | 12 | 13 | 16 | 17 | 20 | 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 | 5 | 8 | 9 | 11 | 12 | 13 | 16 | 17 | 20 | 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 | --------------------------------------------------------------------------------