Randoloot/scripts/ui/floating_window_manager.gd
2026-10-20 20:32:50 +02:00

135 lines
4.7 KiB
GDScript

extends Node
signal interaction_started
const BASE_Z := 100
const Z_STEP := 32
var _windows: Array[Dictionary] = []
var _active: Control
var _dragged: Control
var _drag_offset := Vector2.ZERO
var modal_overlay: Control
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
get_viewport().size_changed.connect(_clamp_all)
func register_window(root: Control, panel: Control, header: Control) -> void:
for entry in _windows:
if entry["root"] == root:
entry["header"] = header
_prepare_header(header)
return
panel.mouse_filter = Control.MOUSE_FILTER_STOP
_prepare_header(header)
var original := panel.get_theme_stylebox("panel").duplicate() as StyleBoxFlat
_windows.append({"root": root, "panel": panel, "header": header, "style": original})
root.visibility_changed.connect(func() -> void:
if root.is_visible_in_tree() and panel.is_visible_in_tree():
focus_window.call_deferred(root)
elif _dragged == root:
_dragged = null
)
_update_order()
func _prepare_header(header: Control) -> void:
header.custom_minimum_size.y = maxf(header.custom_minimum_size.y, 28.0)
header.mouse_default_cursor_shape = Control.CURSOR_DRAG
header.tooltip_text = "Przeciągnij, aby przesunąć okno"
func focus_window(root: Control) -> void:
if not is_instance_valid(root) or not root.is_visible_in_tree():
return
for index in _windows.size():
if _windows[index]["root"] == root:
var entry := _windows[index]
_windows.remove_at(index)
_windows.append(entry)
root.get_parent().move_child(root, root.get_parent().get_child_count() - 1)
_active = root
_update_order()
return
func _update_order() -> void:
for index in _windows.size():
var entry := _windows[index]
if not is_instance_valid(entry["root"]):
continue
var root: Control = entry["root"]
root.z_index = BASE_Z + index * Z_STEP
var panel: Control = entry["panel"]
var style: StyleBoxFlat = entry["style"]
if style != null:
var current := style.duplicate() as StyleBoxFlat
if root == _active:
current.border_color = Color("88c9e2")
current.shadow_color = Color(0, 0, 0, 0.4)
current.shadow_size = 8
panel.add_theme_stylebox_override("panel", current)
func window_at(point: Vector2) -> Control:
for index in range(_windows.size() - 1, -1, -1):
var entry := _windows[index]
var root: Control = entry["root"]
var panel: Control = entry["panel"]
if not is_instance_valid(root) or not is_instance_valid(panel) or not panel.is_visible_in_tree():
continue
if is_instance_valid(modal_overlay) and modal_overlay.is_visible_in_tree() and not modal_overlay.is_ancestor_of(root):
continue
if panel.get_global_rect().has_point(point):
return root
return null
func _header_at(root: Control, point: Vector2) -> bool:
for entry in _windows:
if entry["root"] != root:
continue
var header: Control = entry["header"]
if not is_instance_valid(header) or not header.get_global_rect().has_point(point):
return false
for button in header.find_children("*", "BaseButton", true, false):
if button.is_visible_in_tree() and button.get_global_rect().has_point(point):
return false
return true
return false
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and not event.pressed and is_instance_valid(_dragged):
_dragged = null
get_viewport().set_input_as_handled()
return
if not event.pressed or get_viewport().gui_is_dragging():
return
var target := window_at(event.position)
if target == null:
return
focus_window(target)
interaction_started.emit()
if event.button_index == MOUSE_BUTTON_LEFT and _header_at(target, event.position):
_dragged = target
_drag_offset = event.position - target.global_position
target.set_meta("floating_window_dragged", true)
get_viewport().set_input_as_handled()
elif event is InputEventMouseMotion and is_instance_valid(_dragged):
if not _dragged.is_visible_in_tree():
_dragged = null
return
_dragged.global_position = event.position - _drag_offset
clamp_window(_dragged)
get_viewport().set_input_as_handled()
func clamp_window(root: Control) -> void:
if not is_instance_valid(root) or not root.is_inside_tree():
return
var viewport_size := get_viewport().get_visible_rect().size
var dimensions := root.size * root.get_global_transform().get_scale().abs()
var minimum := Vector2(8, preload("res://scripts/ui/resource_bar.gd").HEIGHT + 8)
root.global_position = root.global_position.clamp(minimum, (viewport_size - dimensions - Vector2(8, 8)).max(minimum))
func _clamp_all() -> void:
for entry in _windows:
var root: Control = entry["root"]
if is_instance_valid(root) and root.has_meta("floating_window_dragged"):
clamp_window(root)