GlassBox
ui.widgets.GlassBox실제 backdrop blur를 사용하는 유리 표면.
BEHAVIOR CONTRACT
가독성, 강조, hover / press, 배경 샘플링이 하나의 Metal 합성 경로를 거칩니다.
RECORDED INTERACTION
harness가 화려한 배경 위에서 interactive 변형을 누릅니다.
pub fn buildGlassBox(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const a = cx.allocator;
const icons = ui.assets.common;
const c = try col(cx, 20);
// ── 1. Toolbar:分组玻璃 capsule(ToolbarSpacer 语义 = 两个独立 slab)──
try c.appendChild(a, try label(cx, "Toolbar — grouped glass capsules (ToolbarSpacer splits slabs)"));
{
const bd = try glassBackdrop(cx, 130);
const bar_row = try ui.box(cx, .{ .direction = .row, .gap = 12, .align_items = .center }, .{});
const group1 = try glassCapsule(cx, 44, regularGlass());
inline for (.{ icons.chevron_left, icons.chevron_right }) |asset| {
try capsuleBody(group1).appendChild(a, try glassIconSlot(cx, asset));
}
const group2 = try glassCapsule(cx, 44, regularGlass());
inline for (.{ icons.plus, icons.folder, icons.star }) |asset| {
try capsuleBody(group2).appendChild(a, try glassIconSlot(cx, asset));
}
try bar_row.appendChild(a, group1);
try bar_row.appendChild(a, group2);
try bd.appendChild(a, bar_row);
try c.appendChild(a, bd);
}
// ── 2. Tab bar:浮动 capsule + 独立 search 圆钮(Tab(role: .search))──
try c.appendChild(a, try label(cx, "Tab bar — floating capsule + detached search circle"));
{
const bd = try glassBackdrop(cx, 130);
const bar_row = try ui.box(cx, .{ .direction = .row, .gap = 10, .align_items = .center }, .{});
const tabbar = try glassCapsule(cx, 48, regularGlass());
inline for (.{ "Today", "Library", "Radio" }, .{ icons.calendar, icons.folder_open, icons.scan }) |txt, asset| {
const item = try ui.box(cx, .{ .direction = .row, .gap = 6, .align_items = .center, .padding = .{ .left = 10, .right = 10, .top = 0, .bottom = 0 } }, .{});
var icon_style = ui.Style{};
icon_style.width = .{ .px = 15 };
icon_style.height = .{ .px = 15 };
try item.appendChild(a, try ui.iconTint(cx, asset, ui.Color.rgba(255, 255, 255, 235), icon_style));
try item.appendChild(a, try glassLabel(cx, txt, 550));
try capsuleBody(tabbar).appendChild(a, item);
}
const search = try glassCapsule(cx, 48, regularGlass());
try capsuleBody(search).appendChild(a, try glassIconSlot(cx, icons.search));
try bar_row.appendChild(a, tabbar);
try bar_row.appendChild(a, search);
try bd.appendChild(a, bar_row);
try c.appendChild(a, bd);
}
// ── 3. Buttons:.glass 与 .glassProminent(prominent = tinted glass)──
try c.appendChild(a, try label(cx, "Buttons — .glass and .glassProminent (tinted)"));
{
const bd = try glassBackdrop(cx, 120);
const btn_row = try ui.box(cx, .{ .direction = .row, .gap = 14, .align_items = .center }, .{});
const plain = try glassCapsule(cx, 40, regularGlass());
try capsuleBody(plain).appendChild(a, try glassLabel(cx, "Continue", 600));
var prominent_params = regularGlass();
prominent_params.glass_tint = ui.Color.rgba(0, 122, 255, 150);
const prominent = try glassCapsule(cx, 40, prominent_params);
capsuleBody(prominent).setBackgroundRaw(ui.Color.rgba(0, 122, 255, 90));
try capsuleBody(prominent).appendChild(a, try glassLabel(cx, "Get", 650));
var orange_params = regularGlass();
orange_params.glass_tint = ui.Color.rgba(255, 149, 0, 150);
const orange = try glassCapsule(cx, 40, orange_params);
capsuleBody(orange).setBackgroundRaw(ui.Color.rgba(255, 149, 0, 90));
try capsuleBody(orange).appendChild(a, try glassLabel(cx, "Tinted", 650));
try btn_row.appendChild(a, plain);
try btn_row.appendChild(a, prominent);
try btn_row.appendChild(a, orange);
try bd.appendChild(a, btn_row);
try c.appendChild(a, bd);
}
// ── 4. GlassBox 面板:regular vs clear 变体 ──
try c.appendChild(a, try label(cx, "GlassBox panel — .regular vs .clear (clear over 35% dimmed media)"));
{
const bd = try glassBackdrop(cx, 220);
const pair = try ui.box(cx, .{ .direction = .row, .gap = 20, .align_items = .center }, .{});
const regular_res = try W.GlassBox(.{ .title = "Regular", .subtitle = "adaptive, legible", .width = 240 }).mountBody(scope, cx);
try regular_res.body.appendChild(a, try glassLabel(cx, "Functional layer content.", 450));
try pair.appendChild(a, regular_res.box);
// interactive:hover 提亮高光,press 玻璃增厚 + 微放大
const inter_res = try W.GlassBox(.{ .title = "Interactive", .subtitle = "hover / press me", .width = 240, .interactive = true }).mountBody(scope, cx);
try inter_res.body.appendChild(a, try glassLabel(cx, "Glass responds to touch.", 450));
inter_res.box.meta.ownership.meta.test_id = "story.glassbox.interactive";
try pair.appendChild(a, inter_res.box);
// clear 变体:HIG 要求亮媒体上先叠 35% 黑色 dimming 层
const dimmed = try ui.box(cx, .{
.direction = .column,
.padding = ui.Padding.all(10),
.background = ui.Color.rgba(0, 0, 0, 89), // 35% dimming
}, .{});
(try dimmed.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(22);
const clear_res = try W.GlassBox(.{ .title = "Clear", .subtitle = "media-rich backdrop", .width = 240, .variant = .clear }).mountBody(scope, cx);
try clear_res.body.appendChild(a, try glassLabel(cx, "Backdrop shows through.", 450));
try dimmed.appendChild(a, clear_res.box);
try pair.appendChild(a, dimmed);
try bd.appendChild(a, pair);
try c.appendChild(a, bd);
}
// ── 5. Interactive motion:hover 抬升 / press 下沉 / 弹性回弹 ──
try c.appendChild(a, try label(cx, "Interactive motion — hover lift, press sink, elastic release (geometry + material)"));
{
const bd = try glassBackdrop(cx, 240);
const tile_row = try ui.box(cx, .{ .direction = .row, .gap = 20, .align_items = .center }, .{});
try tile_row.appendChild(a, try glassMotionTile(scope, cx, "Lift", "ease-out rise", null, "story.glassbox.motion.lift"));
try tile_row.appendChild(a, try glassMotionTile(scope, cx, "Tinted", "prominent blue", ui.Color.rgba(0, 122, 255, 150), "story.glassbox.motion.tinted"));
try tile_row.appendChild(a, try glassMotionTile(scope, cx, "Elastic", "springy release", ui.Color.rgba(255, 149, 0, 150), "story.glassbox.motion.elastic"));
try bd.appendChild(a, tile_row);
try c.appendChild(a, bd);
}
// ── 6. Draggable glass:拖动玻璃观察实时折射 ──
try c.appendChild(a, try label(cx, "Drag the glass — live refraction over the canvas"));
{
const bd = try glassBackdrop(cx, 260);
const drag_node = try glassCapsule(cx, 56, regularGlass());
drag_node.meta.ownership.meta.test_id = "story.glassbox.drag";
drag_node.style.cursor = .pointer;
const body = capsuleBody(drag_node);
var icon_style = ui.Style{};
icon_style.width = .{ .px = 18 };
icon_style.height = .{ .px = 18 };
try body.appendChild(a, try ui.iconTint(cx, ui.assets.common.mark, ui.Color.rgba(255, 255, 255, 235), icon_style));
try body.appendChild(a, try glassLabel(cx, "Drag me", 650));
const st = try a.create(GlassDragState);
st.* = .{ .cx = cx, .target = drag_node };
try scope.registerResource(@ptrCast(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(st);
drag_node.behavior.events.on_event = glassDragHandler;
try bd.appendChild(a, drag_node);
try c.appendChild(a, bd);
}
// ── 7. Floating nav toolbar:选中项盖 liquid glass lens,可拖拽 + 悬停放大 ──
try c.appendChild(a, try label(cx, "Floating nav — draggable magnifying lens over the active item"));
{
const page = try ui.box(cx, .{
.width = .{ .grow = .{} },
.height = .{ .px = 200 },
.align_items = .center,
.justify = .center,
.background = ui.Color.rgba(236, 231, 222, 255),
}, .{});
(try page.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(24);
// 白色浮动 nav bar
const bar_wrapper = try ui.box(cx, .{ .direction = .row, .position = .relative }, .{});
const bar = try ui.box(cx, .{
.direction = .row,
.align_items = .center,
.padding = .{ .left = 28, .right = 28, .top = 18, .bottom = 18 },
.gap = 40,
.background = ui.Color.rgba(255, 255, 255, 255),
}, .{});
const bar_ext = try bar.style.ensureExtFallible(a);
bar_ext.corner_radius = ui.CornerRadius.uniform(44);
bar_ext.setShadow(.{ .color = ui.Color.rgba(40, 36, 30, 30), .blur = 24, .offset_y = 10 });
try bar_wrapper.appendChild(a, bar);
// 灰色高亮底座:bar 第一个子节点(absolute),天然垫在 items 下面
const pill = try ui.box(cx, .{
.width = .{ .px = NAV_LENS_REST_W },
.height = .{ .px = NAV_LENS_REST_H },
.position = .absolute,
}, .{});
const pill_ext = try pill.style.ensureExtFallible(a);
pill_ext.corner_radius = ui.CornerRadius.uniform(NAV_LENS_REST_H / 2);
pill_ext.inset = .{ .top = .{ .px = 3 } };
pill.setBackgroundRaw(ui.Color.rgba(228, 228, 231, 255));
try bar.appendChild(a, pill);
const icons2 = ui.assets.common;
var item_nodes: [NAV_ITEM_COUNT]*ui.Node = undefined;
inline for (.{ "Dashboard", "Explore", "Futures", "Rewards", "Exchange" }, .{ icons2.mark, icons2.search, icons2.file_text, icons2.star, icons2.link }, 0..) |txt, asset, i| {
const item = try ui.box(cx, .{ .direction = .column, .align_items = .center, .gap = 6 }, .{});
var icon_style = ui.Style{};
icon_style.width = .{ .px = 22 };
icon_style.height = .{ .px = 22 };
try item.appendChild(a, try ui.iconTint(cx, asset, ui.Color.rgba(116, 122, 134, 255), icon_style));
try item.appendChild(a, try ui.text(cx, txt, .{ .font_size = 15, .font_weight = 600, .color = ui.Color.rgba(28, 30, 36, 255) }));
try bar.appendChild(a, item);
item_nodes[i] = item;
}
// lens:绝对定位盖在 bar 上,上下出血
const lens = try ui.box(cx, .{
.width = .{ .px = NAV_LENS_REST_W },
.height = .{ .px = NAV_LENS_REST_H },
.position = .absolute,
}, .{});
const lens_ext = try lens.style.ensureExtFallible(a);
lens_ext.inset = .{ .top = .{ .px = -4 } };
lens_ext.corner_radius = ui.CornerRadius.uniform(NAV_LENS_REST_H / 2);
// 静止态:lens 不画任何东西(命中区),玻璃只在按下时出现
lens.style.cursor = .pointer;
lens.meta.ownership.meta.test_id = "story.glassbox.navlens";
try bar_wrapper.appendChild(a, lens);
const st = try a.create(NavLensState);
st.* = .{ .cx = cx, .lens = lens, .pill = pill, .bar = bar, .container = bar_wrapper, .items = item_nodes };
try scope.registerResource(@ptrCast(st), struct {
fn cleanup(ptr: *anyopaque, alloc: std.mem.Allocator) void {
const state: *NavLensState = @ptrCast(@alignCast(ptr));
alloc.destroy(state);
}
}.cleanup);
lens.behavior.events.event_context = @ptrCast(st);
lens.behavior.events.on_event = navLensHandler;
// snap 挂在 in-flow 容器上(absolute 节点的 before_render 不保证执行)
bar_wrapper.behavior.events.event_context = @ptrCast(st);
bar_wrapper.addBeforeRender(navLensBeforeRender);
try page.appendChild(a, bar_wrapper);
try c.appendChild(a, page);
}
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하면 됩니다.