Files
mtgodot-poc/project/ui/mobile/mobile_window_host.gd
T

164 lines
6.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MobileWindowHost —— touch-safe presentation for existing feature windows.
#
# Feature controllers keep ownership of data, packets and permissions. This
# helper only gives their Control roots a landscape-safe frame, a predictable
# back/close affordance and a screen-touch bridge for legacy Button nodes.
extends RefCounted
const NAV_HEIGHT := 40.0
const EDGE_MARGIN := 12.0
const MIN_SCALE := 0.42
static func safe_rect(viewport: Viewport) -> Rect2:
if viewport == null:
return Rect2(Vector2.ZERO, Vector2(844, 390))
var viewport_size := viewport.get_visible_rect().size
return safe_rect_for(viewport_size, DisplayServer.get_display_safe_area())
## Resolve a platform-reported safe area against the current logical viewport.
## Keeping this pure makes the edge cases testable without an Android/iOS
## device and avoids trusting desktop backends that return physical-screen
## coordinates unrelated to the current viewport.
static func safe_rect_for(viewport_size: Vector2, reported: Rect2) -> Rect2:
if viewport_size.x <= 0.0 or viewport_size.y <= 0.0:
return Rect2(Vector2.ZERO, viewport_size)
if reported.size.x <= 0.0 or reported.size.y <= 0.0:
return Rect2(Vector2.ZERO, viewport_size)
var result := reported
# Some desktop backends report a screen-space safe area larger than the
# current viewport. Treat that as unavailable rather than moving windows
# outside the viewport.
if result.size.x < 1.0 or result.size.y < 1.0:
return Rect2(Vector2.ZERO, viewport_size)
if result.position.x < 0.0 or result.position.y < 0.0 \
or result.end.x > viewport_size.x + 1.0 \
or result.end.y > viewport_size.y + 1.0:
return Rect2(Vector2.ZERO, viewport_size)
return result
static func content_size(root: Control) -> Vector2:
if root == null:
return Vector2(320, 240)
var result := root.size
if result.x <= 1.0 or result.y <= 1.0:
result = root.get_combined_minimum_size()
if result.x <= 1.0 or result.y <= 1.0:
result = Vector2(320, 240)
return result
static func install(root: Control, area: Rect2, title: String,
close_cb: Callable, back_cb: Callable = Callable()) -> Dictionary:
if root == null:
return {}
var base_size := content_size(root)
var available := area.size - Vector2(EDGE_MARGIN * 2.0, EDGE_MARGIN * 2.0)
var scale_value := minf(available.x / base_size.x, available.y / base_size.y)
scale_value = maxf(MIN_SCALE, minf(1.0, scale_value))
root.set_anchors_preset(Control.PRESET_TOP_LEFT)
root.size = base_size
root.pivot_offset = Vector2.ZERO
root.scale = Vector2.ONE * scale_value
root.position = area.position + (area.size - base_size * scale_value) * 0.5
root.set_meta("mobile_hosted", true)
root.set_meta("mobile_base_size", base_size)
root.set_meta("mobile_scale", scale_value)
var nav := root.get_node_or_null("__mobile_nav") as Control
if nav == null:
nav = Panel.new()
nav.name = "__mobile_nav"
nav.mouse_filter = Control.MOUSE_FILTER_STOP
nav.z_index = 100
var nav_style := StyleBoxFlat.new()
nav_style.bg_color = Color(0.025, 0.045, 0.08, 0.96)
nav_style.border_color = Color(0.36, 0.6, 0.92, 0.82)
nav_style.border_width_bottom = 1
nav.add_theme_stylebox_override("panel", nav_style)
root.add_child(nav)
nav.position = Vector2.ZERO
nav.size = Vector2(base_size.x, NAV_HEIGHT)
nav.visible = true
var title_label := nav.get_node_or_null("Title") as Label
if title_label == null:
title_label = Label.new()
title_label.name = "Title"
title_label.position = Vector2(88, 0)
title_label.size = Vector2(maxf(80.0, base_size.x - 176.0), NAV_HEIGHT)
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 15)
title_label.add_theme_color_override("font_color", Color(0.95, 0.84, 0.58))
title_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
nav.add_child(title_label)
title_label.text = title if title != "" else String(root.name)
var back := nav.get_node_or_null("Back") as Button
if back == null:
back = Button.new()
back.name = "Back"
back.text = "返回"
back.position = Vector2(8, 4)
back.size = Vector2(70, 32)
back.add_theme_font_size_override("font_size", 12)
nav.add_child(back)
if not back.has_meta("mobile_host_wired"):
back.set_meta("mobile_host_wired", true)
back.pressed.connect(func():
if back_cb.is_valid():
back_cb.call()
elif close_cb.is_valid():
close_cb.call())
var close := nav.get_node_or_null("Close") as Button
if close == null:
close = Button.new()
close.name = "Close"
close.text = "×"
close.add_theme_font_size_override("font_size", 18)
nav.add_child(close)
if not close.has_meta("mobile_host_wired"):
close.set_meta("mobile_host_wired", true)
close.pressed.connect(func(): if close_cb.is_valid(): close_cb.call())
close.position = Vector2(maxf(78.0, base_size.x - 78.0), 4)
close.size = Vector2(70, 32)
_wire_buttons(root)
return {"size": base_size, "scale": scale_value, "area": area}
static func uninstall(root: Control) -> void:
if root == null:
return
var nav := root.get_node_or_null("__mobile_nav") as Control
if nav:
nav.visible = false
root.set_meta("mobile_hosted", false)
static func wire_buttons(root: Node) -> void:
if root:
_wire_buttons(root)
static func _wire_buttons(root: Node) -> void:
for node in root.find_children("*", "BaseButton", true, false):
var button := node as BaseButton
if button == null or button.has_meta("mobile_touch_wired"):
continue
button.set_meta("mobile_touch_wired", true)
button.gui_input.connect(func(event: InputEvent):
_handle_button_touch(button, event))
static func _handle_button_touch(button: BaseButton, event: InputEvent) -> void:
if button == null or not is_instance_valid(button):
return
if event is InputEventScreenTouch:
var touch := event as InputEventScreenTouch
if touch.pressed:
if int(button.get_meta("mobile_touch_index", -1)) < 0:
button.set_meta("mobile_touch_index", touch.index)
else:
if int(button.get_meta("mobile_touch_index", -1)) == touch.index:
button.set_meta("mobile_touch_index", -1)
if not button.disabled:
button.pressed.emit()
button.accept_event()