GlassLab
ui.widgets.GlassBoxGlassBox の光学パラメータ実験台:屈折、ベゼル幅、拡大。
BEHAVIOR CONTRACT
warp_gain、bezel_width、magnification が同じ Metal サンプリングパスにリアルタイムで作用します。
RECORDED INTERACTION
録画ではガラスをドラッグしてパラメータの効果を確認します。
pub fn buildGlassLab(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const a = cx.allocator;
const c = try col(cx, 16);
// ── 画布:网格背景 + 可拖拽玻璃 ──
const bd = try glassBackdrop(cx, 300);
const drag_node = try glassCapsule(cx, 116, .{});
drag_node.meta.ownership.meta.test_id = "story.glasslab.drag";
drag_node.style.cursor = .pointer;
const body = capsuleBody(drag_node);
body.style.padding = .{ .left = 34, .right = 34, .top = 0, .bottom = 0 };
try body.appendChild(a, try glassLabel(cx, "Drag me", 650));
const drag_st = try a.create(GlassDragState);
drag_st.* = .{ .cx = cx, .target = drag_node };
try scope.registerResource(@ptrCast(drag_st), struct {
fn cleanup(ptr: *anyopaque, alloc: std.mem.Allocator) void {
const state: *GlassDragState = @ptrCast(@alignCast(ptr));
alloc.destroy(state);
}
}.cleanup);
drag_node.behavior.events.event_context = @ptrCast(drag_st);
drag_node.behavior.events.on_event = glassDragHandler;
try bd.appendChild(a, drag_node);
try c.appendChild(a, bd);
// ── 参数面板:两列滑杆 ──
const st = try a.create(GlassLabState);
st.cx = cx;
st.glass = capsuleBody(drag_node);
st.section = undefined;
st.light_arrow = undefined;
try scope.registerResource(@ptrCast(st), struct {
fn cleanup(ptr: *anyopaque, alloc: std.mem.Allocator) void {
const state: *GlassLabState = @ptrCast(@alignCast(ptr));
alloc.destroy(state);
}
}.cleanup);
const panel = try ui.box(cx, .{ .direction = .row, .gap = 16 }, .{});
var columns: [2]*ui.Node = undefined;
for (0..2) |i| {
columns[i] = try ui.box(cx, .{ .direction = .column, .gap = 10 }, .{});
try panel.appendChild(a, columns[i]);
}
// ── 镜片侧截面示意(实时跟随 thickness/bezel/distance)──
const section_col = try ui.box(cx, .{ .direction = .column, .gap = 6 }, .{});
try section_col.appendChild(a, try label(cx, "lens cross-section"));
const section_frame = try ui.box(cx, .{
.width = .{ .px = 260 },
.height = .{ .px = 130 },
.position = .relative,
.background = ui.Color.rgba(244, 246, 250, 255),
.border = .{ .width = 1, .color = ui.Color.rgba(210, 214, 224, 255), .radius = 10 },
}, .{});
(try section_frame.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
// 底部背板线(玻璃悬浮参照)
const backdrop_line = try ui.box(cx, .{
.width = .{ .grow = .{} },
.height = .{ .px = 2 },
.position = .absolute,
.background = ui.Color.rgba(150, 158, 172, 255),
}, .{});
(try backdrop_line.style.ensureExtFallible(a)).inset = .{ .top = .{ .px = 114 } };
try section_frame.appendChild(a, backdrop_line);
// 玻璃截面本体(path 填充)
const section = try ui.box(cx, .{
.width = .{ .grow = .{} },
.height = .{ .grow = .{} },
.position = .absolute,
.background = ui.Color.rgba(120, 168, 255, 150),
}, .{});
try section_frame.appendChild(a, section);
// 光向指示箭头(右上角,绕中心旋转)
const arrow = try ui.box(cx, .{
.width = .{ .px = 30 },
.height = .{ .px = 3 },
.position = .absolute,
.background = ui.Color.rgba(240, 170, 40, 255),
}, .{});
const arrow_ext0 = try arrow.style.ensureExtFallible(a);
arrow_ext0.inset = .{ .top = .{ .px = 16 }, .left = .{ .px = 216 } };
arrow_ext0.corner_radius = ui.CornerRadius.uniform(1.5);
try section_frame.appendChild(a, arrow);
try section_col.appendChild(a, section_frame);
try panel.appendChild(a, section_col);
inline for (LAB_SPECS, 0..) |spec, i| {
const prow = try ui.box(cx, .{ .direction = .column, .gap = 2 }, .{});
try prow.appendChild(a, try ui.text(cx, spec.name, .{ .font_size = 12, .color = light.color.fg_secondary }));
const sl = try W.Slider(.{
.min = spec.min,
.max = spec.max,
.step = spec.step,
.initial_value = spec.default,
.width = 200,
.show_value = true,
.on_change = .{ .callback = labApply, .context = @ptrCast(st) },
}).mount(scope, cx);
st.sliders[i] = sl.state;
try prow.appendChild(a, sl.wrapper);
try columns[i % 2].appendChild(a, prow);
}
st.section = section;
st.light_arrow = arrow;
try c.appendChild(a, panel);
labApply(@ptrCast(st));
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 してください。