# UiBuild (P1) —— uiscript 嵌套 Dictionary → Godot Control 树。 # # var spec := UiScript.new().parse_file(path) # var r := UiBuild.build(spec, assets_root) # add_child(r.root) # r.nodes["accept"].pressed.connect(...) # 按 uiscript 里的 "name" 取节点 # # 覆盖 uiscript 最常用的 ~18 种 type;其它降级为 Control / Button / TextureRect。 # board/thinboard 的九宫格用 ui_kit(真实 ymir 贴图,缺失则 StyleBoxFlat 兜底)。 extends RefCounted const UiKit = preload("res://ui_kit.gd") const UiAssets = preload("res://ui/ui_assets.gd") # spec: 顶层 window dict。返回 { root: Control, nodes: {name: Control} } static func build(spec: Dictionary, assets_root: String) -> Dictionary: var ctx := {"assets": assets_root, "nodes": {}} var root := _make(spec, ctx, Vector2i.ZERO) if root == null: root = Control.new() return {"root": root, "nodes": ctx["nodes"]} # --- recursion --------------------------------------------------------- static func _i(spec: Dictionary, key: String, def := 0) -> int: var v = spec.get(key, def) return int(v) if (v is int or v is float or v is bool or v is String) else def static func _str(v) -> String: if v == null: return "" return v if v is String else str(v) static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> Control: var type := _str(spec.get("type", "window")) var w := _i(spec, "width", 0) var h := _i(spec, "height", 0) if type == "titlebar" and h == 0: h = 23 elif type == "horizontalbar" and h == 0: h = 17 elif (type == "grid_table" or type == "slot") and (w == 0 or h == 0): if w == 0 and spec.has("x_count"): var xc := maxi(1, _i(spec, "x_count", 1)) var xs := _i(spec, "x_step", 32) var xb := _i(spec, "x_blank", 0) w = xc * (xs + xb) if h == 0 and spec.has("y_count"): var yc := maxi(1, _i(spec, "y_count", 1)) var ys := _i(spec, "y_step", 32) var yb := _i(spec, "y_blank", 0) h = yc * (ys + yb) var node := _make_by_type(type, spec, ctx, Vector2i(w, h)) if node == null: node = Control.new() node.name = _safe_name(_str(spec.get("name", type))) var halign := _str(spec.get("text_horizontal_align", "")) var valign := _str(spec.get("text_vertical_align", "")) var allalign := _str(spec.get("all_align", "")) if type == "text": if allalign == "center": if w == 0: w = parent_size.x if h == 0: h = parent_size.y else: if halign == "center" and w == 0: w = parent_size.x if parent_size.x > 0 else 100 if (valign == "center" or _str(spec.get("vertical_align", "")) == "center") and h == 0: h = parent_size.y if parent_size.y > 0 else 17 if (w == 0 or h == 0) and node is TextureRect and (node as TextureRect).texture != null: var tex: Texture2D = (node as TextureRect).texture if w == 0: w = tex.get_width() if h == 0: h = tex.get_height() elif (w == 0 or h == 0) and node is Button: var btn := node as Button var nrm_sb = btn.get_theme_stylebox("normal") if nrm_sb is StyleBoxTexture and (nrm_sb as StyleBoxTexture).texture != null: var tex: Texture2D = (nrm_sb as StyleBoxTexture).texture if w == 0: w = tex.get_width() if h == 0: h = tex.get_height() if w > 0: node.custom_minimum_size = Vector2(w, h) node.size = Vector2(w, h) # 位置:x/y + horizontal_align / vertical_align(相对父) var x := _i(spec, "x", 0) var y := _i(spec, "y", 0) if type == "text" and halign == "center" and _i(spec, "width", 0) == 0: x = 0 match _str(spec.get("horizontal_align", "left")): "center": x = (parent_size.x - w) / 2 + x "right": x = parent_size.x - w - x match _str(spec.get("vertical_align", "top")): "center": if type == "text" and (valign == "center" or _str(spec.get("vertical_align", "")) == "center") and h == parent_size.y: pass else: y = (parent_size.y - h) / 2 + y "bottom": y = parent_size.y - h - y node.position = Vector2(x, y) var nm := _str(spec.get("name", "")) if nm != "": ctx["nodes"][nm] = node var self_size := Vector2i(w, h) if (w > 0 and h > 0) else parent_size for child in spec.get("children", []): if child is Dictionary: var c := _make(child, ctx, self_size) if c: node.add_child(c) return node static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: Vector2i) -> Control: var assets: String = ctx["assets"] match type: "window", "box": var c := Control.new() c.mouse_filter = Control.MOUSE_FILTER_PASS return c "board", "thinboard", "board_with_titlebar": var p := _board(assets, "thinboard" if type == "thinboard" else "board", sz) if type == "board_with_titlebar" and spec.has("title"): var tl := Label.new() tl.name = "TitleText" tl.text = _str(spec.get("title", "")) tl.position = Vector2(12, 6) p.add_child(tl) p.set_meta("titlebar_h", 24) return p "titlebar": return _titlebar(assets, sz.x, spec) "text": var lbl := Label.new() lbl.text = _str(spec.get("text", "")) if spec.has("text_color"): lbl.add_theme_color_override("font_color", _argb(int(spec["text_color"]))) elif spec.has("color"): lbl.add_theme_color_override("font_color", _argb(int(spec["color"]))) elif spec.has("r") and spec.has("g") and spec.has("b"): var r := float(spec["r"]) var g := float(spec["g"]) var b := float(spec["b"]) var a := float(spec.get("a", 1.0)) lbl.add_theme_color_override("font_color", Color(r, g, b, a)) else: lbl.add_theme_color_override("font_color", Color(0.8549, 0.8549, 0.8549)) var font_size := 12 var fs_spec := _str(spec.get("fontsize", "")).to_upper() var fn_spec := _str(spec.get("fontname", "")).to_upper() if fs_spec == "LARGE" or "LARGE" in fn_spec: font_size = 14 elif fs_spec == "SMALL" or "SMALL" in fn_spec: font_size = 9 lbl.add_theme_font_size_override("font_size", font_size) var halign := _str(spec.get("text_horizontal_align", "")) var valign := _str(spec.get("text_vertical_align", "")) if _str(spec.get("all_align", "")) == "center": halign = "center" valign = "center" match halign: "center": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER "right": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _: lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT match valign: "center": lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER "bottom": lbl.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM _: lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE return lbl "button", "toggle_button", "radio_button": var b := Button.new() b.text = _str(spec.get("text", "")) b.toggle_mode = (type == "toggle_button" or type == "radio_button") if spec.has("text_color"): var col := _argb(int(spec["text_color"])) b.add_theme_color_override("font_color", col) b.add_theme_color_override("font_pressed_color", col) b.add_theme_color_override("font_hover_color", col) b.add_theme_color_override("font_focus_color", col) elif spec.has("color"): var col := _argb(int(spec["color"])) b.add_theme_color_override("font_color", col) b.add_theme_color_override("font_pressed_color", col) b.add_theme_color_override("font_hover_color", col) b.add_theme_color_override("font_focus_color", col) b.vertical_icon_alignment = VERTICAL_ALIGNMENT_CENTER b.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER b.alignment = HORIZONTAL_ALIGNMENT_CENTER b.clip_text = true var btn_fsize := 12 var fs_spec := _str(spec.get("fontsize", "")).to_upper() var fn_spec := _str(spec.get("fontname", "")).to_upper() if fs_spec == "LARGE" or "LARGE" in fn_spec: btn_fsize = 14 elif fs_spec == "SMALL" or "SMALL" in fn_spec: btn_fsize = 9 b.add_theme_font_size_override("font_size", btn_fsize) var nrm := UiAssets.load_tex(assets, _str(spec.get("default_image", ""))) var ovr := UiAssets.load_tex(assets, _str(spec.get("over_image", ""))) var dwn := UiAssets.load_tex(assets, _str(spec.get("down_image", ""))) var empty := StyleBoxEmpty.new() empty.content_margin_left = 0 empty.content_margin_right = 0 empty.content_margin_top = 0 empty.content_margin_bottom = 0 if nrm: b.add_theme_stylebox_override("normal", _sb_tex(nrm)) b.add_theme_stylebox_override("hover", _sb_tex(ovr if ovr else nrm)) b.add_theme_stylebox_override("pressed", _sb_tex(dwn if dwn else nrm)) b.add_theme_stylebox_override("disabled", _sb_tex(nrm)) b.add_theme_stylebox_override("focus", empty) var tex_w := nrm.get_width() var tex_h := nrm.get_height() b.custom_minimum_size = Vector2(tex_w, tex_h) b.size = Vector2(tex_w, tex_h) # 40250 对齐:进入节点树时重设尺寸,防止被 Godot 默认最小高度撑大到 31px b.tree_entered.connect(func(): b.size = b.custom_minimum_size ) if btn_fsize + 6 > tex_h: btn_fsize = maxi(8, tex_h - 6) b.add_theme_font_size_override("font_size", btn_fsize) else: b.add_theme_stylebox_override("normal", empty) b.add_theme_stylebox_override("hover", empty) b.add_theme_stylebox_override("pressed", empty) b.add_theme_stylebox_override("focus", empty) b.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND return b "image", "expanded_image", "ani_image", "mark": var ir := TextureRect.new() ir.texture = UiAssets.load_tex(assets, _str(spec.get("image", ""))) ir.expand_mode = TextureRect.EXPAND_IGNORE_SIZE ir.stretch_mode = TextureRect.STRETCH_SCALE if type == "expanded_image" \ else TextureRect.STRETCH_KEEP ir.mouse_filter = Control.MOUSE_FILTER_IGNORE return ir "editline": var e := LineEdit.new() e.text = _str(spec.get("text", "")) e.secret = bool(spec.get("secret", false)) if spec.has("input_limit"): e.max_length = int(spec["input_limit"]) return e "line": var ln := ColorRect.new() ln.color = Color(1, 1, 1, 0.15) if _i(spec, "height", 0) == 0: ln.custom_minimum_size = Vector2(_i(spec, "width", 100), 1) return ln "listbox", "candidate_list": return ItemList.new() "gauge", "bar": var pb := ProgressBar.new() pb.show_percentage = false pb.value = 100.0 return pb "horizontalbar": return _horizontalbar(assets, _i(spec, "width", 100)) "grid_table", "slotbar", "slot": # Metin2 物品格容器:有 start_index/x_count 或显式 "slot" 元组 -> 建子格; # 否则 grid_table 退化成普通 GridContainer。 var cells := _slot_cells(spec) if cells.is_empty() and type == "grid_table": var g := GridContainer.new() g.columns = maxi(1, _i(spec, "x_count", _i(spec, "column", 1))) return g var s := Control.new() s.set_meta("is_slot_container", true) var base_img := _str(spec.get("image", "")) var base_tex: Texture2D = UiAssets.load_tex(assets, base_img) if base_img != "" else null for cd in cells: var cell := Panel.new() cell.name = "slot_%d" % int(cd["index"]) cell.position = Vector2(cd["x"], cd["y"]) cell.custom_minimum_size = Vector2(cd["w"], cd["h"]) cell.size = Vector2(cd["w"], cd["h"]) cell.set_meta("slot_index", int(cd["index"])) cell.mouse_filter = Control.MOUSE_FILTER_STOP if base_tex != null: cell.add_theme_stylebox_override("panel", _sb_tex(base_tex)) s.add_child(cell) return s "sliderbar": var slider := HSlider.new() # 40250 sliderbar.sub (175×12), sliderbar_cursor.sub (53×12) slider.custom_minimum_size = Vector2(175, 12) slider.size = Vector2(175, 12) slider.min_value = 0.0 slider.max_value = 1.0 slider.step = 0.01 var track_tex := UiAssets.load_tex(assets, "d:/ymir work/ui/game/windows/sliderbar.sub") var grab_tex := UiAssets.load_tex(assets, "d:/ymir work/ui/game/windows/sliderbar_cursor.sub") if track_tex: var sb_track := StyleBoxTexture.new() sb_track.texture = track_tex sb_track.content_margin_left = 0 sb_track.content_margin_right = 0 sb_track.content_margin_top = 0 sb_track.content_margin_bottom = 0 slider.add_theme_stylebox_override("slider", sb_track) if grab_tex: slider.add_theme_icon_override("grabber", grab_tex) slider.add_theme_icon_override("grabber_highlight", grab_tex) slider.add_theme_icon_override("grabber_disabled", grab_tex) return slider "scrollbar", "thin_scrollbar": return VScrollBar.new() _: return Control.new() # --- helpers --------------------------------------------------------- static func _board(assets: String, prefix: String, sz: Vector2i) -> Panel: var p := Panel.new() if sz.x > 0 and sz.y > 0: p.custom_minimum_size = Vector2(sz) p.size = Vector2(sz) var np: NinePatchRect = null if assets != "": var m := 16 if prefix == "thinboard" else 32 var base_sz := 32 if prefix == "thinboard" else 128 np = UiKit.board(assets, prefix, m, base_sz) if np and np.texture: np.mouse_filter = Control.MOUSE_FILTER_IGNORE p.add_child(np) np.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) p.get_child(0).show_behind_parent = true p.add_theme_stylebox_override("panel", StyleBoxEmpty.new()) else: var sb := StyleBoxFlat.new() sb.bg_color = Color(0.10, 0.11, 0.14, 0.92) sb.border_color = Color(0.35, 0.32, 0.26) sb.set_border_width_all(1) sb.set_corner_radius_all(3) p.add_theme_stylebox_override("panel", sb) return p static func _titlebar(assets: String, width: int, spec: Dictionary) -> Control: var t := Control.new() var w := maxi(64, width) t.custom_minimum_size = Vector2(w, 23) t.size = Vector2(w, 23) t.set_meta("is_titlebar", true) # Background textures (left, center, right) var left_tex := UiKit.ui_tex(assets, "titlebar_left") var center_tex := UiKit.ui_tex(assets, "titlebar_center") var right_tex := UiKit.ui_tex(assets, "titlebar_right") if left_tex and center_tex and right_tex: var img_left := TextureRect.new() img_left.name = "TitleBarLeft" img_left.texture = left_tex img_left.position = Vector2.ZERO img_left.size = Vector2(32, 23) img_left.expand_mode = TextureRect.EXPAND_IGNORE_SIZE img_left.mouse_filter = Control.MOUSE_FILTER_IGNORE t.add_child(img_left) var img_center := TextureRect.new() img_center.name = "TitleBarCenter" img_center.texture = center_tex img_center.position = Vector2(32, 0) img_center.size = Vector2(maxi(0, w - 64), 23) img_center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE img_center.stretch_mode = TextureRect.STRETCH_TILE img_center.mouse_filter = Control.MOUSE_FILTER_IGNORE t.add_child(img_center) var img_right := TextureRect.new() img_right.name = "TitleBarRight" img_right.texture = right_tex img_right.position = Vector2(w - 32, 0) img_right.size = Vector2(32, 23) img_right.expand_mode = TextureRect.EXPAND_IGNORE_SIZE img_right.mouse_filter = Control.MOUSE_FILTER_IGNORE t.add_child(img_right) else: var p := Panel.new() p.set_anchors_preset(Control.PRESET_FULL_RECT) var sb := StyleBoxFlat.new() sb.bg_color = Color(0.2, 0.2, 0.25, 0.9) p.add_theme_stylebox_override("panel", sb) p.mouse_filter = Control.MOUSE_FILTER_IGNORE t.add_child(p) # Close Button var btn_close := Button.new() btn_close.name = "CloseButton" var nrm := UiAssets.load_tex(assets, "d:/ymir work/ui/public/close_button_01.sub") var ovr := UiAssets.load_tex(assets, "d:/ymir work/ui/public/close_button_02.sub") var dwn := UiAssets.load_tex(assets, "d:/ymir work/ui/public/close_button_03.sub") if nrm: btn_close.add_theme_stylebox_override("normal", _sb_tex(nrm)) if ovr: btn_close.add_theme_stylebox_override("hover", _sb_tex(ovr)) if dwn: btn_close.add_theme_stylebox_override("pressed", _sb_tex(dwn)) var bw := nrm.get_width() var bh := nrm.get_height() btn_close.size = Vector2(bw, bh) btn_close.position = Vector2(w - bw - 3, 3) else: btn_close.text = "×" btn_close.size = Vector2(16, 16) btn_close.position = Vector2(w - 19, 3) btn_close.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND t.add_child(btn_close) t.set_meta("close_button", btn_close) return t static func _horizontalbar(assets: String, width: int) -> Control: var w := maxi(96, width) var bar := Control.new() bar.custom_minimum_size = Vector2(w, 17) bar.size = Vector2(w, 17) bar.mouse_filter = Control.MOUSE_FILTER_PASS var left_tex := UiKit.ui_tex(assets, "horizontalbar_left") var center_tex := UiKit.ui_tex(assets, "horizontalbar_center") var right_tex := UiKit.ui_tex(assets, "horizontalbar_right") if left_tex and center_tex and right_tex: var img_left := TextureRect.new() img_left.name = "HorizontalBarLeft" img_left.texture = left_tex img_left.position = Vector2.ZERO img_left.size = Vector2(32, 17) img_left.mouse_filter = Control.MOUSE_FILTER_IGNORE bar.add_child(img_left) var img_center := TextureRect.new() img_center.name = "HorizontalBarCenter" img_center.texture = center_tex img_center.position = Vector2(32, 0) img_center.size = Vector2(maxi(0, w - 64), 17) img_center.expand_mode = TextureRect.EXPAND_IGNORE_SIZE img_center.stretch_mode = TextureRect.STRETCH_TILE img_center.mouse_filter = Control.MOUSE_FILTER_IGNORE bar.add_child(img_center) var img_right := TextureRect.new() img_right.name = "HorizontalBarRight" img_right.texture = right_tex img_right.position = Vector2(w - 32, 0) img_right.size = Vector2(32, 17) img_right.mouse_filter = Control.MOUSE_FILTER_IGNORE bar.add_child(img_right) else: var sb := StyleBoxFlat.new() sb.bg_color = Color(0.2, 0.22, 0.25, 0.85) var p := Panel.new() p.set_anchors_preset(Control.PRESET_FULL_RECT) p.add_theme_stylebox_override("panel", sb) bar.add_child(p) return bar # slot 容器的子格:显式 "slot" 元组,或 start_index/x_count/y_count/x_step/y_step 网格。 static func _slot_cells(spec: Dictionary) -> Array: var cells := [] if spec.get("slot", null) is Array: for sd in spec["slot"]: if sd is Dictionary: cells.append({"index": _i(sd, "index"), "x": _i(sd, "x"), "y": _i(sd, "y"), "w": _i(sd, "width", 32), "h": _i(sd, "height", 32)}) elif spec.has("x_count"): var si := _i(spec, "start_index", 0) var xc := maxi(1, _i(spec, "x_count", 1)) var yc := maxi(1, _i(spec, "y_count", 1)) var xs := _i(spec, "x_step", 32) var ys := _i(spec, "y_step", 32) var xb := _i(spec, "x_blank", 0) var yb := _i(spec, "y_blank", 0) for row in yc: for col in xc: cells.append({"index": si + row * xc + col, "x": col * (xs + xb), "y": row * (ys + yb), "w": xs, "h": ys}) return cells static func _sb_tex(t: Texture2D) -> StyleBoxTexture: var sb := StyleBoxTexture.new() sb.texture = t sb.content_margin_left = 0 sb.content_margin_top = 0 sb.content_margin_right = 0 sb.content_margin_bottom = 0 return sb static func _argb(v: int) -> Color: # 0xAARRGGBB var a := (v >> 24) & 0xff var r := (v >> 16) & 0xff var g := (v >> 8) & 0xff var b := v & 0xff if a == 0: a = 255 return Color8(r, g, b, a) static func _safe_name(s: String) -> String: var out := s for bad: String in [".", ":", "@", "/", "%", " "]: out = out.replace(bad, "_") return out if out != "" else "node"