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() root.global_position = root.global_position.clamp(Vector2(8, 8), (viewport_size - dimensions - Vector2(8, 8)).max(Vector2(8, 8))) 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)