import { Button, Color, EditBox, Graphics, Label, Layout, Mask, Node, ScrollView, Widget, UITransform, HorizontalTextAlignment, VerticalTextAlignment, Layers, Overflow, view, } from 'cc'; import { GmBuiltinPanelId, GmKind, type IGmCommand, type IGmContentPanel, type IGmHost, type IGmNumberStepperData, type IGmPanelContext } from './GmTypes'; import { GmRegistry } from './GmRegistry'; /** 高对比主题 */ const C = { bg: new Color(6, 8, 14, 255), head: new Color(16, 22, 38, 255), card: new Color(28, 36, 56, 255), cardLine: new Color(120, 160, 220, 200), quick: new Color(18, 92, 78, 255), quickLine: new Color(100, 255, 200, 255), accent: new Color(255, 220, 70, 255), text: new Color(255, 255, 255, 255), textDim: new Color(200, 210, 230, 255), on: new Color(40, 210, 120, 255), off: new Color(70, 78, 96, 255), tabOn: new Color(255, 220, 70, 255), tabOff: new Color(36, 46, 68, 255), chip: new Color(52, 70, 110, 255), setBtn: new Color(220, 140, 40, 255), outline: new Color(0, 0, 0, 220), }; const PAD = 16; /** 头栏高度(含刘海安全区余量) */ const HEAD_H = 108; const TAB_H = 60; const ROW_H = 76; const QUICK_H = 72; const SECTION_H = 36; /** * GM 主视图:设备全屏、高对比;数值支持设为覆盖 + 增减。 */ export class GmView { readonly root: Node; private readonly _host: IGmHost; private readonly _onClose: () => void; private _panel: Node = null!; private _bg: Node = null!; private _head: Node = null!; private _tabBar: Node = null!; private _listHost: Node = null!; private _scrollContent: Node = null!; private _contentHost: Node = null!; private _groups: string[] = []; private _groupIdx = 0; private _activePanel: IGmContentPanel | null = null; private _embedPanel: IGmContentPanel | null = null; private _listMode = true; private _listW = 700; private _listH = 900; private _screenW = 720; private _screenH = 1280; constructor(host: IGmHost, onClose: () => void) { this._host = host; this._onClose = onClose; this.root = new Node('GmShell'); this.root.layer = Layers.Enum.UI_2D; const vs = view.getVisibleSize(); this._screenW = vs.width; this._screenH = vs.height; this.root.addComponent(UITransform).setContentSize(vs.width, vs.height); const w = this.root.addComponent(Widget); w.isAlignLeft = w.isAlignRight = w.isAlignTop = w.isAlignBottom = true; w.left = w.right = w.top = w.bottom = 0; w.alignMode = Widget.AlignMode.ALWAYS; this._buildChrome(); this.refresh(); } /** 按父节点 / 可见区域铺满全屏,并重绘背景 */ layoutFullscreen(): void { let W = view.getVisibleSize().width; let H = view.getVisibleSize().height; const parent = this.root.parent; if (parent?.isValid) { const put = parent.getComponent(UITransform); if (put && put.width > 1 && put.height > 1) { W = put.width; H = put.height; } } const ut = this.root.getComponent(UITransform)!; ut.setContentSize(W, H); this.root.setPosition(0, 0); const widget = this.root.getComponent(Widget); if (widget) { widget.left = widget.right = widget.top = widget.bottom = 0; widget.updateAlignment(); W = ut.width; H = ut.height; } this._screenW = W; this._screenH = H; if (this._bg?.isValid) { this._bg.getComponent(UITransform)!.setContentSize(W, H); this._fillRect(this._bg, C.bg, W, H); } if (this._head?.isValid) { this._head.getComponent(UITransform)!.setContentSize(W, HEAD_H); this._fillRect(this._head, C.head, W, HEAD_H); } if (this._panel?.isValid) { this._panel.getComponent(UITransform)!.setContentSize(W, H); this._panel.getComponent(Widget)?.updateAlignment(); } this._relayout(W, H); this._rebuildTabs(); if (this._listMode) this._rebuildList(); } /** 重建当前分组列表 */ refresh(): void { this._groups = GmRegistry.listGroups(); if (!GmRegistry.hasCommands()) { console.warn('[GM] 未注册任何命令'); } if (this._groupIdx >= this._groups.length) this._groupIdx = 0; this._rebuildTabs(); if (this._listMode) this._rebuildList(); } /** 销毁视图 */ destroy(): void { this._closeActivePanel(); this._closeEmbed(); if (this.root?.isValid) this.root.destroy(); } private _buildChrome(): void { const W = this._screenW; const H = this._screenH; this._bg = new Node('bg'); this._bg.layer = Layers.Enum.UI_2D; this._bg.addComponent(UITransform).setContentSize(W, H); this._fillRect(this._bg, C.bg, W, H); const bgW = this._bg.addComponent(Widget); bgW.isAlignLeft = bgW.isAlignRight = bgW.isAlignTop = bgW.isAlignBottom = true; bgW.left = bgW.right = bgW.top = bgW.bottom = 0; bgW.alignMode = Widget.AlignMode.ALWAYS; this.root.addChild(this._bg); this._panel = new Node('panel'); this._panel.layer = Layers.Enum.UI_2D; this._panel.addComponent(UITransform).setContentSize(W, H); const pw = this._panel.addComponent(Widget); pw.isAlignLeft = pw.isAlignRight = pw.isAlignTop = pw.isAlignBottom = true; pw.left = pw.right = pw.top = pw.bottom = 0; pw.alignMode = Widget.AlignMode.ALWAYS; this.root.addChild(this._panel); this._head = new Node('head'); this._head.layer = Layers.Enum.UI_2D; this._head.addComponent(UITransform).setContentSize(W, HEAD_H); this._fillRect(this._head, C.head, W, HEAD_H); const hw = this._head.addComponent(Widget); hw.isAlignTop = hw.isAlignLeft = hw.isAlignRight = true; hw.isAlignBottom = false; hw.top = 0; hw.left = hw.right = 0; hw.alignMode = Widget.AlignMode.ALWAYS; this._panel.addChild(this._head); const closeBtn = this._pillBtn(this._head, '关闭', 100, 48, () => this._onClose(), C.card, C.text); const cw = closeBtn.addComponent(Widget); cw.isAlignLeft = true; cw.isAlignBottom = true; cw.left = PAD; cw.bottom = 12; cw.isAlignRight = cw.isAlignTop = cw.isAlignVerticalCenter = false; const title = this._label(this._head, 'GM', 42, C.accent, HorizontalTextAlignment.LEFT); title.getComponent(UITransform)!.setContentSize(200, 48); const titleLb = title.getComponent(Label)!; titleLb.isBold = true; const tw = title.addComponent(Widget); tw.isAlignLeft = true; tw.isAlignBottom = true; tw.left = PAD + 112; tw.bottom = 12; tw.isAlignRight = tw.isAlignTop = tw.isAlignHorizontalCenter = tw.isAlignVerticalCenter = false; this._tabBar = new Node('tabs'); this._tabBar.layer = Layers.Enum.UI_2D; this._tabBar.addComponent(UITransform).setContentSize(W - PAD * 2, TAB_H); const tabW = this._tabBar.addComponent(Widget); tabW.isAlignTop = tabW.isAlignLeft = tabW.isAlignRight = true; tabW.top = HEAD_H + 6; tabW.left = tabW.right = PAD; tabW.alignMode = Widget.AlignMode.ALWAYS; const tabLayout = this._tabBar.addComponent(Layout); tabLayout.type = Layout.Type.HORIZONTAL; tabLayout.resizeMode = Layout.ResizeMode.CONTAINER; tabLayout.spacingX = 10; this._panel.addChild(this._tabBar); this._listH = Math.max(200, H - HEAD_H - TAB_H - 28); this._listW = Math.max(200, W - PAD * 2); this._listHost = this._makeScrollArea('list', this._listW, this._listH); const lw = this._listHost.addComponent(Widget); lw.isAlignTop = lw.isAlignLeft = lw.isAlignRight = lw.isAlignBottom = true; lw.top = HEAD_H + TAB_H + 14; lw.bottom = PAD; lw.left = lw.right = PAD; lw.alignMode = Widget.AlignMode.ALWAYS; this._panel.addChild(this._listHost); this._scrollContent = this._listHost.getChildByName('view')!.getChildByName('content')!; this._contentHost = new Node('contentHost'); this._contentHost.layer = Layers.Enum.UI_2D; this._contentHost.addComponent(UITransform).setContentSize(this._listW, this._listH); const chw = this._contentHost.addComponent(Widget); chw.isAlignTop = chw.isAlignLeft = chw.isAlignRight = chw.isAlignBottom = true; chw.top = HEAD_H + TAB_H + 14; chw.bottom = PAD; chw.left = chw.right = PAD; chw.alignMode = Widget.AlignMode.ALWAYS; this._contentHost.active = false; this._panel.addChild(this._contentHost); } private _relayout(W: number, H: number): void { this._listW = Math.max(200, W - PAD * 2); this._listH = Math.max(200, H - HEAD_H - TAB_H - 28); const listUt = this._listHost?.getComponent(UITransform); if (listUt) listUt.setContentSize(this._listW, this._listH); const viewN = this._listHost?.getChildByName('view'); viewN?.getComponent(UITransform)?.setContentSize(this._listW, this._listH); const content = this._scrollContent; if (content) { const cut = content.getComponent(UITransform); if (cut) { cut.setContentSize(this._listW, this._listH); content.setPosition(0, this._listH / 2); } } this._contentHost?.getComponent(UITransform)?.setContentSize(this._listW, this._listH); } private _makeScrollArea(name: string, w: number, h: number): Node { const host = new Node(name); host.layer = Layers.Enum.UI_2D; host.addComponent(UITransform).setContentSize(w, h); const viewN = new Node('view'); viewN.layer = Layers.Enum.UI_2D; viewN.addComponent(UITransform).setContentSize(w, h); viewN.addComponent(Mask); const vw = viewN.addComponent(Widget); vw.isAlignLeft = vw.isAlignRight = vw.isAlignTop = vw.isAlignBottom = true; vw.left = vw.right = vw.top = vw.bottom = 0; vw.alignMode = Widget.AlignMode.ALWAYS; host.addChild(viewN); const content = new Node('content'); content.layer = Layers.Enum.UI_2D; const cut = content.addComponent(UITransform); cut.setContentSize(w, h); cut.setAnchorPoint(0.5, 1); content.setPosition(0, h / 2); const layout = content.addComponent(Layout); layout.type = Layout.Type.VERTICAL; layout.resizeMode = Layout.ResizeMode.CONTAINER; layout.spacingY = 12; layout.paddingTop = 6; layout.paddingBottom = 48; viewN.addChild(content); const sv = host.addComponent(ScrollView); sv.content = content; sv.horizontal = false; sv.vertical = true; sv.brake = 0.75; sv.elastic = true; return host; } private _rebuildTabs(): void { this._tabBar.removeAllChildren(); const n = this._groups.length || 1; const barW = this._tabBar.getComponent(UITransform)?.width ?? this._listW; const btnW = Math.max(110, (barW - (n - 1) * 10) / n); for (let i = 0; i < this._groups.length; i++) { const gname = this._groups[i]; const idx = i; const on = idx === this._groupIdx; const btn = this._pillBtn(this._tabBar, gname, btnW, TAB_H - 6, () => { this._groupIdx = idx; this._showList(); this._rebuildTabs(); this._rebuildList(); }, on ? C.tabOn : C.tabOff, on ? new Color(20, 16, 4, 255) : C.text); const lb = btn.getChildByName('lb')?.getComponent(Label); if (lb) { lb.isBold = true; lb.fontSize = 26; } } this._tabBar.getComponent(Layout)?.updateLayout(); } private _rebuildList(): void { this._closeActivePanel(); this._closeEmbed(); this._scrollContent.removeAllChildren(); const group = this._groups[this._groupIdx]; if (!group) { this._addHint('无分组'); return; } const cmds = GmRegistry.listByGroup(group); if (cmds.length === 0) { this._addHint('本组无命令'); return; } let embedCmd: IGmCommand | null = null; const quicks: IGmCommand[] = []; const toggles: IGmCommand[] = []; const contents: IGmCommand[] = []; for (let i = 0; i < cmds.length; i++) { const c = cmds[i]; if (c.embed && c.kind === GmKind.Content) { embedCmd = c; continue; } if (c.kind === GmKind.Quick) quicks.push(c); else if (c.kind === GmKind.Toggle) toggles.push(c); else if (c.kind === GmKind.Content) contents.push(c); else console.warn('[GM] 未知 kind', c.id, c.kind); } if (embedCmd) { this._openEmbed(embedCmd); return; } this._listHost.active = true; this._contentHost.active = false; if (quicks.length) { this._addSection('快捷 · 点一下即执行'); this._addQuickGrid(quicks); } if (toggles.length) { this._addSection('开关 · 点右侧切换'); for (let i = 0; i < toggles.length; i++) this._addToggle(toggles[i]); } if (contents.length) { this._addSection('数值 / 选择'); for (let i = 0; i < contents.length; i++) { const c = contents[i]; if (c.panelId === GmBuiltinPanelId.numberStepper) this._addInlineStepper(c); else this._addContentEntry(c); } } this._scrollContent.getComponent(Layout)?.updateLayout(); } /** Tab 内嵌整页面板 */ private _openEmbed(cmd: IGmCommand): void { if (!cmd.panelId) { console.warn('[GM] embed 无 panelId', cmd.id); return; } const factory = GmRegistry.getPanelFactory(cmd.panelId); if (!factory) { console.warn('[GM] embed 面板未注册', cmd.panelId); this._host.toast(`面板未注册: ${cmd.panelId}`); return; } this._closeActivePanel(); this._listHost.active = false; this._contentHost.active = true; this._contentHost.removeAllChildren(); this._listMode = true; this._embedPanel = factory(); const ctx: IGmPanelContext = { command: cmd, host: this._host, goBack: () => { /* 嵌入页无返回 */ }, refreshList: () => this.refresh(), }; this._embedPanel.open(this._contentHost, ctx); } private _closeEmbed(): void { if (this._embedPanel) { this._embedPanel.close(); this._embedPanel = null; } } private _addHint(text: string): void { const n = new Node('hint'); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, 48); const lb = n.addComponent(Label); this._styleLabel(lb, text, 28, C.textDim); lb.horizontalAlign = HorizontalTextAlignment.CENTER; this._scrollContent.addChild(n); } private _addSection(title: string): void { const n = new Node(`sec_${title}`); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, SECTION_H); const lb = n.addComponent(Label); this._styleLabel(lb, title, 24, C.accent); lb.isBold = true; lb.horizontalAlign = HorizontalTextAlignment.LEFT; this._scrollContent.addChild(n); } private _addQuickGrid(quicks: IGmCommand[]): void { const gap = 12; const colW = (this._listW - gap) / 2; for (let i = 0; i < quicks.length; i += 2) { const row = new Node(`qrow_${i}`); row.layer = Layers.Enum.UI_2D; row.addComponent(UITransform).setContentSize(this._listW, QUICK_H); const lay = row.addComponent(Layout); lay.type = Layout.Type.HORIZONTAL; lay.spacingX = gap; lay.resizeMode = Layout.ResizeMode.NONE; this._scrollContent.addChild(row); this._addQuickCell(row, quicks[i], colW); if (i + 1 < quicks.length) this._addQuickCell(row, quicks[i + 1], colW); else { const spacer = new Node('sp'); spacer.layer = Layers.Enum.UI_2D; spacer.addComponent(UITransform).setContentSize(colW, QUICK_H); row.addChild(spacer); } lay.updateLayout(); } } private _addQuickCell(parent: Node, cmd: IGmCommand, w: number): void { const n = new Node(cmd.id); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(w, QUICK_H); this._fillRound(n, C.quick, w, QUICK_H, 14, C.quickLine); const lbNode = new Node('lb'); lbNode.layer = Layers.Enum.UI_2D; lbNode.addComponent(UITransform).setContentSize(w - 12, QUICK_H - 10); const lb = lbNode.addComponent(Label); this._styleLabel(lb, cmd.title, 24, C.text); lb.isBold = true; lb.horizontalAlign = HorizontalTextAlignment.CENTER; lb.verticalAlign = VerticalTextAlignment.CENTER; lb.overflow = Overflow.SHRINK; lb.enableWrapText = true; n.addChild(lbNode); n.addComponent(Button); n.on(Button.EventType.CLICK, () => { void this._runQuick(cmd); }, this); parent.addChild(n); } private async _runQuick(cmd: IGmCommand): Promise { if (!cmd.run) { console.warn('[GM] Quick 无 run', cmd.id); this._host.toast('命令未实现'); return; } try { await cmd.run(); this.refresh(); } catch (e) { console.warn('[GM] Quick 执行失败', cmd.id, e); this._host.toast('执行失败'); } } private _addToggle(cmd: IGmCommand): void { const n = new Node(cmd.id); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, ROW_H); this._fillRound(n, C.card, this._listW, ROW_H, 14, C.cardLine); const title = this._label(n, cmd.title, 28, C.text, HorizontalTextAlignment.LEFT); title.getComponent(UITransform)!.setContentSize(this._listW - 180, ROW_H - 8); title.setPosition(-(this._listW / 2) + (this._listW - 180) / 2 + 18, 0); const on = !!(cmd.get?.()); const sw = this._makeSwitch(n, on, () => { if (!cmd.set || !cmd.get) { console.warn('[GM] Toggle 缺少 get/set', cmd.id); return; } cmd.set(!cmd.get()); this.refresh(); }); sw.setPosition(this._listW / 2 - 72, 0); this._scrollContent.addChild(n); } private _makeSwitch(parent: Node, on: boolean, onClick: () => void): Node { const trackW = 100; const trackH = 48; const n = new Node('switch'); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(trackW, trackH); this._fillRound(n, on ? C.on : C.off, trackW, trackH, trackH / 2); const knob = new Node('knob'); knob.layer = Layers.Enum.UI_2D; knob.addComponent(UITransform).setContentSize(38, 38); this._fillRound(knob, C.text, 38, 38, 19); knob.setPosition(on ? 24 : -24, 0); n.addChild(knob); const tag = this._label(n, on ? '开' : '关', 20, on ? new Color(8, 40, 20, 255) : C.text, HorizontalTextAlignment.CENTER); tag.getComponent(UITransform)!.setContentSize(44, 30); tag.setPosition(on ? -20 : 20, 0); n.addComponent(Button); n.on(Button.EventType.CLICK, onClick, this); parent.addChild(n); return n; } /** * 数值卡:EditBox +「设为」覆盖;下方 ± 立刻增减。 */ private _addInlineStepper(cmd: IGmCommand): void { const data = cmd.panelData as IGmNumberStepperData; if (!data?.getValue || !data?.apply) { console.warn('[GM] numberStepper 缺少 panelData', cmd.id); this._addContentEntry(cmd); return; } const cardH = 148; const n = new Node(cmd.id); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, cardH); this._fillRound(n, C.card, this._listW, cardH, 14, C.cardLine); const title = this._label(n, cmd.title, 26, C.textDim, HorizontalTextAlignment.LEFT); title.getComponent(UITransform)!.setContentSize(this._listW * 0.4, 30); title.setPosition(-(this._listW / 2) + this._listW * 0.2 + 14, 48); const cur = Math.floor(Number(data.getValue()) || 0); const editNode = new Node('edit'); editNode.layer = Layers.Enum.UI_2D; const editW = Math.min(200, this._listW * 0.32); editNode.addComponent(UITransform).setContentSize(editW, 48); this._fillRound(editNode, new Color(12, 16, 28, 255), editW, 48, 10, C.accent); const textLbN = new Node('TEXT_LABEL'); textLbN.layer = Layers.Enum.UI_2D; textLbN.addComponent(UITransform).setContentSize(editW - 12, 40); const textLb = textLbN.addComponent(Label); this._styleLabel(textLb, `${cur}`, 28, C.accent); textLb.isBold = true; textLb.horizontalAlign = HorizontalTextAlignment.CENTER; textLb.overflow = Overflow.SHRINK; editNode.addChild(textLbN); const phN = new Node('PLACEHOLDER_LABEL'); phN.layer = Layers.Enum.UI_2D; phN.addComponent(UITransform).setContentSize(editW - 12, 40); const phLb = phN.addComponent(Label); this._styleLabel(phLb, '输入数值', 22, C.textDim); phLb.horizontalAlign = HorizontalTextAlignment.CENTER; editNode.addChild(phN); const eb = editNode.addComponent(EditBox); eb.textLabel = textLb; eb.placeholderLabel = phLb; eb.string = `${cur}`; eb.maxLength = 12; eb.inputMode = EditBox.InputMode.NUMERIC; eb.inputFlag = EditBox.InputFlag.DEFAULT; eb.returnType = EditBox.KeyboardReturnType.DONE; editNode.setPosition(this._listW / 2 - editW / 2 - 130, 48); n.addChild(editNode); const syncEdit = (): void => { eb.string = `${Math.floor(Number(data.getValue()) || 0)}`; }; const setBtn = this._pillBtn(n, '设为', 96, 48, () => { let v = Math.floor(Number(eb.string)); if (!Number.isFinite(v)) { this._host.toast('请输入有效数字'); return; } if (data.min !== undefined && v < data.min) v = data.min; if (data.max !== undefined && v > data.max) v = data.max; data.apply(v, 'set'); syncEdit(); this._host.toast(`${cmd.title} 已设为 ${Math.floor(Number(data.getValue()) || 0)}`); }, C.setBtn, C.text); setBtn.setPosition(this._listW / 2 - 52, 48); const steps = [-100, -10, -1, 1, 10, 100]; const gap = 8; const chipW = Math.min(92, (this._listW - 28 - gap * (steps.length - 1)) / steps.length); const totalW = steps.length * chipW + (steps.length - 1) * gap; const baseX = -totalW / 2 + chipW / 2; for (let i = 0; i < steps.length; i++) { const s = steps[i]; const chip = this._pillBtn(n, s > 0 ? `+${s}` : `${s}`, chipW, 46, () => { const now = Math.floor(Number(data.getValue()) || 0); let next = now + s; if (data.min !== undefined && next < data.min) next = data.min; if (data.max !== undefined && next > data.max) next = data.max; data.apply(next, 'set'); syncEdit(); }, C.chip, C.text); chip.setPosition(baseX + i * (chipW + gap), -36); } const hint = this._label(n, '输入后点「设为」覆盖 · 下方按钮增减', 18, C.textDim, HorizontalTextAlignment.CENTER); hint.getComponent(UITransform)!.setContentSize(this._listW - 24, 24); hint.setPosition(0, 14); this._scrollContent.addChild(n); } private _addContentEntry(cmd: IGmCommand): void { const n = new Node(cmd.id); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(this._listW, ROW_H); this._fillRound(n, C.card, this._listW, ROW_H, 14, C.cardLine); const title = this._label(n, cmd.title, 28, C.text, HorizontalTextAlignment.LEFT); title.getComponent(UITransform)!.setContentSize(280, ROW_H - 8); title.setPosition(-(this._listW / 2) + 150, 0); const sumText = cmd.summary ? cmd.summary() : '选择'; const sum = this._label(n, `${sumText} ›`, 24, C.accent, HorizontalTextAlignment.RIGHT); sum.getComponent(UITransform)!.setContentSize(this._listW - 320, ROW_H - 8); sum.setPosition(this._listW / 2 - (this._listW - 320) / 2 - 16, 0); n.addComponent(Button); n.on(Button.EventType.CLICK, () => this._openContent(cmd), this); this._scrollContent.addChild(n); } private _openContent(cmd: IGmCommand): void { if (!cmd.panelId) { console.warn('[GM] Content 无 panelId', cmd.id); this._host.toast('未配置面板'); return; } const factory = GmRegistry.getPanelFactory(cmd.panelId); if (!factory) { console.warn('[GM] 面板未注册', cmd.panelId); this._host.toast(`面板未注册: ${cmd.panelId}`); return; } this._closeActivePanel(); this._listHost.active = false; this._contentHost.active = true; this._contentHost.removeAllChildren(); this._listMode = false; this._activePanel = factory(); const ctx: IGmPanelContext = { command: cmd, host: this._host, goBack: () => this._showList(), refreshList: () => this.refresh(), }; this._activePanel.open(this._contentHost, ctx); } private _showList(): void { this._closeActivePanel(); this._closeEmbed(); this._contentHost.active = false; this._listHost.active = true; this._listMode = true; this._rebuildList(); } private _closeActivePanel(): void { if (this._activePanel) { this._activePanel.close(); this._activePanel = null; } if (this._contentHost?.isValid) this._contentHost.removeAllChildren(); } private _styleLabel(lb: Label, text: string, size: number, color: Color): void { lb.string = text; lb.fontSize = size; lb.lineHeight = Math.ceil(size * 1.25); lb.color = color; lb.isBold = true; lb.enableOutline = false; lb.verticalAlign = VerticalTextAlignment.CENTER; } private _label(parent: Node, text: string, size: number, color: Color, align: HorizontalTextAlignment): Node { const n = new Node('lb'); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(100, 40); const lb = n.addComponent(Label); this._styleLabel(lb, text, size, color); lb.horizontalAlign = align; parent.addChild(n); return n; } private _pillBtn(parent: Node, text: string, w: number, h: number, onClick: () => void, bg: Color, fg: Color): Node { const n = new Node(text); n.layer = Layers.Enum.UI_2D; n.addComponent(UITransform).setContentSize(w, h); this._fillRound(n, bg, w, h, Math.min(16, h / 2)); const lbNode = new Node('lb'); lbNode.layer = Layers.Enum.UI_2D; lbNode.addComponent(UITransform).setContentSize(w - 8, h - 4); const lb = lbNode.addComponent(Label); this._styleLabel(lb, text, Math.min(26, Math.floor(h * 0.48)), fg); lb.isBold = true; lb.horizontalAlign = HorizontalTextAlignment.CENTER; lb.overflow = Overflow.SHRINK; n.addChild(lbNode); n.addComponent(Button); n.on(Button.EventType.CLICK, onClick, this); parent.addChild(n); return n; } private _fillRect(node: Node, color: Color, w: number, h: number): void { const g = node.getComponent(Graphics) || node.addComponent(Graphics); g.clear(); g.fillColor = color; g.rect(-w / 2, -h / 2, w, h); g.fill(); } private _fillRound(node: Node, color: Color, w: number, h: number, r: number, stroke?: Color): void { const g = node.getComponent(Graphics) || node.addComponent(Graphics); g.clear(); g.fillColor = color; g.roundRect(-w / 2, -h / 2, w, h, r); g.fill(); if (stroke) { g.strokeColor = stroke; g.lineWidth = 2; g.roundRect(-w / 2, -h / 2, w, h, r); g.stroke(); } } }