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(); 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) 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) private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok, Device dev)
{ {
if (win?.root == null) return; if (win?.root == null) return;
Slice(win.root.GetComponent<Image>(), "ui_hw_shell", 10); var rows = _rows.Select(x => new PledgerSkin.RowDef
Slice(bezel, "ui_hw_bezel", 5);
Slice(paper, "ui_hw_paper", 3);
// 标题栏和分隔线透明,标题当作机身上的刻字
foreach (var name in new[] { "header", "rule" })
{ {
var img = win.root.transform.Find(name)?.GetComponent<Image>(); Root = x.root,
if (img != null) img.color = new Color(0, 0, 0, 0); Image = x.image,
} Selected = (x.asset == dev.Asset),
if (win.frame?.titleText != null) OnClick = () => { dev.Asset = x.asset; dev.Note = null; Open(dev); }
{ });
var t = win.frame.titleText; PledgerSkin.Apply(win, screen, bezel, paper, _picked, _clear, rows);
t.color = new Color(0.62f, 0.64f, 0.70f); t.fontStyle = FontStyles.Bold; t.characterSpacing = 18f; t.fontSize = 16f; t.horizontalAlignment = HorizontalAlignmentOptions.Center; if (plate != null) plate.color = PledgerSkin.Transparent;
}
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); });
}
// 签名要按住 ✓:在按键上挂一个按住计时 // 签名要按住 ✓:在按键上挂一个按住计时
if (ok != null && dev.Screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>().Device = dev.Address; 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 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<(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<Image> _clear = new List<Image>();
private static readonly List<GameObject> _picked = new List<GameObject>(); private static readonly List<GameObject> _picked = new List<GameObject>();
/// <summary>屏上的选择按钮(币种、手续费档位):选中的一个画成白底黑框黑字、四角带准星角标。</summary> /// <summary>屏上的选择按钮(币种、手续费档位):选中的一个画成白底黑框黑字、四角带准星角标。</summary>
private static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick) private static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick) =>
{ PledgerSkin.Pick(b, text, selected, onClick, _picked);
b.AddButton(text, onClick);
if (selected) _picked.Add(b.Last.root);
}
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture); public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);