📝 docs(code-reader): 代码阅读 Rust 调用链卡片两种排法的原型

This commit is contained in:
cruldra committed 2026-10-15 20:08:03 +11:00
1 parent afa86778d9
commit 324df3d5bd
6 files changed
+19934

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+743
View File
@@ -0,0 +1,743 @@
// 代码阅读 Rust:链式调用的专用卡片「调用链」。从左到右四张板:方案 A 步骤轨道、方案 B 按方法分组、
// 方案 A 展开 setup、短链放在变量初始值里。
// 数据取自 luffy-agent-platform 的 apps/desktop/src-tauri/src/lib.rs 第 461 行起:run 函数最后一句
// `tauri::Builder::default()…run(…)` 整条链和第一个 .plugin 上面那行注释,方法名、实参、注释、命令名照抄;
// 展开 setup 的子块照抄同一段闭包体的前 14 句,上下文里露一角的是它前面的 `let app_state`。
// 短链那张板的 `names` 是手写示例。
// 配色、卡片画法取自同目录 build.js(VS Code Dark Modern);子块左边的竖线取自 react.js,
// 链接色是 Dark Modern 的 textLink.foreground(webview 的 .cr-link 用它)。
// OpenPencil 自动折行量不准,长文字按估出的宽度手动断行,文字框都按内容长。
const C = {
editor: "#1f1f1f",
side: "#181818",
card: "#242424",
borderStrong: "#3c3c3c",
input: "#313131",
guide: "#404040",
fg: "#cccccc",
desc: "#9d9d9d",
dim: "#6e7681",
comment: "#6a9955",
control: "#c586c0",
storage: "#569cd6",
variable: "#9cdcfe",
fn: "#dcdcaa",
type: "#4ec9b0",
string: "#ce9178",
link: "#4daafc",
};
const KIND = {
chain: { label: "调用链", color: C.fn, icon: "workflow" },
variable: { label: "变量", color: "#9cdcfe", icon: "variable" },
statement: { label: "语句", color: "#9d9d9d", icon: "code" },
return: { label: "返回", color: "#c586c0", icon: "corner-down-left" },
if: { label: "如果", color: "#c586c0", icon: "split" },
};
const COMMANDS = [
"device_resume",
"device_connect",
"device_disconnect",
"settings_get",
"settings_set",
"workspace_move",
"open_workspace",
"desktop_state",
"desktop_activity",
"web::preview_open",
"web::preview_embed",
"update_check",
"update_install",
"screen_permission_get",
"notice::desktop_notify",
"screen_permission_open",
"app_restart",
"web::retry_connect",
];
const PLUGIN_NOTE = "新窗口链接只走主窗口 on_new_window 一条路; 插件自带的点击脚本会注进远程网页版, 在那里调 open_url 没有权限";
// 一步一项:arg 是一行写完的实参;sub 是实参本身又是一条链;closure 是收起的闭包;commands 是 generate_handler! 里的命令
const CHAIN = {
start: "tauri::Builder::default()",
steps: [
{ method: "plugin", sub: ["tauri_plugin_opener::Builder::new()", ".open_js_links_on_click(false)", ".build()"], note: PLUGIN_NOTE },
{ method: "plugin", arg: "tauri_plugin_dialog::init()" },
{ method: "plugin", arg: "tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, None)" },
{ method: "plugin", arg: "tauri_plugin_global_shortcut::Builder::new().build()" },
{ method: "plugin", arg: "tauri_plugin_updater::Builder::new().build()" },
{ method: "setup", closure: { params: "|app|", count: "37 句" } },
{ method: "on_window_event", closure: { params: "|window, event|", count: "1 句" } },
{ method: "manage", arg: "app_state" },
{ method: "invoke_handler", macro: "tauri::generate_handler!", commands: COMMANDS },
{ method: "build", arg: "tauri::generate_context!()" },
{ method: "expect", arg: "\"error while building tauri application\"" },
{ method: "run", closure: { params: "|app, event|", head: "match event", count: "3 支" } },
],
};
// 方案 B 的插件表:插件名、配置(Builder 上调的方法或 init 的实参),没配置的写 —
const PLUGINS = [
{ name: "tauri_plugin_opener", config: "open_js_links_on_click(false)" },
{ name: "tauri_plugin_dialog" },
{ name: "tauri_plugin_autostart", config: "tauri_plugin_autostart::MacosLauncher::LaunchAgent, None" },
{ name: "tauri_plugin_global_shortcut" },
{ name: "tauri_plugin_updater" },
];
// 编译条件的中文读法同编译属性的「生效条件」:「非」按运算符、系统名按字符串着色
const CFG = {
macos: [["目标系统 ", C.fg], ["macos", C.string]],
debug: [["调试构建", C.fg]],
release: [["非", C.storage], [" 调试构建", C.fg]],
};
// setup 闭包体的前 14 句(板子截在第 14 句):variable 是名称 / 初始值;call 是跨几行写的调用,排成「调用 … · N 个参数」加实参表
const SETUP_BODY = [
{ kind: "variable", name: "store", note: "记下的转发按原端口先开起来: 执行器重启后, 跑着的开发服务器重连就能接上", value: "app.path().app_data_dir()?.join(\"tunnels.json\")" },
{ kind: "variable", name: "tunnels", value: "Arc::new(Tunnels::new(store))" },
{ kind: "variable", name: "restoring", value: "Arc::clone(&tunnels)" },
{ kind: "statement", line: "tauri::async_runtime::spawn(async move { restoring.restore().await });", kids: [{ kind: "return", line: "restoring.restore().await" }] },
{ kind: "statement", line: "app.manage(tunnels);" },
{
kind: "statement",
cfg: CFG.macos,
call: { callee: "app.manage", tail: ";", args: [{ call: { callee: "Arc::new", args: [{ call: { callee: "previewtls::PreviewTls::new", args: ["app.path().app_data_dir()?.join(\"preview-tls\")"] } }] } }] },
},
{ kind: "variable", name: "credentials", cfg: CFG.debug, call: { callee: "credentials::FileStore::new", args: ["app.path().app_data_dir()?.join(\"dev-credentials.json\")"] } },
{ kind: "variable", name: "credentials", cfg: CFG.release, value: "credentials::Keychain" },
{ kind: "statement", line: "app.manage(Arc::new(credentials) as Arc<dyn CredentialStore>);" },
{ kind: "variable", name: "settings", call: { callee: "Arc::new", args: [{ call: { callee: "settings::Store::new", args: ["app.path().app_data_dir()?.join(\"settings.json\")"] } }] } },
{ kind: "variable", name: "root", value: "settings.get().workspace_root" },
{ kind: "variable", name: "hub", call: { callee: "Arc::new", args: [{ call: { callee: "Hub::new", args: ["machine::facts(&root)", "root", "Arc::new(state::WindowEvents(app.handle().clone()))"] } }] } },
{
kind: "if",
head: "如果 令 ",
line: "Some(e) = settings.load_error()",
kids: [{ kind: "statement", call: { callee: "hub.activity", tail: ";", args: ["\"warn\"", "\"settings\"", "&format!(\"设置文件读不出来, 已按缺省: {e}\")"] } }],
},
{ kind: "statement", line: "apply_autostart(app.handle(), &hub, settings.get().autostart);" },
];
const APP_STATE = { name: "app_state", value: "AppState { connection: ConnectionSlot::default(), linked: Mutex::new(None) }" };
// 手写的短链:let names: Vec<String> = rows.iter().filter(|r| r.active).map(|r| r.name.clone()).collect();
const SHORT = {
name: "names",
type: "Vec<String>",
steps: [
{ verb: "遍历", code: "rows" },
{ verb: "筛选", code: "|r| r.active" },
{ verb: "映射", code: "|r| r.name.clone()" },
{ verb: "收集" },
],
};
const BOARD_W = 1100;
const BOARD_GAP = 80;
const BOARD_PAD = 20;
const LABEL_W = 72;
const METHOD_W = 140;
// 上下文那张变量卡片的整高(上下内边距 8 + 两行 20 + 行距 4),露出它最下面的 SLICE_H
const APP_STATE_H = 60;
const SLICE_H = 32;
// 展开 setup 那张板截在第 14 句(apply_autostart)卡片中间,像滚动中的截图
const SETUP_BOARD_H = 1938;
const MONO = "Menlo";
const UI = "PingFang SC";
// 轨道:主链一步一个实心圆点,竖线是函数色淡到 35%;实参里的小链用空心小圆点
const RAIL = { col: 20, dot: 8, up: 6, line: 2, color: mix(C.fn, C.card, 0.35), fill: C.fn, pad: 6 };
const MINI = { col: 14, dot: 6, up: 7, line: 1, color: mix(C.desc, C.card, 0.45), fill: C.card, stroke: C.desc, pad: 0 };
const SHORT_RAIL = { col: 14, dot: 6, up: 7, line: 2, color: mix(C.fn, C.card, 0.35), fill: C.fn, pad: 0 };
// 估出来比给的宽度还长的文字记在这里,Node 里先跑一遍时检查
const OVERFLOW = [];
// ─── 文字 ────────────────────────────────────────────────────────────────────
function solid(color) {
return [{ type: "solid", color }];
}
function rgb(hex) {
const h = hex.replace("#", "");
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
}
/** color 按 t 盖在 base 上(color-mix(in srgb, color t, base))。 */
function mix(color, base, t) {
const a = rgb(color);
const b = rgb(base);
return "#" + a.map((v, i) => Math.round(v * t + b[i] * (1 - t)).toString(16).padStart(2, "0")).join("");
}
function seg(text, fill, weight) {
return { text, fill, w: weight };
}
const WIDE = /[⺀-鿿 -〿＀-￯“”‘’…—·×→]/;
function charWidth(ch, size, mono) {
if (WIDE.test(ch))
return size;
if (mono)
return size * 0.61;
if (/[A-Z0-9]/.test(ch))
return size * 0.68;
if (/[ilj.,:;'|!()[\] ]/.test(ch))
return size * 0.34;
return size * 0.58;
}
/** 估一段字的宽度,宁大勿小。 */
function widthOf(s, size, mono) {
let w = 0;
for (const ch of s) w += charWidth(ch, size, mono);
return w * 1.04;
}
/** 正文按宽度断行:汉字之间、空格处能断,句读跟着前一个字。 */
function wrapProse(s, maxW, size) {
const tokens = s.match(/[⺀-鿿 -〿＀-￯]|[^\s⺀-鿿 -〿＀-￯]+|\s+/g) || [];
const lines = [""];
let w = 0;
for (const tok of tokens) {
const tw = widthOf(tok, size, false);
if (/^\s+$/.test(tok)) {
if (w > 0) {
lines[lines.length - 1] += tok;
w += tw;
}
continue;
}
if (w > 0 && w + tw > maxW && !/^[,.;:!?,。;:、)」]/.test(tok)) {
lines[lines.length - 1] = lines[lines.length - 1].replace(/\s+$/, "");
lines.push("");
w = 0;
}
lines[lines.length - 1] += tok;
w += tw;
}
return lines.join("\n");
}
const RS_STORAGE = new Set(["let", "mut", "as", "dyn", "async", "move", "fn", "true", "false", "self", "ref"]);
const RS_CONTROL = new Set(["match", "if", "else", "return", "await", "for", "while", "loop", "in"]);
const RS_TOKEN = /"(?:[^"\\]|\\.)*"|[A-Za-z_][A-Za-z0-9_]*|\d+|\s+|[\s\S]/g;
/** Rust 源码按 Dark Modern 着色:路径前段和大写名字是类型色,后面跟括号或 ! 的是函数色。 */
function rs(src) {
const out = [];
let at = 0;
for (const tok of src.match(RS_TOKEN) || []) {
at += tok.length;
const rest = src.slice(at);
let color = C.fg;
if (tok[0] === "\"")
color = C.string;
else if (/^[A-Za-z_]/.test(tok)) {
if (RS_STORAGE.has(tok))
color = C.storage;
else if (RS_CONTROL.has(tok))
color = C.control;
else if (/^[A-Z]/.test(tok) || /^::/.test(rest))
color = C.type;
else if (/^\s*[(!]/.test(rest))
color = C.fn;
else
color = C.variable;
}
const last = out[out.length - 1];
if (last && last.fill === color)
last.text += tok;
else
out.push(seg(tok, color));
}
return out;
}
/** 方法名写成 `.名字`:点是前景色,名字是函数色。 */
function method(name, weight) {
return [seg(".", C.fg), seg(name, C.fn, weight)];
}
/**
* 文字:content 是字符串或着色的段。宽、高跟着字长,不靠 OpenPencil 折行;正文给了 maxW 就手动断行。
* o: size、color、weight、mono、lh(行高 px)、maxW
*/
function text(parent, content, o) {
const opt = o || {};
const size = opt.size || 13;
const mono = Boolean(opt.mono);
let segs = typeof content === "string" ? [seg(content, opt.color || C.fg)] : content;
if (opt.maxW && !mono && segs.length === 1)
segs = [seg(wrapProse(segs[0].text, opt.maxW, size), segs[0].fill, segs[0].w)];
if (opt.maxW) {
for (const line of segs.map((s) => s.text).join("").split("\n"))
if (widthOf(line, size, mono) > opt.maxW)
OVERFLOW.push(line);
}
const node = {
type: "text",
name: segs.map((s) => s.text).join("").replace(/\n/g, " ").slice(0, 40),
fontFamily: mono ? MONO : UI,
fontSize: size,
fontWeight: opt.weight || 400,
lineHeight: (opt.lh || 20) / size,
width: "fit_content",
height: "fit_content",
textGrowth: "auto",
};
if (segs.length === 1) {
node.content = segs[0].text;
node.fill = solid(segs[0].fill);
if (segs[0].w)
node.fontWeight = segs[0].w;
}
else {
node.content = segs.map((s) => {
const piece = { text: s.text, fill: s.fill };
if (s.w)
piece.fontWeight = s.w;
return piece;
});
node.fill = solid(C.fg);
}
return I(parent, node);
}
function code(parent, src, maxW, size) {
return text(parent, rs(src), { mono: true, maxW, size });
}
function icon(parent, name, color, size) {
return I(parent, { type: "icon_font", name, iconFontName: name, width: size, height: size, fill: solid(color) });
}
// ─── 框 ──────────────────────────────────────────────────────────────────────
function box(parent, name, o) {
const opt = o || {};
const node = {
type: "frame",
name,
layout: opt.layout || "horizontal",
width: opt.width || "fill_container",
height: opt.height || "fit_content",
gap: opt.gap == null ? 0 : opt.gap,
clipContent: Boolean(opt.clip),
};
if (opt.x != null) {
node.x = opt.x;
node.y = opt.y || 0;
}
if (opt.padding != null)
node.padding = opt.padding;
if (opt.align)
node.alignItems = opt.align;
if (opt.justify)
node.justifyContent = opt.justify;
if (opt.fill)
node.fill = solid(opt.fill);
if (opt.radius != null)
node.cornerRadius = opt.radius;
if (opt.stroke)
node.stroke = { thickness: 1, fill: solid(opt.stroke) };
return I(parent, node);
}
function row(parent, name, o) {
return box(parent, name, Object.assign({ layout: "horizontal", align: "center" }, o || {}));
}
function col(parent, name, o) {
return box(parent, name, Object.assign({ layout: "vertical" }, o || {}));
}
/** 一行里横排几样东西(方法名后面跟实参或收起的框)。 */
function inline(parent, gap) {
return row(parent, "Inline", { height: 20, gap: gap == null ? 8 : gap });
}
/** 收起 / 展开的闭包、命令:一个小框,前面折叠箭头。 */
function foldBox(parent, open, segs) {
const frame = row(parent, "Fold box", {
width: "fit_content",
height: 20,
padding: [0, 6, 0, 3],
gap: 3,
radius: 4,
fill: C.input,
stroke: C.borderStrong,
});
icon(frame, open ? "chevron-down" : "chevron-right", C.desc, 13);
text(frame, segs, { mono: true, size: 12.5, lh: 18 });
return frame;
}
/** 「闭包 |app| · 37 句」:参数照抄,句数、分支数是提示色。 */
function closureSegs(closure) {
const head = closure.head ? [seg(" · ", C.desc)].concat(rs(closure.head)) : [];
return [seg("闭包 ", C.desc)].concat(rs(closure.params), head, [seg(" · " + closure.count, C.desc)]);
}
// ─── 卡片 ────────────────────────────────────────────────────────────────────
/** 卡片右上角:问 AI、编辑。 */
function tools(parent) {
const bar = row(parent, "Tools", { width: "fit_content", height: 20, gap: 2 });
for (const name of ["sparkles", "pencil"]) {
const button = row(bar, name, { width: 22, height: 20, justify: "center", radius: 4 });
icon(button, name, C.desc, 14);
}
}
/**
* 一张卡片:折叠箭头(有子块才有)、种类图标、内容、右上角两个按钮;描边用种类色。
* w 是卡片外宽;给了 x 就是绝对定位(上下文那一角)。返回内容区和内容区的宽度。
*/
function card(parent, kind, w, o) {
const opt = o || {};
const node = box(parent, kind.label + (opt.name ? " / " + opt.name : ""), {
layout: "horizontal",
width: opt.x != null ? w : "fill_container",
x: opt.x,
y: opt.y,
gap: 10,
padding: [8, 10, 8, 8],
radius: 8,
align: "start",
fill: C.card,
stroke: kind.color,
});
const fold = row(node, "Fold", { width: 14, height: 20, justify: "center" });
if (opt.fold)
icon(fold, opt.fold === "open" ? "chevron-down" : "chevron-right", C.desc, 14);
const kindBox = row(node, "Kind / " + kind.label, { width: 18, height: 20, justify: "center" });
icon(kindBox, kind.icon, kind.color, 18);
const content = col(node, "Content", { gap: 4 });
tools(node);
return { node, content, w: w - 18 - 14 - 18 - 46 - 30 };
}
/** 属性行:左边标签列,右边值。value 是着色的段、字符串,或自己画的函数 (frame, width)。 */
function prop(ctx, label, value, o) {
const opt = o || {};
const line = row(ctx.content, "Prop / " + label, { gap: 12, align: "start" });
const key = row(line, "Key", { width: LABEL_W, height: 20 });
text(key, label, { size: 12, color: C.desc });
const valueW = ctx.w - LABEL_W - 12;
const frame = col(line, "Value", { gap: 4 });
if (typeof value === "function")
value(frame, valueW);
else
text(frame, value, { mono: opt.mono !== false, maxW: valueW, color: opt.color });
return line;
}
/** 表格:cols 是 {name, width?},最后一列分剩下的;格子是着色的段、字符串(按列色)或 (frame, width) 函数。 */
function table(parent, w, cols, rows) {
const wrap = col(parent, "Table wrap", { padding: [2, 0, 2, 4] });
const grid = col(wrap, "Table", { radius: 4, stroke: C.borderStrong, clip: true });
const innerW = w - 4 - 2;
const fixed = cols.slice(0, -1).reduce((sum, c) => sum + c.width, 0);
const widths = cols.map((c, i) => (i < cols.length - 1 ? c.width : innerW - fixed));
const drawRow = (cells, header) => {
const line = row(grid, header ? "Header" : "Row", { align: "start", fill: header ? C.side : null });
cells.forEach((cell, i) => {
const last = i === cols.length - 1;
const frame = col(line, cols[i].name, { width: last ? "fill_container" : widths[i], padding: [3, 8], gap: 4 });
const cellW = widths[i] - 16;
if (header)
text(frame, cell, { size: 12, color: C.desc });
else if (typeof cell === "function")
cell(frame, cellW);
else
text(frame, cell, { size: 12.5, mono: cols[i].mono !== false, maxW: cellW, color: cols[i].color });
});
};
drawRow(cols.map((c) => c.name), true);
for (const cells of rows) drawRow(cells, false);
}
/** 「调用 app.manage · 1 个参数」:跨几行写的调用排成一行,后面是实参表。 */
function callSegs(call) {
return [seg("调用", C.control), seg(" ", C.fg)]
.concat(rs(call.callee), [seg(" · " + call.args.length + " 个参数", C.desc)], call.tail ? [seg(call.tail, C.fg)] : []);
}
/** 实参表「参数 | 值」:按位置传的写「第 N 个」;值又是跨几行写的调用时,格子里写它的一行、它的表套在格子里。 */
function argTable(parent, w, args) {
table(parent, w, [{ name: "参数", width: 76, mono: false, color: C.desc }, { name: "值" }], args.map((arg, i) => [
"第 " + (i + 1) + " 个",
typeof arg === "string" ? rs(arg) : (frame, cellW) => callValue(frame, cellW, arg.call, 12.5),
]));
}
function callValue(frame, w, call, size) {
text(frame, callSegs(call), { mono: true, size, maxW: w });
argTable(frame, w, call.args);
}
/** 一层子块:左边一条竖线标出这一层,块排在右边。 */
function level(parent, w) {
const lv = row(parent, "Level", { gap: 8, align: "start" });
const guide = row(lv, "Guide", { width: 12, height: "fill_container", justify: "center" });
I(guide, { type: "rectangle", name: "Line", width: 1, height: "fill_container", fill: solid(C.guide) });
const list = col(lv, "Blocks", { gap: 8 });
return { list, w: w - 20 };
}
// ─── 函数体子块 ──────────────────────────────────────────────────────────────
function variableBlock(list, w, block) {
const ctx = card(list, KIND.variable, w, { name: block.name });
prop(ctx, "名称", [seg(block.name, C.variable, 600)]);
if (block.note)
prop(ctx, "注释", block.note, { mono: false, color: C.comment });
if (block.cfg)
prop(ctx, "编译条件", block.cfg.map(([t, color]) => seg(t, color)), { mono: false });
if (block.call)
prop(ctx, "初始值", (frame, fw) => callValue(frame, fw, block.call, 13));
else
prop(ctx, "初始值", rs(block.value));
}
/** 一行译文的块(语句、返回、如果):编译条件排在译文前面,跨几行写的调用后面跟实参表。 */
function lineBlock(list, w, block) {
const ctx = card(list, KIND[block.kind], w, { fold: block.kids ? "open" : null });
if (block.cfg)
prop(ctx, "编译条件", block.cfg.map(([t, color]) => seg(t, color)), { mono: false });
let segs = block.call ? callSegs(block.call) : rs(block.line);
if (block.kind === "return")
segs = [seg("返回 ", C.control)].concat(segs);
if (block.kind === "if")
segs = [seg(block.head, C.control)].concat(segs);
text(ctx.content, segs, { mono: true, maxW: ctx.w });
if (block.call)
argTable(ctx.content, ctx.w, block.call.args);
}
function drawBlocks(list, w, blocks) {
for (const block of blocks) {
if (block.kind === "variable")
variableBlock(list, w, block);
else
lineBlock(list, w, block);
if (block.kids) {
const lv = level(list, w);
drawBlocks(lv.list, lv.w, block.kids);
}
}
}
// ─── 轨道 ────────────────────────────────────────────────────────────────────
/**
* 轨道上的一步:左边一列是竖线和圆点(第一步上面、最后一步下面不画线),右边是这一步的内容。
* 步与步之间的空隙放在内容的下内边距里,竖线才能连上。
*/
function railStep(list, index, count, spec, w, draw) {
const step = row(list, "Step " + (index + 1), { align: "start", gap: 4 });
const rail = col(step, "Rail", { width: spec.col, height: "fill_container", align: "center" });
I(rail, { type: "rectangle", name: "Up", width: spec.line, height: spec.up, fill: solid(index === 0 ? C.card : spec.color) });
const dot = { type: "ellipse", name: "Dot", width: spec.dot, height: spec.dot, fill: solid(spec.fill) };
if (spec.stroke)
dot.stroke = { thickness: 1, fill: solid(spec.stroke) };
I(rail, dot);
I(rail, { type: "rectangle", name: "Down", width: spec.line, height: "fill_container", fill: solid(index === count - 1 ? C.card : spec.color) });
const body = col(step, "Body", { gap: 2, padding: [0, 0, spec.pad, 0] });
draw(body, w - spec.col - 4);
}
/** 一步里的一行:方法名列(或「注释」标签)加值列。 */
function stepLine(body, drawKey, drawValue) {
const line = row(body, "Line", { align: "start", gap: 8 });
const key = row(line, "Method", { width: METHOD_W, height: 20 });
drawKey(key);
const value = col(line, "Value", { gap: 2 });
drawValue(value);
}
/** 实参本身是一条链:在值列里缩进成一条小轨道,一节一行。 */
function miniChain(parent, w, parts) {
const list = col(parent, "Sub chain");
parts.forEach((src, i) => railStep(list, i, parts.length, MINI, w, (body, bodyW) => code(body, src, bodyW)));
}
function stepValue(value, w, step, open) {
if (step.sub)
return miniChain(value, w, step.sub);
if (step.closure)
return foldBox(value, open, closureSegs(step.closure));
if (step.commands) {
const line = inline(value);
code(line, step.macro);
foldBox(line, false, [seg("命令 " + step.commands.length + " 个", C.desc)]);
return line;
}
return code(value, step.arg, w);
}
// ─── 调用链卡片 ──────────────────────────────────────────────────────────────
function chainHead(ctx) {
prop(ctx, "起点", rs(CHAIN.start));
prop(ctx, "步骤", CHAIN.steps.length + " 个", { mono: false });
}
/** 方案 A:一步一行排在竖轨道上。expand 是展开的那一步(方法名),它的闭包体子块排在这一步下面。 */
function chainCardA(parent, w, expand) {
const ctx = card(parent, KIND.chain, w, { fold: "open" });
chainHead(ctx);
const list = col(ctx.content, "Steps", { padding: [4, 0, 0, 4] });
CHAIN.steps.forEach((step, i) => {
const open = step.method === expand;
railStep(list, i, CHAIN.steps.length, RAIL, ctx.w - 4, (body, bodyW) => {
const valueW = bodyW - METHOD_W - 8;
stepLine(body, (key) => text(key, method(step.method, 600), { mono: true }), (value) => stepValue(value, valueW, step, open));
if (step.note)
stepLine(body, (key) => text(key, "注释", { size: 12, color: C.desc }), (value) => text(value, step.note, { color: C.comment, maxW: valueW }));
if (open) {
const lv = level(body, bodyW);
drawBlocks(lv.list, lv.w, SETUP_BODY);
}
});
});
return ctx;
}
/** 名字列表:每行放得下几个放几个,名字是链接色,逗号是前景色。 */
function nameList(parent, w, names) {
const segs = [];
let lineW = 0;
names.forEach((name, i) => {
const piece = name + (i < names.length - 1 ? "," : "");
const pw = widthOf(piece + " ", 13, true);
if (lineW > 0 && lineW + pw > w) {
segs.push(seg("\n", C.fg));
lineW = 0;
}
else if (lineW > 0)
segs.push(seg(" ", C.fg));
segs.push(seg(name, C.link));
if (i < names.length - 1)
segs.push(seg(",", C.fg));
lineW += pw;
});
text(parent, segs, { mono: true, maxW: w });
}
/** 方案 B:连着的同名调用合成一组,每组一行属性,按源码顺序。 */
function chainCardB(parent, w) {
const ctx = card(parent, KIND.chain, w, { fold: "open" });
chainHead(ctx);
const byMethod = (name) => CHAIN.steps.find((s) => s.method === name);
prop(ctx, "插件", (frame) => {
const line = inline(frame);
text(line, method("plugin"), { mono: true });
text(line, PLUGINS.length + " 个", { color: C.desc });
});
prop(ctx, "注释", PLUGIN_NOTE, { mono: false, color: C.comment });
table(ctx.content, ctx.w, [{ name: "插件", width: 260, color: C.type }, { name: "配置" }],
PLUGINS.map((p) => [p.name, p.config ? rs(p.config) : [seg("—", C.dim)]]));
prop(ctx, "启动时", (frame) => {
const line = inline(frame);
text(line, method("setup"), { mono: true });
foldBox(line, false, closureSegs(byMethod("setup").closure));
});
prop(ctx, "窗口事件", (frame) => {
const line = inline(frame);
text(line, method("on_window_event"), { mono: true });
foldBox(line, false, closureSegs(byMethod("on_window_event").closure));
});
prop(ctx, "托管状态", (frame) => {
const line = inline(frame);
text(line, method("manage"), { mono: true });
code(line, byMethod("manage").arg);
});
prop(ctx, "命令", (frame, fw) => {
const line = inline(frame);
text(line, method("invoke_handler"), { mono: true });
text(line, COMMANDS.length + " 个", { color: C.desc });
nameList(frame, fw, COMMANDS);
});
prop(ctx, "构建并运行", (frame, fw) => {
const parts = [
(line) => code(line, ".build(" + byMethod("build").arg + ")", fw - 22),
(line) => code(line, ".expect(" + byMethod("expect").arg + ")", fw - 22),
(line) => {
text(line, method("run"), { mono: true });
foldBox(line, false, closureSegs(byMethod("run").closure));
},
];
parts.forEach((draw, i) => {
const line = inline(frame, 6);
const arrow = row(line, "Arrow", { width: 16, height: 20 });
if (i > 0)
text(arrow, "→", { color: C.desc });
draw(line);
});
});
return ctx;
}
/** 上下文:前一句 `let app_state` 的卡片只露出最下面一截,像预览往下滚过了它。 */
function contextSlice(list, w) {
const viewport = box(list, "Context / app_state", { layout: "none", width: w, height: SLICE_H, clip: true });
const ctx = card(viewport, KIND.variable, w, { name: APP_STATE.name, x: 0, y: SLICE_H - APP_STATE_H });
prop(ctx, "名称", [seg(APP_STATE.name, C.variable, 600)]);
prop(ctx, "初始值", rs(APP_STATE.value));
}
/** 短链:变量卡片的初始值排成几步,一步一个动词加照抄的闭包参数。 */
function shortChainCard(parent, w) {
const ctx = card(parent, KIND.variable, w, { fold: "open", name: SHORT.name });
prop(ctx, "名称", [seg(SHORT.name, C.variable, 600)]);
prop(ctx, "类型", rs(SHORT.type));
prop(ctx, "初始值", (frame, fw) => {
const list = col(frame, "Steps");
SHORT.steps.forEach((step, i) => railStep(list, i, SHORT.steps.length, SHORT_RAIL, fw, (body) => {
const line = inline(body, 6);
const verb = row(line, "Verb", { width: 34, height: 20 });
text(verb, step.verb, { color: C.fn });
if (step.code)
code(line, step.code);
}));
});
}
// ─── 画板 ────────────────────────────────────────────────────────────────────
function boardRoot(index, name, o) {
const opt = o || {};
return I(null, {
type: "frame",
name,
x: index * (BOARD_W + BOARD_GAP),
y: 0,
width: BOARD_W,
height: opt.height || "fit_content",
layout: "vertical",
gap: 0,
padding: opt.padding,
fill: solid(C.editor),
clipContent: true,
});
}
/** run 函数体里的最后两句:露一角的 app_state,下面是调用链卡片。 */
function inRunBody(root, drawChain) {
const lv = level(root, BOARD_W - BOARD_PAD * 2);
contextSlice(lv.list, lv.w);
drawChain(lv.list, lv.w);
}
const RUN_PAD = [0, BOARD_PAD, BOARD_PAD, BOARD_PAD];
inRunBody(boardRoot(0, "Prototype / 调用链 · 方案A 步骤轨道", { padding: RUN_PAD }), (list, w) => chainCardA(list, w));
inRunBody(boardRoot(1, "Prototype / 调用链 · 方案B 按方法分组", { padding: RUN_PAD }), (list, w) => chainCardB(list, w));
inRunBody(boardRoot(2, "Prototype / 调用链 · 方案A 展开 setup", { padding: RUN_PAD, height: SETUP_BOARD_H }), (list, w) => chainCardA(list, w, "setup"));
shortChainCard(boardRoot(3, "Prototype / 调用链 · 短链 变量初始值", { padding: BOARD_PAD }), BOARD_W - BOARD_PAD * 2);
File diff suppressed because it is too large. Load diff