Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions plugins/VelaShell.Plugin.DockerPanel/DockerIcon.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using VelaShell.PluginSdk;

namespace VelaShell.Plugin.DockerPanel;

/// <summary>
/// Docker 面板标签页上的那个鲸鱼图标。
/// </summary>
/// <remarks>
/// <para>
/// <b>为什么是品牌标而不是 lucide 的某个字形。</b>lucide 里没有 Docker,而"容器"这个概念
/// 在那套字形里只有 <c>container</c>(货运集装箱)与 <c>box</c> 之类的近似物 —— 摆在标签条上
/// 谁也认不出它是 Docker。鲸鱼驮着集装箱这个标反而是这一行里辨识度最高的几个之一。
/// </para>
/// <para>
/// <b>实心、视框 1024,两件都必须报给宿主</b>(见 <see cref="PluginIcon" />)。视框不报的话
/// 宿主按 lucide 的 24 缩放,等于把它放大四十多倍,屏幕上什么都看不见;而用描边去画一个实心
/// 图形,得到的是它的轮廓线 —— 这个标本来就是"空心鲸鱼 + 实心集装箱",再描一圈边会糊成一团。
/// </para>
/// <para>
/// 原图只有一段 path,鲸鱼肚子与那只眼睛是靠反向绕向挖出来的洞,
/// evenodd 与 nonzero 两种填充规则画出来一模一样,因此不必在路径上声明 <c>F0</c> / <c>F1</c>。
/// </para>
/// <para>
/// ⚠️ 与面板头上那只鲸鱼(<c>Docker.whale</c>,`DockerTheme.axaml`)**不是一回事**,也不该被
/// "顺手统一"成同一个:那只是描边、视框 24 的手绘字形,与面板里其余几十个 <c>Docker.*</c>
/// 图标成套 —— 它要的是融进界面;标签页这一个要的是在一条挤满标签的标签条上被一眼认出来,
/// 那是品牌标才做得到的事。
/// </para>
/// <para>
/// 单独一个文件而不是塞进 <see cref="DockerPanelPlugin" />:一千多字符的路径数据横在开面板的
/// 逻辑中间,读代码的人要翻很久才能翻过去(Redis 插件的 <c>RedisIcon</c>、AI 插件的
/// <c>AiIcon</c> 同此)。
/// </para>
/// </remarks>
internal static class DockerIcon
{
/// <summary>交给宿主的那份图标:开面板时填进 <c>PanelOptions.Icon</c> 的就是它。</summary>
public static readonly PluginIcon Panel = PluginIcon.Filled(PathData, ViewBoxSize);

/// <summary>视框边长(原始 SVG 是 <c>viewBox="0 0 1024 1024"</c>)。</summary>
public const double ViewBoxSize = 1024d;

/// <summary>鲸鱼字形的路径数据(原始 SVG 那一段 path,原样)。</summary>
public const string PathData =
"M998.016 455.008h-0.992q-24-8-48-12-46.016-8-82.016-0.992-6.016-32-19.008-55.008-19.008-32.992-52.992-44.992l-4.992-3.008-7.008 2.016q-24 7.008-39.008 28.992-28.992 44-12 128.992H59.008q-16 0-24.992 3.008-16 6.016-18.016 22.016-2.016 12.992-2.016 17.504t0 16.512q0 138.016 62.016 219.008 51.008 66.016 146.016 92.992 68.992 20 170.016 20 148.992 0 255.008-48.992 91.008-43.008 148-120 47.008-63.008 71.008-147.008 42.016 8 76-4.992 52.992-20.992 70.016-91.008l4-16.992-18.016-6.016m-130.048 78.976q-12.992-3.008-24.992 4.512t-16 21.504q-30.016 112-104.992 182.016-64 59.008-159.008 86.016-78.016 22.016-172 22.016-176 0-256.992-70.496T53.024 558.08v-8l0.992-12h688q15.008 0 24-11.008t6.016-28q-11.008-52-4-84.992 4.992-26.016 18.016-34.016 26.016 14.016 36.992 50.016 6.016 19.008 6.016 36v28l26.016-10.016q24-8.992 59.008-6.016 24.992 0.992 52 8-12.992 36.992-43.008 47.008-24 8.992-55.008 0.992M71.008 382.016h86.016v86.016H71.008v-86.016m124.992 0h86.016v86.016H196v-86.016m126.016 0h84.992v86.016h-84.992v-86.016m124 0h86.016v86.016h-86.016v-86.016M196 252h86.016v86.016H196V252m126.016 0h84.992v86.016h-84.992V252m124 0h86.016v86.016h-86.016V252m0-129.984h86.016v84.992h-86.016V122.016m125.984 260h86.016v86.016h-86.016v-86.016M228.992 607.008q-22.016 0-36.992 15.488t-15.008 36.992T192 696t36.512 15.008 36.992-15.008 15.488-36.512-15.488-36.992-36.512-15.488m0 64q-4.992 0-8.512-3.488t-3.488-8.512 3.488-8.512 8.512-3.488 8.512 3.488 3.488 8.512-3.488 8.512-8.512 3.488z";
}
4 changes: 3 additions & 1 deletion plugins/VelaShell.Plugin.DockerPanel/PluginMain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ private async Task OpenPanelAsync()
try
{
_panel = await context.Ui.ShowPanelAsync(
new() { Title = "Docker", DisplayMode = PanelDisplayMode.Document },
// 图标不填的话,这条标签顶的是所有插件共用的那个通用插头 —— 与 Redis / S3 /
// 串口的标签并排时,图标那一格等于白占。路径数据与取舍见 DockerIcon。
new() { Title = "Docker", Icon = DockerIcon.Panel, DisplayMode = PanelDisplayMode.Document },
() => new DockerPanelView(viewModel),
context.Shutdown).ConfigureAwait(false);
_panel.Closed += async () =>
Expand Down
1 change: 1 addition & 0 deletions plugins/VelaShell.Plugin.DockerPanel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ SDK 1.1 的远程执行只有两种**文本**形态:整段 UTF-8 解码,或按 `
| 文件 | 内容 |
| --- | --- |
| `PluginMain.cs` | 入口:注册命令、开 / 激活面板、停用时收掉视图模型 |
| `DockerIcon.cs` | 标签页图标:Docker 的鲸鱼标(实心、视框 1024,经 `PanelOptions.Icon` 交给宿主)。lucide 里没有 Docker,而 `container` / `box` 那几个近似字形摆在标签条上认不出是它;不自报则是所有插件共用的通用插头 |
| `Docker/DockerTransport.cs` | 两种传输:SSH 隧道 / 本机 socket-管道 |
| `Docker/DockerClient*.cs` | Engine API 客户端,按域拆 partial |
| `Docker/DockerFrames.cs` | 8 字节多路复用帧解码 + 跨帧 UTF-8 与半行处理 |
Expand Down
66 changes: 66 additions & 0 deletions tests/VelaShell.Plugin.DockerPanel.Tests/DockerIconTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Avalonia;
using Avalonia.Headless;
using Avalonia.Media;

namespace VelaShell.Plugin.DockerPanel.Tests;

/// <summary>
/// 标签页图标那串路径数据:必须真的解析得出几何,而且撑满它自报的视框。
/// <para>
/// 这一层守的是一种**不会报错的失败**:宿主拿到插件给的路径是 <c>Geometry.Parse</c> 一把,
/// 解析不出来就当作没给、退回通用插头 —— 于是路径里打错一个字符,编译过、测试过、
/// 装载过,只是 Docker 标签上悄悄换成了插头。没人盯着标签条看就发现不了。
/// </para>
/// <para>
/// ⚠️ 这里验不了"鲸鱼肚子与那只眼睛还是不是洞":headless 宿主的 <c>FillContains</c> 给的
/// 结果与真实几何对不上(整行整行地报 true,连视框外的点也报)。镂空那件事是另一条路核的 ——
/// 原图那段 path 的内外绕向相反,evenodd 与 nonzero 两种填充规则画出来一模一样。
/// </para>
/// </summary>
[TestClass]
public sealed class DockerIconTests
{
private static HeadlessUnitTestSession _session = null!;

[ClassInitialize]
public static void Init(TestContext _) =>
// 几何要平台后端才建得起来(走的是渲染接口),所以这组也得进 headless 会话。
_session = HeadlessUnitTestSession.GetOrStartForAssembly(typeof(DockerIconTests).Assembly);

private static void OnUi(Action body) =>
_session.Dispatch(() =>
{
body();
return true;
}, CancellationToken.None).GetAwaiter().GetResult();

/// <summary>路径解析得出几何,撑满自报的视框,且不越界。</summary>
[TestMethod]
public void PathData_ParsesAndFillsItsOwnViewBox() =>
OnUi(() =>
{
// 解析不出来会抛(宿主那边则是吞掉、退回通用插头)—— 这一行本身就是一半的验收。
Rect bounds = Geometry.Parse(DockerIcon.PathData).Bounds;

// 撑满:横向几乎顶满 1024,纵向是鲸鱼加上那摞集装箱的高度。截断的路径不会解析失败,
// 只会让图形缺一块 —— 而缺了集装箱的鲸鱼画出来仍是个图标,只是不再是 Docker 的标。
Assert.IsGreaterThan(DockerIcon.ViewBoxSize * 0.9, bounds.Width, $"实际 {bounds}");
Assert.IsGreaterThan(DockerIcon.ViewBoxSize * 0.7, bounds.Height, $"实际 {bounds}");
// 落在视框内:宿主按 min(宽,高)/视框 缩放后**不裁剪**,越界的部分会画到相邻标签上。
Assert.IsGreaterThanOrEqualTo(0d, bounds.X, $"实际 {bounds}");
Assert.IsGreaterThanOrEqualTo(0d, bounds.Y, $"实际 {bounds}");
Assert.IsLessThanOrEqualTo(DockerIcon.ViewBoxSize, bounds.Right, $"实际 {bounds}");
Assert.IsLessThanOrEqualTo(DockerIcon.ViewBoxSize, bounds.Bottom, $"实际 {bounds}");
});

/// <summary>交给宿主的那份是**实心 + 自报视框**,两位错一位图标就画不对。</summary>
[TestMethod]
public void Panel_ReportsAFilledIconWithItsNativeViewBox()
{
// 实心图形被 2px 圆头画笔描边会变成一圈轮廓线;视框留在 lucide 的 24
// 则等于把这张 1024 的图放大四十多倍,屏幕上什么也看不见。
Assert.IsTrue(DockerIcon.Panel.IsFilled);
Assert.AreEqual(1024d, DockerIcon.Panel.ViewBoxSize);
Assert.AreEqual(DockerIcon.PathData, DockerIcon.Panel.PathData);
}
}