feat: complete mobile UI implementation

This commit is contained in:
shenlei
2026-09-04 19:54:33 +09:00
parent 9a4a044da5
commit b19d5b5dd3
132 changed files with 6643 additions and 180 deletions
+277 -14
View File
@@ -67,6 +67,18 @@ var _drag_from := -1
var _combine_from := -1 # Shift 选中的“使用到物品”来源格
var _hint: Label
var _money_button: BaseButton
var _mobile_mode := false
var _mobile_selected_ui := -1
var _mobile_press_cell := -1
var _mobile_touch_index := -1
var _mobile_long_press_token := 0
var _mobile_detail_dialog: Control
var _mobile_dragging := false
var _mobile_drag_ui := -1
var _mobile_press_position_value := Vector2.ZERO
var _mobile_long_press_ready := false
const MOBILE_LONG_PRESS_SECONDS := 0.55
const MOBILE_DRAG_THRESHOLD := 14.0
func setup(ui_manager: CanvasLayer, m2client: Node, proto_node: Node, assets: String,
il: RefCounted = null) -> void:
@@ -82,15 +94,35 @@ func setup(ui_manager: CanvasLayer, m2client: Node, proto_node: Node, assets: St
uiscript_dir = assets.path_join("uiscript")
if client and client.has_signal("inventory_changed"):
client.inventory_changed.connect(_on_inv_changed)
set_process_input(true)
func is_open() -> bool:
return not _win.is_empty() and is_instance_valid(_win.get("root"))
func set_mobile_mode(enabled: bool) -> void:
_mobile_mode = enabled
mobile_selected_clear()
if enabled:
_set_hint("点按物品选择;再次点按使用,点另一格移动")
func mobile_selected() -> int:
return _mobile_selected_ui
func toggle() -> void:
if is_open(): close()
else: open()
func close() -> void:
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog) and ui:
ui.close(_mobile_detail_dialog)
_mobile_detail_dialog = null
_mobile_long_press_token += 1
_mobile_press_cell = -1
_mobile_touch_index = -1
_mobile_dragging = false
_mobile_drag_ui = -1
_mobile_press_position_value = Vector2.ZERO
_mobile_long_press_ready = false
if item_mouse and item_mouse.has_method("unregister_owner"):
item_mouse.unregister_owner(self)
if item_mouse.has_method("is_attached") and item_mouse.is_attached():
@@ -100,6 +132,7 @@ func close() -> void:
_win = {}
_cells.clear()
_combine_from = -1
_mobile_selected_ui = -1
_hint = null
func open() -> void:
@@ -312,6 +345,22 @@ func _apply_belt_locks(eq: Array) -> void:
cell.tooltip_text = "需要腰带等级 %d" % BELT_RULES[local]
func _on_cell_input(ui_idx: int, e: InputEvent) -> void:
if _mobile_mode and e is InputEventScreenTouch:
var touch := e as InputEventScreenTouch
if touch.pressed:
_begin_mobile_cell_press(ui_idx, touch.index, touch.position)
elif touch.index == _mobile_touch_index:
_finish_mobile_cell_press(ui_idx, touch.index)
var cell: Panel = _cells.get(ui_idx, null)
if cell:
cell.accept_event()
return
if _mobile_mode and e is InputEventScreenDrag:
_handle_mobile_cell_drag(ui_idx, e as InputEventScreenDrag)
var drag_cell: Panel = _cells.get(ui_idx, null)
if drag_cell:
drag_cell.accept_event()
return
if not (e is InputEventMouseButton):
return
var cell: Panel = _cells[ui_idx]
@@ -361,6 +410,199 @@ func _on_cell_input(ui_idx: int, e: InputEvent) -> void:
move_to(_drag_from, over, 1)
_drag_from = -1
func _mobile_cell_tap(ui_idx: int) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or bool(cell.get_meta("locked", false)):
_set_hint("该格当前不可用")
return
var vnum := int(cell.get_meta("vnum", 0))
if _mobile_selected_ui < 0:
if vnum == 0:
_set_hint("这里没有物品")
return
_mobile_selected_ui = ui_idx
_set_hint("已选择 %s;再次点按使用,点另一格移动" % _item_label(ui_idx))
return
if _mobile_selected_ui == ui_idx:
use(ui_idx)
mobile_selected_clear()
return
var from := _mobile_selected_ui
mobile_selected_clear()
if move_to(from, ui_idx, 1):
_set_hint("已发送移动请求")
else:
_set_hint("物品移动请求发送失败")
func _begin_mobile_cell_press(ui_idx: int, touch_index: int,
position := Vector2(-1, -1)) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or bool(cell.get_meta("locked", false)):
return
_mobile_press_cell = ui_idx
_mobile_touch_index = touch_index
_mobile_dragging = false
_mobile_drag_ui = -1
_mobile_long_press_ready = false
var center: Vector2 = _cells[ui_idx].get_global_rect().get_center()
_mobile_press_position_value = position if position.x >= 0.0 and position.y >= 0.0 else center
_mobile_long_press_token += 1
var token := _mobile_long_press_token
get_tree().create_timer(MOBILE_LONG_PRESS_SECONDS).timeout.connect(func():
if token != _mobile_long_press_token or _mobile_press_cell != ui_idx:
return
_mobile_long_press_ready = true
_show_mobile_detail(ui_idx))
func _finish_mobile_cell_press(ui_idx: int, touch_index: int) -> void:
if touch_index != _mobile_touch_index or ui_idx != _mobile_press_cell:
return
if _mobile_dragging:
_mobile_dragging = false
_mobile_drag_ui = -1
_mobile_press_cell = -1
_mobile_touch_index = -1
_mobile_press_position_value = Vector2.ZERO
_mobile_long_press_ready = false
return
_mobile_long_press_token += 1
_mobile_touch_index = -1
_mobile_press_cell = -1
_mobile_long_press_ready = false
if _mobile_detail_dialog == null:
_mobile_cell_tap(ui_idx)
func _handle_mobile_cell_drag(ui_idx: int, event: InputEventScreenDrag) -> void:
if event.index != _mobile_touch_index or ui_idx != _mobile_press_cell:
return
var distance := event.position.distance_to(_mobile_press_position())
if _mobile_dragging:
return
# A long press first exposes the item detail, as on the reference tooltip
# path. Continuing to drag that same finger converts the gesture into a
# real item drag, so a user can move across inventory/equipment targets
# without relying on right-click or a tiny desktop drag handle.
if not _mobile_long_press_ready or distance < MOBILE_DRAG_THRESHOLD:
return
_start_mobile_drag(ui_idx, event.index, event.position)
func _input(event: InputEvent) -> void:
# Once a long press has opened the detail card, the card can cover the
# source cell and steal GUI events. Keep the source touch at the feature
# level so dragging out of that card still reaches MouseController.
if not _mobile_mode or _mobile_touch_index < 0 or _mobile_press_cell < 0:
return
if event is InputEventScreenDrag and event.index == _mobile_touch_index:
_handle_mobile_cell_drag(_mobile_press_cell, event as InputEventScreenDrag)
elif event is InputEventScreenTouch and not event.pressed \
and event.index == _mobile_touch_index:
_finish_mobile_cell_press(_mobile_press_cell, event.index)
func _mobile_press_position() -> Vector2:
return _mobile_press_position_value
func _start_mobile_drag(ui_idx: int, touch_index: int, position: Vector2) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or int(cell.get_meta("vnum", 0)) == 0:
return
if item_mouse == null or not item_mouse.has_method("attach_item"):
_set_hint("当前窗口不支持拖放")
return
var wire := _to_wire(ui_idx)
var metadata: Dictionary = client.get_item(wire[0], wire[1]) \
if client and client.has_method("get_item") else {}
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog):
_close_mobile_detail()
_mobile_long_press_token += 1
_mobile_dragging = true
_mobile_drag_ui = ui_idx
item_mouse.attach_item(wire[0], wire[1], int(cell.get_meta("vnum", 0)),
_item_count(ui_idx), _icon(int(cell.get_meta("vnum", 0))), "inventory",
metadata, touch_index, position)
_set_hint("拖动中:松手放置到目标格")
func _show_mobile_detail(ui_idx: int) -> void:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null or int(cell.get_meta("vnum", 0)) == 0 or ui == null:
return
if _mobile_detail_dialog and is_instance_valid(_mobile_detail_dialog):
ui.close(_mobile_detail_dialog)
var vnum := int(cell.get_meta("vnum", 0))
var count := int(cell.get_meta("count", 1))
var dialog := Panel.new()
dialog.name = "MobileItemDetail"
dialog.size = Vector2(360, 220)
dialog.set_meta("mobile_title", "物品详情")
dialog.set_meta("mobile_modal", true)
dialog.set_meta("mobile_modal_close", Callable(self, "_close_mobile_detail"))
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.04, 0.065, 0.1, 0.98)
bg.border_color = Color(0.72, 0.56, 0.25, 0.95)
bg.set_border_width_all(1)
bg.set_corner_radius_all(8)
dialog.add_theme_stylebox_override("panel", bg)
var title := Label.new()
title.text = _item_label(ui_idx)
title.position = Vector2(18, 52)
title.size = Vector2(324, 28)
title.add_theme_font_size_override("font_size", 15)
title.add_theme_color_override("font_color", Color(0.98, 0.84, 0.52))
dialog.add_child(title)
var detail := Label.new()
detail.text = "物品编号:%d\n数量:%d\n\n%s" % [vnum, count, String(cell.tooltip_text)]
detail.position = Vector2(18, 84)
detail.size = Vector2(324, 70)
detail.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
detail.add_theme_font_size_override("font_size", 10)
detail.add_theme_color_override("font_color", Color(0.78, 0.86, 0.96))
dialog.add_child(detail)
var select := Button.new()
select.text = "选择"
select.position = Vector2(18, 168)
select.size = Vector2(100, 40)
select.pressed.connect(func():
_close_mobile_detail()
_mobile_cell_tap(ui_idx))
dialog.add_child(select)
var use_button := Button.new()
use_button.text = "使用"
use_button.position = Vector2(130, 168)
use_button.size = Vector2(100, 40)
use_button.pressed.connect(func():
_close_mobile_detail()
use(ui_idx)
mobile_selected_clear())
dialog.add_child(use_button)
var cancel := Button.new()
cancel.text = "关闭"
cancel.position = Vector2(242, 168)
cancel.size = Vector2(100, 40)
cancel.pressed.connect(_close_mobile_detail)
dialog.add_child(cancel)
_mobile_detail_dialog = dialog
ui.open(dialog, true)
func _close_mobile_detail() -> void:
var dialog := _mobile_detail_dialog
_mobile_detail_dialog = null
if dialog and is_instance_valid(dialog) and ui:
ui.close(dialog)
func _item_label(ui_idx: int) -> String:
var cell: Panel = _cells.get(ui_idx, null)
if cell == null:
return "物品"
var vnum := int(cell.get_meta("vnum", 0))
if proto and proto.has_method("item") and vnum != 0:
var data: Dictionary = proto.item(vnum)
var name := String(data.get("locale_name", data.get("name", "")))
if name != "":
return name
return "#%d" % vnum
func mobile_selected_clear() -> void:
_mobile_selected_ui = -1
func _drop_mouse_item(payload: Dictionary, target_ui: int) -> bool:
if client == null or payload.is_empty() or target_ui < 0:
return false
@@ -516,7 +758,9 @@ func _ask_count(title_text: String, max_count: int, on_confirm: Callable) -> voi
return
var dialog := Panel.new()
dialog.position = Vector2(760, 420)
dialog.size = Vector2(240, 132)
dialog.size = Vector2(360, 210) if _mobile_mode else Vector2(240, 132)
if _mobile_mode:
dialog.set_meta("mobile_title", title_text)
var bg := StyleBoxFlat.new()
bg.bg_color = Color(0.05, 0.06, 0.09, 0.97)
bg.border_color = Color(0.7, 0.55, 0.22, 0.9)
@@ -524,28 +768,47 @@ func _ask_count(title_text: String, max_count: int, on_confirm: Callable) -> voi
dialog.add_theme_stylebox_override("panel", bg)
var title := Label.new()
title.text = title_text
title.position = Vector2(12, 10)
title.position = Vector2(18, 50) if _mobile_mode else Vector2(12, 10)
if _mobile_mode:
title.add_theme_font_size_override("font_size", 15)
dialog.add_child(title)
var spin := SpinBox.new()
spin.min_value = 1
spin.max_value = max(1, max_count)
spin.value = 1
spin.step = 1
spin.allow_greater = false
spin.position = Vector2(12, 42)
spin.size = Vector2(216, 28)
dialog.add_child(spin)
var amount_input: Control
if _mobile_mode:
var edit := LineEdit.new()
edit.name = "CountInput"
edit.text = "1"
edit.placeholder_text = "1 - %d" % max(1, max_count)
edit.max_length = 8
edit.virtual_keyboard_type = LineEdit.KEYBOARD_TYPE_NUMBER
edit.position = Vector2(18, 88)
edit.size = Vector2(324, 40)
amount_input = edit
dialog.add_child(edit)
else:
var spin := SpinBox.new()
spin.min_value = 1
spin.max_value = max(1, max_count)
spin.value = 1
spin.step = 1
spin.allow_greater = false
spin.position = Vector2(12, 42)
spin.size = Vector2(216, 28)
amount_input = spin
dialog.add_child(spin)
var ok := Button.new()
ok.text = "确认"
ok.position = Vector2(76, 88)
ok.position = Vector2(18, 150) if _mobile_mode else Vector2(76, 88)
ok.size = Vector2(154, 44) if _mobile_mode else Vector2(58, 28)
ok.pressed.connect(func():
var amount := int(spin.value)
var amount := int((amount_input as LineEdit).text) if amount_input is LineEdit else int((amount_input as SpinBox).value)
amount = clampi(amount, 1, max(1, max_count))
ui.close(dialog)
on_confirm.call(amount))
dialog.add_child(ok)
var cancel := Button.new()
cancel.text = "取消"
cancel.position = Vector2(142, 88)
cancel.position = Vector2(188, 150) if _mobile_mode else Vector2(142, 88)
cancel.size = Vector2(154, 44) if _mobile_mode else Vector2(58, 28)
cancel.pressed.connect(func(): ui.close(dialog))
dialog.add_child(cancel)
ui.open(dialog, true)