GlassChrome
ui.widgets.GlassBoxアプリの chrome:スクロールするコンテンツの上にガラスのタイトルバーとツールバー。
BEHAVIOR CONTRACT
ぼかし半径、レイヤー、スクロールした内容が同じフレームで合成されます。
RECORDED INTERACTION
harness が Scroll +90 をクリックし、コンテンツがガラスの下をスクロールします。
pub fn buildGlassChrome(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const a = cx.allocator;
const c = try col(cx, 16);
const STAGE_W: f32 = 620;
// ── 1. header 浮在滚动画布之上(画板详情页主场景)──
try c.appendChild(a, try label(cx, "App header — pure-blur glass pills floating over a scrolling canvas"));
const stage = try ui.box(cx, .{
.width = .{ .px = STAGE_W },
.height = .{ .px = 200 },
.position = .relative,
.overflow_hidden = true,
}, .{});
(try stage.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
const sa = try W.mountScrollArea(.{ .width = STAGE_W, .height = 200, .direction = .vertical }, scope, cx);
// 三屏画布内容,滚动时玻璃下的背景持续变化
for (0..3) |_| try sa.content.appendChild(a, try chromeCanvas(cx, STAGE_W, 260));
sa.content.meta.ownership.meta.test_id = "story.glasschrome.canvas";
try stage.appendChild(a, sa.container);
// 真实应用的分层:76px 渐进 blur strip 贴顶(header 行本身**无**玻璃),
// 玻璃胶囊浮在 strip 之上。所以这里必须先挂 strip 再挂 header 行。
const edge_strip = try ui.box(cx, .{
.position = .absolute,
.width = .{ .px = STAGE_W },
.height = .{ .px = CHROME_EDGE_H },
}, .{});
edge_strip.meta.ownership.meta.test_id = "story.glasschrome.edge";
const sext = try edge_strip.style.ensureExtFallible(a);
sext.inset = .{ .left = .{ .px = 0 }, .top = .{ .px = 0 } };
sext.glass = progressiveEdgeGlass();
try stage.appendChild(a, edge_strip);
// header wrapper:绝对定位 + translate_y = ISLAND_M(同款)。
// 行本身透明——只有里面的胶囊有玻璃。
const hdr_wrap = try ui.box(cx, .{
.position = .absolute,
.width = .{ .px = STAGE_W },
.direction = .row,
}, .{});
(try hdr_wrap.style.ensureExtFallible(a)).inset = .{ .left = .{ .px = 0 }, .top = .{ .px = 0 } };
hdr_wrap.setStyle(null, .translate_y, CHROME_ISLAND_M);
try hdr_wrap.appendChild(a, try chromeHeaderRow(cx, scope, STAGE_W));
try stage.appendChild(a, hdr_wrap);
try c.appendChild(a, stage);
const st = try cx.bindState(GlassChromeStory, .{ .scroll = sa.state, .content = sa.content });
const controls = try ui.box(cx, .{ .direction = .row, .gap = 10 }, .{});
const down = try W.Button(.{ .label = "Scroll +90", .on_click = cx.on(GlassChromeStory, st, GlassChromeStory.scrollDown) }).mount(scope, cx);
down.meta.ownership.meta.test_id = "story.glasschrome.scrolldown";
try controls.appendChild(a, down);
const up = try W.Button(.{ .label = "Scroll −90", .on_click = cx.on(GlassChromeStory, st, GlassChromeStory.scrollUp) }).mount(scope, cx);
up.meta.ownership.meta.test_id = "story.glasschrome.scrollup";
try controls.appendChild(a, up);
try c.appendChild(a, controls);
// ── 2. 渐进式毛玻璃 scroll edge(progressiveBlurEdge)──
// 顶部 30% 满 blur → 100% 处收敛到 0。验收点:不该出现下缘切线、
// 白纱、或接缝——这些是这一档参数特有的失败模式。
try c.appendChild(a, try label(cx, "Progressive blur scroll edge — full blur at top, fading to none (no seam, no bottom hairline)"));
const edge_stage = try ui.box(cx, .{
.width = .{ .px = STAGE_W },
.height = .{ .px = 150 },
.position = .relative,
.overflow_hidden = true,
}, .{});
(try edge_stage.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
const esa = try W.mountScrollArea(.{ .width = STAGE_W, .height = 150, .direction = .vertical }, scope, cx);
for (0..3) |_| try esa.content.appendChild(a, try chromeCanvas(cx, STAGE_W, 260));
try edge_stage.appendChild(a, esa.container);
const edge = try ui.box(cx, .{
.position = .absolute,
.width = .{ .px = STAGE_W },
.height = .{ .px = CHROME_EDGE_H },
}, .{});
edge.meta.ownership.meta.test_id = "story.glasschrome.edgebare";
const eext = try edge.style.ensureExtFallible(a);
eext.inset = .{ .left = .{ .px = 0 }, .top = .{ .px = 0 } };
eext.glass = progressiveEdgeGlass();
try edge_stage.appendChild(a, edge);
try c.appendChild(a, edge_stage);
// ── 3. 强度对照阶梯:同一底、只变 backdrop_blur ──
// 用于判断 20 这档在浅底上到底够不够"磨砂"——单看一个说不清。
try c.appendChild(a, try label(cx, "backdrop_blur ladder (transparent fill) — 0 / 8 / 20 (app) / 40"));
const ladder_stage = try ui.box(cx, .{
.width = .{ .px = STAGE_W },
.height = .{ .px = 150 },
.position = .relative,
.overflow_hidden = true,
}, .{});
(try ladder_stage.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
try ladder_stage.appendChild(a, try chromeCanvas(cx, STAGE_W, 150));
const ladder_row = try ui.box(cx, .{
.position = .absolute,
.width = .{ .px = STAGE_W },
.height = .{ .px = 150 },
.direction = .row,
.align_items = .center,
.justify = .space_evenly,
}, .{});
(try ladder_row.style.ensureExtFallible(a)).inset = .{ .left = .{ .px = 0 }, .top = .{ .px = 0 } };
inline for (.{ 0, 8, 20, 40 }, 0..) |blur, i| {
const pill = try chromePill(cx, 118, CHROME_PALETTE_H, 0);
// 诊断:背景全透明,排除 0xE0 白底遮住 blur 的可能
pill.setStyle(null, .background, ui.Color.rgba(0, 0, 0, 0));
(try pill.style.ensureExtFallible(a)).glass = .{ .backdrop_blur = blur, .glass_intensity = 0 };
var buf: [16]u8 = undefined;
const txt = try std.fmt.bufPrint(&buf, "blur {d}", .{blur});
try pill.appendChild(a, try ui.text(cx, txt, .{
.font_size = 12,
.font_weight = 600,
.color = chrome_col.ink2,
}));
_ = i;
try ladder_row.appendChild(a, pill);
}
try ladder_stage.appendChild(a, ladder_row);
try c.appendChild(a, ladder_stage);
// ── 4. 填充不透明度阶梯:blur 固定 20,只变白底 alpha ──
// 关键对照:应用里用的 0xE0(88% 白)只剩 12% 透光,blur 再大也看不见
// ——玻璃钱花了但没有视觉收益。0x99/0xB0 才真读得出磨砂。
try c.appendChild(a, try label(cx, "fill alpha ladder at fixed blur 20 — 0x66 / 0x99 / 0xB0 / 0xE0 (app): blur is invisible past ~0xB0"));
const alpha_stage = try ui.box(cx, .{
.width = .{ .px = STAGE_W },
.height = .{ .px = 150 },
.position = .relative,
.overflow_hidden = true,
}, .{});
(try alpha_stage.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
try alpha_stage.appendChild(a, try chromeCanvas(cx, STAGE_W, 150));
const alpha_row = try ui.box(cx, .{
.position = .absolute,
.width = .{ .px = STAGE_W },
.height = .{ .px = 150 },
.direction = .row,
.align_items = .center,
.justify = .space_evenly,
}, .{});
(try alpha_row.style.ensureExtFallible(a)).inset = .{ .left = .{ .px = 0 }, .top = .{ .px = 0 } };
inline for (.{ 0x66, 0x99, 0xB0, 0xE0 }) |alpha| {
const pill = try chromePill(cx, 118, CHROME_PALETTE_H, 0);
pill.setStyle(null, .background, ui.Color.rgba(0xFF, 0xFF, 0xFF, alpha));
var buf: [16]u8 = undefined;
const txt = try std.fmt.bufPrint(&buf, "0x{X:0>2}", .{alpha});
try pill.appendChild(a, try ui.text(cx, txt, .{
.font_size = 12,
.font_weight = 600,
.color = chrome_col.ink2,
}));
try alpha_row.appendChild(a, pill);
}
try alpha_stage.appendChild(a, alpha_row);
try c.appendChild(a, alpha_stage);
return c;
}Storybook にあるこの story のコードそのままです(zenit 5f9add5+wip 2026-09-30)。W は ui.widgets のことで、col / row / label は Storybook 内の小さなレイアウトヘルパーです。
GlassBoxProps
ui.widgets.GlassBoxfn GlassBox(props: GlassBoxProps) GlassBoxBuilderフィールド
型
デフォルト値
説明
title?[]const u8null—
subtitle?[]const u8null—
width?f32null—
height?f32null—
padding?Paddingnull—
body_gapf3212—
radiusf3218—
readabilityGlassBoxReadability.balanced.preferred.maximum.preferred—
emphasisGlassBoxEmphasis.subtle.regular.prominent.regular—
variantGlassBoxVariant.regular.clear.regular—
tint?Colornull—
interactiveboolfalseApple Liquid Glass interactive:hover 提亮高光、press 玻璃增厚 + 微放大。
scroll_edge?ScrollEdgeBindingnullscroll-edge effect:绑定 ScrollArea 后,内容滚过玻璃边缘时出现渐进 模糊/压暗带(随滚动位置实时驱动 + 插值平滑)。
*Node
ui.widgets.GlassBoxmount は *Node を直接返します。親ノードに append してください。