261 lines
9.6 KiB
GDScript
261 lines
9.6 KiB
GDScript
class_name ABGPageView
|
|
extends Control
|
|
|
|
const EntryChrome = preload("res://entry/entry_chrome.gd")
|
|
const UI_FONT = preload("res://assets/fonts/NotoSansSC.ttf")
|
|
const PageContext = preload("res://foundation/page_context.gd")
|
|
const UIFactory = preload("res://ui/shared/ui_factory.gd")
|
|
const Tokens = preload("res://foundation/ui_tokens.gd")
|
|
|
|
## Passive View base for the new Entry page boundary.
|
|
##
|
|
## A PageView owns Godot nodes, layout, focus and local visual state. It
|
|
## receives a copied ViewData snapshot and emits named intents; it does not
|
|
## resolve services, read Save or make route/economy decisions.
|
|
|
|
signal intent_emitted(intent_id: String, payload: Dictionary)
|
|
|
|
var context: ABGPageContext
|
|
var view_data: Dictionary = {}
|
|
var presentation: Dictionary = {
|
|
"safe_area": 24,
|
|
"text_scale": 1.0,
|
|
}
|
|
|
|
## Every authored page is laid out in this one coordinate system. The root
|
|
## stays 1280x720 and receives one uniform transform for the current viewport;
|
|
## children never recompute their own design scale.
|
|
const DESIGN_CANVAS_SIZE := Vector2(1280.0, 720.0)
|
|
|
|
func layout_fixed_design_canvas(canvas: Control) -> void:
|
|
if not is_instance_valid(canvas):
|
|
return
|
|
var available := size
|
|
if available.x <= 0.0 or available.y <= 0.0:
|
|
return
|
|
var uniform_scale := minf(available.x / DESIGN_CANVAS_SIZE.x, available.y / DESIGN_CANVAS_SIZE.y)
|
|
canvas.position = (available - DESIGN_CANVAS_SIZE * uniform_scale) * 0.5
|
|
canvas.size = DESIGN_CANVAS_SIZE
|
|
canvas.scale = Vector2.ONE * uniform_scale
|
|
canvas.pivot_offset = Vector2.ZERO
|
|
|
|
|
|
func mount(next_context) -> ABGPageView:
|
|
if next_context is ABGPageContext:
|
|
context = next_context.duplicate_context()
|
|
elif next_context is Dictionary:
|
|
context = PageContext.new(next_context)
|
|
else:
|
|
context = PageContext.new()
|
|
return self
|
|
|
|
|
|
func configure_presentation(values: Dictionary = {}) -> ABGPageView:
|
|
for key in values.keys():
|
|
presentation[String(key)] = values[key]
|
|
return self
|
|
|
|
|
|
func render(next_view_data: Dictionary = {}) -> void:
|
|
view_data = next_view_data.duplicate(true)
|
|
_render_view(view_data)
|
|
|
|
|
|
func emit_intent(intent_id: String, payload: Dictionary = {}) -> void:
|
|
intent_emitted.emit(String(intent_id), payload.duplicate(true))
|
|
|
|
|
|
func _render_view(_data: Dictionary) -> void:
|
|
# Concrete Views own their node tree and override this method.
|
|
pass
|
|
|
|
|
|
func clear_view_children() -> void:
|
|
for child in get_children():
|
|
child.free()
|
|
|
|
|
|
func page_locale(data: Dictionary = view_data) -> String:
|
|
return String(data.get("locale", presentation.get("locale", "zh_CN")))
|
|
|
|
|
|
func localize(zh: String, en: String, data: Dictionary = view_data) -> String:
|
|
return zh if page_locale(data) == "zh_CN" else en
|
|
|
|
|
|
func scaled(value: int) -> int:
|
|
return int(round(float(value) * float(presentation.get("text_scale", 1.0))))
|
|
|
|
|
|
func make_label(value: String, font_size: int, color: Color) -> Label:
|
|
var label := UIFactory.label(value, "body", 1.0, color)
|
|
label.add_theme_font_size_override("font_size", font_size)
|
|
return label
|
|
|
|
|
|
func make_entry_chrome(node_name: String, variant: String, icon := "") -> Control:
|
|
var chrome := EntryChrome.new().configure(variant, icon)
|
|
chrome.name = node_name
|
|
chrome.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
return chrome
|
|
|
|
|
|
func make_company_button(node_name: String, text_value: String, intent_id: String, variant: String, icon: String, intent_payload: Dictionary = {}) -> Button:
|
|
var button := UIFactory.command_button(
|
|
node_name,
|
|
text_value,
|
|
variant,
|
|
icon,
|
|
"angular",
|
|
"content" if variant.begins_with("content_") else "company",
|
|
Vector2(190, 52),
|
|
false,
|
|
text_value,
|
|
Callable(),
|
|
scaled(16)
|
|
)
|
|
button.pressed.connect(func(): emit_intent(intent_id, intent_payload), CONNECT_DEFERRED)
|
|
return button
|
|
|
|
|
|
func set_command_state(button: Button, disabled := false, selected := false) -> Button:
|
|
UIFactory.refresh_command_button(button, disabled, selected)
|
|
return button
|
|
|
|
|
|
func make_unified_mvp_shell(host: Control, prefix: String, title: String, subtitle: String, state_text: String, compact := false) -> Dictionary:
|
|
"""Mount the shared MVP shell grammar without owning page behavior.
|
|
|
|
The host remains responsible for geometry and intent wiring. The shell only
|
|
adds a quiet top rail, a state capsule and a bottom rhythm marker so the
|
|
three MVP pages share the same visual syntax over different art masters.
|
|
"""
|
|
if host == null:
|
|
return {}
|
|
var shell := Control.new()
|
|
shell.name = "%sUnifiedShell" % prefix
|
|
shell.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
shell.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
shell.z_index = 2
|
|
host.add_child(shell)
|
|
|
|
var top := Panel.new()
|
|
top.name = "%sShellTopRail" % prefix
|
|
top.anchor_right = 1.0
|
|
top.offset_bottom = 42.0 if not compact else 4.0
|
|
top.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
if compact:
|
|
var compact_top := StyleBoxFlat.new()
|
|
compact_top.bg_color = Color(0, 0, 0, 0)
|
|
compact_top.border_color = Color(Tokens.COLOR_GOLD, 0.62)
|
|
compact_top.set_border_width(SIDE_BOTTOM, Tokens.border_width("hairline"))
|
|
top.add_theme_stylebox_override("panel", compact_top)
|
|
else:
|
|
top.add_theme_stylebox_override("panel", Tokens.surface_style("shell"))
|
|
shell.add_child(top)
|
|
var heading := UIFactory.label(title, "heading", float(presentation.get("text_scale", 1.0)), Tokens.COLOR_SURFACE_INK)
|
|
heading.name = "%sShellTitle" % prefix
|
|
heading.anchor_left = 0.03
|
|
heading.anchor_top = 0.08
|
|
heading.anchor_right = 0.42
|
|
heading.anchor_bottom = 0.92
|
|
heading.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
heading.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
top.add_child(heading)
|
|
var copy := UIFactory.label(subtitle, "micro", float(presentation.get("text_scale", 1.0)), Tokens.COLOR_GOLD_LIGHT)
|
|
copy.name = "%sShellSubtitle" % prefix
|
|
copy.anchor_left = 0.42
|
|
copy.anchor_top = 0.12
|
|
copy.anchor_right = 0.76
|
|
copy.anchor_bottom = 0.88
|
|
copy.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
copy.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
top.add_child(copy)
|
|
var state := UIFactory.label(state_text, "label", float(presentation.get("text_scale", 1.0)), Tokens.COLOR_GOLD_LIGHT)
|
|
state.name = "%sShellState" % prefix
|
|
state.anchor_left = 0.76
|
|
state.anchor_top = 0.08
|
|
state.anchor_right = 0.97
|
|
state.anchor_bottom = 0.92
|
|
state.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
|
state.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
state.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
|
state.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
top.add_child(state)
|
|
if compact:
|
|
heading.visible = false
|
|
copy.visible = false
|
|
state.visible = false
|
|
|
|
var bottom := Panel.new()
|
|
bottom.name = "%sShellBottomRail" % prefix
|
|
bottom.anchor_top = 1.0
|
|
bottom.anchor_right = 1.0
|
|
bottom.offset_top = -34.0 if not compact else -2.0
|
|
bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
if compact:
|
|
var compact_bottom := StyleBoxFlat.new()
|
|
compact_bottom.bg_color = Color(0, 0, 0, 0)
|
|
compact_bottom.border_color = Color(Tokens.COLOR_GOLD, 0.42)
|
|
compact_bottom.set_border_width(SIDE_TOP, Tokens.border_width("hairline"))
|
|
bottom.add_theme_stylebox_override("panel", compact_bottom)
|
|
else:
|
|
bottom.add_theme_stylebox_override("panel", Tokens.surface_style("context"))
|
|
shell.add_child(bottom)
|
|
var footer := UIFactory.label(localize("公会记录 · 当前阶段", "GUILD LEDGER · CURRENT STAGE"), "micro", float(presentation.get("text_scale", 1.0)), Tokens.COLOR_MUTED)
|
|
footer.name = "%sShellFooter" % prefix
|
|
footer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
footer.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
footer.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
footer.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
footer.visible = not compact
|
|
bottom.add_child(footer)
|
|
return {"root": shell, "top": top, "bottom": bottom}
|
|
|
|
|
|
func make_framed_page_safe(prefix: String) -> MarginContainer:
|
|
var safe := MarginContainer.new()
|
|
safe.name = "%sFrameSafeArea" % prefix
|
|
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
safe.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
var margin := maxi(12, int(presentation.get("safe_area", 24)))
|
|
var vertical_margin := 12 if size.y < 500.0 else margin
|
|
safe.add_theme_constant_override("margin_left", margin)
|
|
safe.add_theme_constant_override("margin_right", margin)
|
|
safe.add_theme_constant_override("margin_top", vertical_margin)
|
|
safe.add_theme_constant_override("margin_bottom", vertical_margin)
|
|
add_child(safe)
|
|
return safe
|
|
|
|
|
|
func inset_safe_to_card_face(safe: MarginContainer, edge_x := 0.055, edge_y := 0.15) -> void:
|
|
"""Seat page content inside a shared card's rolled edges.
|
|
|
|
The card's authored roll owns the top/left/right face; content that keeps
|
|
only the touch safe-area margin sits on the roll and gets clipped. M67
|
|
pages call this right after make_content_page_safe (the M64/M65 pages do the
|
|
equivalent inset per page).
|
|
"""
|
|
if safe == null:
|
|
return
|
|
var face_x := int(size.x * edge_x) + 14
|
|
var face_y := int(size.y * edge_y) + 10
|
|
safe.add_theme_constant_override("margin_left", face_x)
|
|
safe.add_theme_constant_override("margin_right", face_x)
|
|
safe.add_theme_constant_override("margin_top", face_y)
|
|
safe.add_theme_constant_override("margin_bottom", int(face_y * 0.55))
|
|
|
|
|
|
func make_content_page_safe(prefix: String) -> MarginContainer:
|
|
var safe := MarginContainer.new()
|
|
safe.name = "%sSafeArea" % prefix
|
|
safe.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
var margin := maxi(12, int(presentation.get("safe_area", 24)))
|
|
var vertical_margin := 12 if size.y < 500.0 else margin
|
|
safe.add_theme_constant_override("margin_left", margin + 12)
|
|
safe.add_theme_constant_override("margin_right", margin + 12)
|
|
safe.add_theme_constant_override("margin_top", vertical_margin + 8)
|
|
safe.add_theme_constant_override("margin_bottom", vertical_margin + 8)
|
|
add_child(safe)
|
|
return safe
|