ui: extract PledgerSkin common visual components and refactor WalletWindow

This commit is contained in:
ZHU MINCHAO committed 2026-11-04 09:57:18 +08:00
1 parent fb29b61a6a
commit d7d2fec536
2 files changed
+265 -69

No files matched your search

+255
View File
@@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Decentralized
{
/// <summary>
/// PLEDGER 硬件风格公共皮肤与组件:
/// - 机身:自画像素九宫格 ui_hw_shell(圆角石墨,左上受光边、右下背光边),3× 像素放大
/// - 刻字:游戏 machinery 窗口的标题栏与分隔线设为透明;标题当机身刻字(灰色粗体、字距 18、居中)
/// - 屏幕:黑色边框 ui_hw_bezel 包一块浅色墨水屏 ui_hw_paper(#D8DAD0),左右对称居中
/// - 文字:深墨色 Ink,次要信息灰 _dim,警示暗红 _note
/// - 按钮:黑底白字触控键 Key / KeyText;选中的项反色(白底黑框黑字加粗)加准星角标 ui_hw_sel,不用 [方括号]
/// - 数据表:列右对齐,整行可点,选中行整行反色(黑底白字)
/// - 滚动区:背景透明露出墨水屏底色,滚动条滑块为墨色
/// </summary>
public static class PledgerSkin
{
public static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f);
public static readonly Color Key = new Color(0.16f, 0.16f, 0.19f);
public static readonly Color KeyText = new Color(0.93f, 0.93f, 0.90f);
public static readonly Color Dim = new Color(0.38f, 0.39f, 0.40f);
public static readonly Color Alert = new Color(0.62f, 0.16f, 0.12f);
public static readonly Color Title = new Color(0.62f, 0.64f, 0.70f);
public static readonly Color Paper = new Color(0.847f, 0.855f, 0.816f); // #D8DAD0
public static readonly Color RowNormal = new Color(0f, 0f, 0f, 0.04f);
public static readonly Color Transparent = new Color(0f, 0f, 0f, 0f);
public static readonly Color Separator = new Color(0.12f, 0.12f, 0.14f, 0.18f);
private static readonly Dictionary<string, Sprite> Sliced = new Dictionary<string, Sprite>();
/// <summary>把本 mod 的像素图做成九宫格贴到 Image 上:边角 border 像素不拉伸,一个像素放大成 3 个界面单位。</summary>
public static void Slice(Image img, string key, int border)
{
var src = Items.ModSprite(key);
if (img == null || src == null) return;
if (!Sliced.TryGetValue(key, out var sp))
{
src.texture.filterMode = FilterMode.Point;
sp = Sprite.Create(src.texture, src.rect, new Vector2(0.5f, 0.5f), src.pixelsPerUnit, 0, SpriteMeshType.FullRect, new Vector4(border, border, border, border));
Sliced[key] = sp;
}
img.sprite = sp;
img.type = Image.Type.Sliced;
img.pixelsPerUnitMultiplier = 100f / (3f * sp.pixelsPerUnit);
img.color = Color.white;
}
/// <summary>屏上的选择按钮(币种、档位、子页):选中的一个画成白底黑框黑字、四角带准星角标。</summary>
public static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick, ICollection<GameObject> picked)
{
b.AddButton(text, onClick);
if (selected && picked != null) picked.Add(b.Last.root);
}
/// <summary>数据表行定义:整行高亮与点击回调。</summary>
public sealed class RowDef
{
public GameObject Root;
public Image Image;
public bool Selected;
public Action OnClick;
}
/// <summary>数据表的一行:列右对齐(第一列默认左对齐),整行可点击,选中行反色。</summary>
public static void TableRow(CustomUIBuilder b, float totalW, float height, float[] widths, TextAnchor[] aligns, string[] texts, bool selected, Action onClick, ICollection<RowDef> rows, ICollection<Image> clear)
{
b.BeginPanel(totalW, height, TextAnchor.MiddleLeft, 6f, 0f);
var root = b.Last.root;
var img = b.Last.image;
if (rows != null) rows.Add(new RowDef { Root = root, Image = img, Selected = selected, OnClick = onClick });
b.BeginRow(0f, TextAnchor.MiddleLeft);
for (int i = 0; i < widths.Length; i++)
{
float w = widths[i];
var align = (aligns != null && i < aligns.Length) ? aligns[i] : (i == 0 ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight);
b.BeginPanel(w, height - 6f, align, 0f, 0f);
if (clear != null) clear.Add(b.Last.image);
b.AddLabel(i < texts.Length ? texts[i] : "");
b.End();
}
b.End();
b.End();
}
/// <summary>数据表表头:与表行同对齐方式,加粗,无底色。</summary>
public static void TableHeader(CustomUIBuilder b, float totalW, float height, float[] widths, TextAnchor[] aligns, string[] headers, ICollection<Image> clear)
{
b.BeginPanel(totalW, height, TextAnchor.MiddleLeft, 6f, 0f);
if (clear != null) clear.Add(b.Last.image);
b.BeginRow(0f, TextAnchor.MiddleLeft);
for (int i = 0; i < widths.Length; i++)
{
float w = widths[i];
var align = (aligns != null && i < aligns.Length) ? aligns[i] : (i == 0 ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight);
b.BeginPanel(w, height - 6f, align, 0f, 0f);
if (clear != null) clear.Add(b.Last.image);
b.AddLabel(i < headers.Length ? "<b>" + headers[i] + "</b>" : "");
b.End();
}
b.End();
b.End();
}
/// <summary>应用整套 PLEDGER 皮肤:机身、边框、墨水屏、刻字、文字颜色、触控键、滚动条与数据表行。</summary>
public static void Apply(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, ICollection<GameObject> picked, ICollection<Image> clear, IEnumerable<RowDef> rows = null)
{
if (win?.root == null) return;
Slice(win.root.GetComponent<Image>(), "ui_hw_shell", 10);
if (bezel != null) Slice(bezel, "ui_hw_bezel", 5);
if (paper != null) Slice(paper, "ui_hw_paper", 3);
// 标题栏和分隔线透明,标题当机身刻字
foreach (var name in new[] { "header", "rule" })
{
var img = win.root.transform.Find(name)?.GetComponent<Image>();
if (img != null) img.color = Transparent;
}
if (win.frame?.titleText != null)
{
var t = win.frame.titleText;
t.color = Title;
t.fontStyle = FontStyles.Bold;
t.characterSpacing = 18f;
t.fontSize = 16f;
t.horizontalAlignment = HorizontalAlignmentOptions.Center;
}
var content = win.frame?.contentRoot?.GetComponent<VerticalLayoutGroup>();
if (content != null) content.childAlignment = TextAnchor.UpperCenter;
if (screen == null) return;
// 收集所有表格行 root,避免按钮样式覆盖整行
var rowRoots = new HashSet<GameObject>();
if (rows != null)
{
foreach (var r in rows)
if (r?.Root != null) rowRoots.Add(r.Root);
}
// 屏上文字:note 暗红,dim 灰,其余深墨色
foreach (var t in screen.GetComponentsInChildren<TextMeshProUGUI>(true))
{
if (t.GetComponentInParent<Button>() != null || t.GetComponentInParent<TMP_InputField>() != null) continue;
t.color = t.gameObject.name.Contains("_note") ? Alert : t.gameObject.name.Contains("_dim") ? Dim : Ink;
}
// 屏上输入框
foreach (var inp in screen.GetComponentsInChildren<TMP_InputField>(true))
{
if (inp.textComponent != null) inp.textComponent.color = Ink;
if (inp.placeholder is TextMeshProUGUI ph) ph.color = Dim;
}
// 分隔线:墨水屏上为淡墨色细线
foreach (var img in screen.GetComponentsInChildren<Image>(true))
{
if (img.gameObject.name.Contains("separator"))
{
img.sprite = MachineryStyle.White;
img.color = Separator;
}
}
// 屏上普通按钮与选择按钮
foreach (var btn in screen.GetComponentsInChildren<Button>(true))
{
if (rowRoots.Contains(btn.gameObject)) continue;
bool on = picked != null && picked.Contains(btn.gameObject);
var img = btn.GetComponent<Image>();
if (img != null)
{
if (on)
{
Slice(img, "ui_hw_sel", 5);
btn.transition = Selectable.Transition.None;
}
else
{
img.color = Key;
}
}
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true))
{
t.color = on ? Ink : KeyText;
if (on) t.fontStyle |= FontStyles.Bold;
}
}
// 滚动视图:背景全透明露出墨水屏底色,滑块墨色
foreach (var scroll in screen.GetComponentsInChildren<ScrollRect>(true))
{
var bg = scroll.GetComponent<Image>();
if (bg != null) bg.color = Transparent;
if (scroll.viewport != null)
{
var vpImg = scroll.viewport.GetComponent<Image>();
if (vpImg != null) vpImg.color = Transparent;
}
if (scroll.verticalScrollbar != null)
{
var sb = scroll.verticalScrollbar;
var track = sb.GetComponent<Image>();
if (track != null) track.color = Transparent;
if (sb.handleRect != null)
{
var handleImg = sb.handleRect.GetComponent<Image>();
if (handleImg != null)
{
handleImg.sprite = MachineryStyle.White;
handleImg.color = Ink;
}
}
}
}
// 清理辅助 Panel 底图
if (clear != null)
{
foreach (var img in clear)
if (img != null) img.color = Transparent;
}
// 数据表行:反色高亮与整行点击
if (rows != null)
{
foreach (var r in rows)
{
if (r?.Root == null || r.Image == null) continue;
r.Image.sprite = MachineryStyle.White;
r.Image.type = Image.Type.Simple;
r.Image.raycastTarget = true;
r.Image.color = r.Selected ? Ink : RowNormal;
foreach (var t in r.Root.GetComponentsInChildren<TextMeshProUGUI>(true))
{
// 避免修改行内子按钮的文字颜色
var pBtn = t.GetComponentInParent<Button>();
if (pBtn != null && pBtn.gameObject != r.Root) continue;
t.color = r.Selected ? KeyText : Ink;
}
if (r.OnClick != null)
{
var btn = r.Root.GetComponent<Button>() ?? r.Root.AddComponent<Button>();
btn.transition = Selectable.Transition.None;
btn.onClick.RemoveAllListeners();
var act = r.OnClick;
btn.onClick.AddListener(() => act());
}
}
}
}
}
}
+10 -69
View File
@@ -89,9 +89,6 @@ namespace Decentralized
return t.ToArray();
}
private static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f),
Key = new Color(0.16f, 0.16f, 0.19f), KeyText = new Color(0.93f, 0.93f, 0.90f),
Dim = new Color(0.38f, 0.39f, 0.40f), Alert = new Color(0.62f, 0.16f, 0.12f);
public static string NameOf(Wallet w)
{
@@ -190,83 +187,27 @@ namespace Decentralized
private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok, Device dev)
{
if (win?.root == null) return;
Slice(win.root.GetComponent<Image>(), "ui_hw_shell", 10);
Slice(bezel, "ui_hw_bezel", 5);
Slice(paper, "ui_hw_paper", 3);
// 标题栏和分隔线透明,标题当作机身上的刻字
foreach (var name in new[] { "header", "rule" })
var rows = _rows.Select(x => new PledgerSkin.RowDef
{
var img = win.root.transform.Find(name)?.GetComponent<Image>();
if (img != null) img.color = new Color(0, 0, 0, 0);
}
if (win.frame?.titleText != null)
{
var t = win.frame.titleText;
t.color = new Color(0.62f, 0.64f, 0.70f); t.fontStyle = FontStyles.Bold; t.characterSpacing = 18f; t.fontSize = 16f; t.horizontalAlignment = HorizontalAlignmentOptions.Center;
}
var content = win.frame?.contentRoot?.GetComponent<VerticalLayoutGroup>();
if (content != null) content.childAlignment = TextAnchor.UpperCenter;
if (plate != null) plate.color = new Color(0, 0, 0, 0);
foreach (var t in screen.GetComponentsInChildren<TextMeshProUGUI>(true))
{
if (t.GetComponentInParent<Button>() != null || t.GetComponentInParent<TMP_InputField>() != null) continue;
t.color = t.gameObject.name.Contains("_note") ? Alert : t.gameObject.name.Contains("_dim") ? Dim : Ink;
}
foreach (var btn in screen.GetComponentsInChildren<Button>(true))
{
var img = btn.GetComponent<Image>();
bool on = _picked.Contains(btn.gameObject);
if (img != null) { if (on) Slice(img, "ui_hw_sel", 5); else img.color = Key; }
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) { t.color = on ? Ink : KeyText; if (on) t.fontStyle |= FontStyles.Bold; }
}
foreach (var img in _clear) if (img != null) img.color = new Color(0, 0, 0, 0);
// 余额行:整行可点,选中的一行反色高亮
foreach (var (root, img, asset) in _rows)
{
if (root == null || img == null) continue;
bool on = asset == dev.Asset;
img.sprite = MachineryStyle.White; img.type = Image.Type.Simple; img.raycastTarget = true;
img.color = on ? Ink : new Color(0, 0, 0, 0.04f);
foreach (var t in root.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = on ? KeyText : Ink;
var btn = root.GetComponent<Button>() ?? root.AddComponent<Button>();
btn.transition = Selectable.Transition.None;
btn.onClick.RemoveAllListeners();
string a = asset;
btn.onClick.AddListener(() => { dev.Asset = a; dev.Note = null; Open(dev); });
}
Root = x.root,
Image = x.image,
Selected = (x.asset == dev.Asset),
OnClick = () => { dev.Asset = x.asset; dev.Note = null; Open(dev); }
});
PledgerSkin.Apply(win, screen, bezel, paper, _picked, _clear, rows);
if (plate != null) plate.color = PledgerSkin.Transparent;
// 签名要按住 ✓:在按键上挂一个按住计时
if (ok != null && dev.Screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>().Device = dev.Address;
}
/// <summary>把本 mod 的像素图做成九宫格贴到 Image 上:边角 border 像素不拉伸,一个像素放大成 3 个界面单位。</summary>
private static void Slice(Image img, string key, int border)
{
var src = Items.ModSprite(key);
if (img == null || src == null) return;
if (!Sliced.TryGetValue(key, out var sp))
{
src.texture.filterMode = FilterMode.Point;
sp = Sprite.Create(src.texture, src.rect, new Vector2(0.5f, 0.5f), src.pixelsPerUnit, 0, SpriteMeshType.FullRect, new Vector4(border, border, border, border));
Sliced[key] = sp;
}
img.sprite = sp;
img.type = Image.Type.Sliced;
img.pixelsPerUnitMultiplier = 100f / (3f * sp.pixelsPerUnit);
img.color = Color.white;
}
private static readonly Dictionary<string, Sprite> Sliced = new Dictionary<string, Sprite>();
private const float RowW = ScreenW - 28f;
private static readonly List<(GameObject root, Image image, string asset)> _rows = new List<(GameObject, Image, string)>();
private static readonly List<Image> _clear = new List<Image>();
private static readonly List<GameObject> _picked = new List<GameObject>();
/// <summary>屏上的选择按钮(币种、手续费档位):选中的一个画成白底黑框黑字、四角带准星角标。</summary>
private static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick)
{
b.AddButton(text, onClick);
if (selected) _picked.Add(b.Last.root);
}
private static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick) =>
PledgerSkin.Pick(b, text, selected, onClick, _picked);
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);