Zmiany w oknach

This commit is contained in:
Tomasz Boruc 2026-10-16 22:18:07 +02:00
parent e843d76c68
commit a3e470ce9a
12 changed files with 310 additions and 23 deletions

View File

@ -8,6 +8,7 @@ const RENDER_LAYERS := preload("res://scripts/ui/render_layers.gd")
@onready var inventory_button: Button = $HUDRoot/ShipEquipmentPanel/Contents/InventoryButton
@onready var inventory_panel: Control = $HUDRoot/ShipInventoryPanel
var _window_manager: Node
var _energy_bank_button: Button
var _debug_expanded := false
var _ability_buttons: Dictionary = {}
@ -86,6 +87,7 @@ func _create_window(node_name: String, title: String, dimensions: Vector2) -> Pa
contents.add_theme_constant_override("separation", 12)
window.add_child(contents)
var header := HBoxContainer.new()
header.name = "Header"
contents.add_child(header)
var label := Label.new()
label.text = title
@ -100,6 +102,7 @@ func _create_window(node_name: String, title: String, dimensions: Vector2) -> Pa
close.pressed.connect(window.hide)
header.add_child(close)
window.hide()
_window_manager.call("register_window", window, window, header)
return window
func _toggle_window(window: PanelContainer) -> void:
@ -125,10 +128,15 @@ func _ready() -> void:
debug_info.add_theme_font_size_override("font_size", 10)
# The display font has unusually tall vertical metrics; keep debug rows compact.
debug_info.add_theme_constant_override("line_spacing", mini(0, 13 - ceili(debug_info.get_theme_font("font").get_height(10))))
_window_manager = Node.new()
_window_manager.name = "FloatingWindows"
_window_manager.set_script(preload("res://scripts/ui/floating_window_manager.gd"))
$HUDRoot.add_child(_window_manager)
_window_manager.connect("interaction_started", player.get_node("WeaponController").call.bind("block_ui_mouse_input"))
_create_sidebar()
_inventory_clip = Control.new()
_inventory_clip.name = "InventoryDrawer"
_inventory_clip.clip_contents = true
_inventory_clip.clip_contents = false
_inventory_clip.mouse_filter = Control.MOUSE_FILTER_IGNORE
$HUDRoot.add_child(_inventory_clip)
inventory_panel.reparent(_inventory_clip, false)
@ -137,6 +145,7 @@ func _ready() -> void:
inventory_panel.size = Vector2(560, inventory_panel.get_combined_minimum_size().y)
inventory_panel.scale = Vector2.ONE * EQUIPMENT_UI_SCALE
inventory_panel.set("drawer_width", inventory_panel.size.x * EQUIPMENT_UI_SCALE)
_window_manager.call("register_window", _inventory_clip, inventory_panel, inventory_panel.get_node("Contents/Header"))
$HUDRoot.resized.connect(func() -> void: _layout_sidebar.call_deferred())
_create_ability_bar()
debug_toggle.pressed.connect(_toggle_debug)
@ -229,6 +238,7 @@ func _create_sidebar() -> void:
_cockpit.set("player", player)
_cockpit.set("game", get_parent())
$HUDRoot.add_child(_cockpit)
_window_manager.call("register_window", _sidebar, _sidebar, equipment.get_node("Contents/Title"))
_create_navigation()
_layout_debug()
@ -260,10 +270,10 @@ func _toggle_sidebar() -> void:
_sidebar.show()
else:
close_inventory()
_sidebar_tween = create_tween()
_sidebar_tween.tween_property(_sidebar, "position:x", $HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 0.25).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT if _sidebar_open else Tween.EASE_IN)
if not _sidebar_open:
_sidebar_tween.tween_callback(_sidebar.hide)
_sidebar.visible = _sidebar_open
if _sidebar_open:
_window_manager.call("focus_window", _sidebar)
_layout_sidebar()
func _layout_sidebar() -> void:
if not is_instance_valid(_sidebar) or not is_instance_valid(_cockpit):
@ -283,7 +293,10 @@ func _layout_sidebar() -> void:
if is_instance_valid(_captain_skills_ui):
_captain_skills_ui.call_deferred("_layout")
_layout_debug()
_sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8 if _sidebar_open else $HUDRoot.size.x, 136)
if not _sidebar.has_meta("floating_window_dragged"):
_sidebar.position = Vector2($HUDRoot.size.x - _sidebar.size.x * EQUIPMENT_UI_SCALE - 8, 136)
else:
_window_manager.call("clamp_window", _sidebar)
_sidebar.visible = _sidebar_open
_inventory_clip.size = inventory_panel.size * EQUIPMENT_UI_SCALE
_layout_inventory()
@ -291,6 +304,9 @@ func _layout_sidebar() -> void:
func _layout_inventory() -> void:
if not is_instance_valid(_inventory_clip) or not is_instance_valid(_sidebar):
return
if _inventory_clip.has_meta("floating_window_dragged"):
_window_manager.call("clamp_window", _inventory_clip)
return
var equipment := _sidebar.get_node("Scroll/Contents/ShipEquipmentPanel") as Control
_inventory_clip.position = Vector2(maxf(12.0, _sidebar.position.x - _inventory_clip.size.x - 8), minf(equipment.global_position.y, maxf(8.0, $HUDRoot.size.y - inventory_panel.size.y * EQUIPMENT_UI_SCALE - 8)))

View File

@ -39,6 +39,11 @@ func _ensure_input_action(action_name: StringName, button: MouseButton) -> void:
if not InputMap.action_has_event(action_name, event):
InputMap.action_add_event(action_name, event)
func block_ui_mouse_input() -> void:
_fire_buttons_down.fill(false)
_fire_just_pressed.fill(false)
_drag_fire_blocked = true
func _unhandled_input(event: InputEvent) -> void:
if not event is InputEventMouseButton:
return

View File

@ -24,6 +24,7 @@ func _ready() -> void:
add_child(_screen)
_screen.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_screen.hide()
get_parent().get_node("HUDRoot/FloatingWindows").set("modal_overlay", _screen)
var dim := ColorRect.new()
dim.color = Color(0.015, 0.025, 0.045, 0.9)
_screen.add_child(dim)
@ -99,6 +100,7 @@ func _build_management() -> void:
child.queue_free()
var choosing := not _captain.skill_offers.is_empty()
var heading := HBoxContainer.new()
heading.name = "Header"
_contents.add_child(heading)
var title := _label("AWANS KAPITANA" if choosing else "UMIEJĘTNOŚCI KAPITANA", 25, Color("91dbea"))
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@ -143,6 +145,9 @@ func _build_management() -> void:
if owned.get_child_count() == 0:
_contents.add_child(_label("Nie masz jeszcze umiejętności. To początek Twojej specjalizacji.", 14, Color("91aabd")))
_build_loadout()
var manager := get_parent().get_node("HUDRoot/FloatingWindows")
manager.call("register_window", _panel, _panel, heading)
manager.call("focus_window", _panel)
_layout.call_deferred()
func _build_loadout() -> void:
@ -314,6 +319,9 @@ func _layout() -> void:
var ui_scale := minf(1.0, minf((viewport_size.x - 32) / base_size.x, (viewport_size.y - 32) / base_size.y))
_panel.size = base_size
_panel.scale = Vector2.ONE * ui_scale
if _panel.has_meta("floating_window_dragged"):
get_parent().get_node("HUDRoot/FloatingWindows").call("clamp_window", _panel)
else:
_panel.position = (viewport_size - base_size * ui_scale) * 0.5
var cockpit := get_parent().get("_cockpit") as Control
if is_instance_valid(cockpit):

View File

@ -0,0 +1,133 @@
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)

View File

@ -0,0 +1 @@
uid://dg15v55ayqn22

View File

@ -17,6 +17,7 @@ var _missile_button: Button
var _gamma_buy: Button
func _ready() -> void:
get_parent().get_node("FloatingWindows").call("register_window", self, panel, $Center/Panel/Contents/Title)
_gamma_buy = Button.new()
_gamma_buy.name = "BuyIrradiatedAmmunition"
_gamma_buy.custom_minimum_size.y = 36
@ -69,6 +70,9 @@ func _process(_delta: float) -> void:
_position_at_planet()
func _position_at_planet() -> void:
if has_meta("floating_window_dragged"):
get_parent().get_node("FloatingWindows").call("clamp_window", self)
return
if not is_instance_valid(planet):
return
var canvas := get_viewport().get_canvas_transform()

View File

@ -108,15 +108,7 @@ func _add_resource_card(row: HBoxContainer, data: Array) -> void:
func toggle_drawer() -> void:
_set_drawer_open(not _drawer_open)
func close_drawer(immediately: bool = false) -> void:
if immediately:
if _slide_tween != null and _slide_tween.is_running():
_slide_tween.kill()
_drawer_open = false
GameState.hud_panel_state["inventory_open"] = false
position.x = drawer_width
hide()
return
func close_drawer(_immediately: bool = false) -> void:
_set_drawer_open(false)
func _set_drawer_open(value: bool) -> void:
@ -124,10 +116,9 @@ func _set_drawer_open(value: bool) -> void:
_slide_tween.kill()
_drawer_open = value
GameState.hud_panel_state["inventory_open"] = value
if value and not visible:
position = Vector2(drawer_width, 0)
show()
_slide_tween = create_tween()
_slide_tween.tween_property(self, "position:x", 0.0 if value else drawer_width, 0.25).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT if value else Tween.EASE_IN)
if not value:
_slide_tween.tween_callback(hide)
position = Vector2.ZERO
visible = value
if value:
var manager := get_parent().get_parent().get_node_or_null("FloatingWindows")
if manager != null:
manager.call("focus_window", get_parent())

View File

@ -0,0 +1 @@
uid://bc7qva7gpdo01

View File

@ -0,0 +1 @@
uid://d2cbqokcr60c5

View File

@ -0,0 +1,122 @@
extends Node
const GAME := preload("res://scenes/game/game.tscn")
func motion(point: Vector2, relative: Vector2 = Vector2.ZERO) -> void:
var event := InputEventMouseMotion.new()
event.position = point
event.global_position = point
event.relative = relative
event.button_mask = MOUSE_BUTTON_MASK_LEFT if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) else 0
get_viewport().push_input(event, true)
func mouse(point: Vector2, pressed: bool) -> void:
var event := InputEventMouseButton.new()
event.position = point
event.global_position = point
event.button_index = MOUSE_BUTTON_LEFT
event.pressed = pressed
event.button_mask = MOUSE_BUTTON_MASK_LEFT if pressed else 0
get_viewport().push_input(event, true)
func click(point: Vector2) -> void:
motion(point)
mouse(point, true)
mouse(point, false)
func drag(header: Control, displacement: Vector2) -> void:
var origin := header.get_global_rect().position + Vector2(15, header.get_global_rect().size.y * 0.5)
motion(origin)
mouse(origin, true)
motion(origin + displacement, displacement)
mouse(origin + displacement, false)
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
GameState.start_new_run()
var game := GAME.instantiate() as Node2D
add_child(game)
await get_tree().process_frame
await get_tree().process_frame
game.set_physics_process(false)
var ship := game.get_node("PlayerShip") as CharacterBody2D
ship.set_physics_process(false)
ship.get_node("WeaponController").set_physics_process(false)
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.set_physics_process(false)
var hud := game.get_node("HUD")
var manager := hud.get_node("HUDRoot/FloatingWindows")
var craft: PanelContainer = hud.get("_crafting_window")
var inventory: Control = hud.get("inventory_panel")
var drawer: Control = hud.get("_inventory_clip")
var sidebar: Control = hud.get("_sidebar")
hud.call("_toggle_inventory")
craft.show()
await get_tree().process_frame
await get_tree().process_frame
# Put inventory directly behind crafting. Its salvage button occupies the same pixel.
drawer.set_meta("floating_window_dragged", true)
var recipe: Button = craft.get_node("Contents/CraftingPanel").get("_recipe_buttons")["parts"]
var point := recipe.get_global_rect().get_center()
var first_slot := inventory.get_node("Contents/CargoGrid").get_child(0) as Control
drawer.global_position += point - first_slot.get_global_rect().get_center()
manager.call("focus_window", drawer)
manager.call("focus_window", craft)
var stock := ship.get_node("ShipInventory") as ShipInventory
var item := ItemInstance.new()
item.definition = preload("res://resources/items/plasma_cannon.tres")
assert(stock.add_item(item))
GameState.captain.minerals.merge({"iron": 2, "titanium": 2, "aluminium": 2}, true)
GameState.captain.emit_changed()
await get_tree().process_frame
assert(manager.call("window_at", point) == craft)
click(point)
assert(GameState.captain.parts == 1.0, "Top crafting button must receive the click")
assert(stock.cargo[0] == item, "Underlying inventory must not consume the click")
assert(craft.z_index > drawer.z_index)
assert(craft.get_index() > drawer.get_index())
# Dragging a title moves its window; content clicks do not start a window drag.
var before := craft.global_position
drag(craft.get_node("Contents/Header"), Vector2(-80, -60))
assert(craft.global_position.is_equal_approx(before + Vector2(-80, -60)))
await get_tree().process_frame
assert(craft.global_position.is_equal_approx(before + Vector2(-80, -60)))
# The exposed inventory header can activate and raise the lower window.
drawer.global_position = Vector2(70, 80)
manager.call("focus_window", craft)
var inv_header := inventory.get_node("Contents/Header") as Control
click(inv_header.get_global_rect().position + Vector2(10, 8))
assert(drawer.z_index > craft.z_index and drawer.get_index() > craft.get_index())
before = drawer.global_position
drag(inv_header, Vector2(100, 80))
assert(drawer.global_position.is_equal_approx(before + Vector2(100, 80)))
hud.call("_layout_inventory")
assert(drawer.global_position.is_equal_approx(before + Vector2(100, 80)))
# Close buttons do not turn into drag handles; reopening preserves the moved position.
click(inventory.get_node("Contents/Header/CloseButton").get_global_rect().get_center())
assert(not inventory.visible)
inventory.call("_set_drawer_open", true)
assert(drawer.global_position.is_equal_approx(before + Vector2(100, 80)))
# Sidebar dragging survives equipment relayout and viewport clamping.
manager.call("focus_window", sidebar)
before = sidebar.global_position
drag(sidebar.get_node("Scroll/Contents/ShipEquipmentPanel/Contents/Title"), Vector2(-100, -50))
hud.call("_layout_sidebar")
assert(sidebar.global_position.is_equal_approx(before + Vector2(-100, -50)))
# The captain modal remains draggable while paused and blocks windows below it.
var skills: CanvasLayer = hud.get("_captain_skills_ui")
skills.call("open_management")
await get_tree().process_frame
await get_tree().process_frame
assert(get_tree().paused)
var panel: Control = skills.get("_panel")
before = panel.global_position
drag((skills.get("_contents") as Control).get_node("Header"), Vector2(50, 30))
assert(panel.global_position.is_equal_approx(before + Vector2(50, 30)))
skills.call("_layout")
assert(panel.global_position.is_equal_approx(before + Vector2(50, 30)))
skills.call("_close")
assert(not get_tree().paused)
assert(not ship.get_node("WeaponController").get("_fire_buttons_down")[0])
print("FLOATING_WINDOWS_TEST: PASS (real GUI click routing over inventory, matching z/tree order, title drag, focus/raise, close/reopen, relayout persistence, paused captain modal, gameplay input blocked)")
get_tree().quit()

View File

@ -0,0 +1 @@
uid://dus5cwp87y35y

View File

@ -0,0 +1,4 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tests/ships/floating_windows_test.gd" id="1"]
[node name="FloatingWindowsTest" type="Node"]
script = ExtResource("1")