UI 트리와 레이아웃
노드 생성자와 Flex 스타일 레이아웃으로 zenit의 공간 모델을 구성합니다.
멘탈 모델
마운트 함수는 실제로 유지되는 *ui.Node 트리를 한 번만 만듭니다. 컨테이너는 자식의 배치 방식을 정하고, 리프는 텍스트, 이미지, 아이콘을 담습니다. 이후 매 프레임마다 프레임워크는 dirty 노드에서만 파이프라인에 다시 진입합니다. 레이아웃, display item으로 페인트, Metal로 전달의 순서입니다.
생성자
ui.box방향, 크기, 간격, 배경을 완전히 제어하는 범용 컨테이너. 기본값은 column, 너비와 높이는 fitui.hstack / ui.vstackdirection이 row / column으로 고정된 boxui.text / ui.textFmt정적 텍스트 / Signal을 구독하는 포맷 텍스트: textFmt(cx, scope, fmt, .{signals}, props)ui.icon / ui.iconTint / ui.svg / ui.image벡터 아이콘, 색조 아이콘, SVG, 비트맵 텍스처ui.grid열 / 행 트랙에 따라 배치되는 그리드 컨테이너ui.spacer두 축 모두 grow인 유연한 빈 공간ui.clickable어떤 노드에든 on_click 핸들러를 붙이고 그 노드를 반환ui.boxStyled / hstackStyled / vstackStyled / textStyled이름 있는 스타일 함수를 받아 테마 변경 시 다시 적용. 스타일과 테마 참고레이아웃 예제
먼저 컨테이너를 만들고 appendChild로 자식을 붙입니다. 스타일 값은 우선 cx.tokens에서 가져오므로, 테마 전환이나 간격 조정 때 리터럴을 일일이 찾아다닐 필요가 없습니다.
const card = try ui.vstack(cx, .{
.width = .fixed(360),
.gap = cx.tokens.space._4,
.padding = ui.Padding.all(cx.tokens.space._6),
.background = cx.tokens.color.bg_secondary,
.corner_radius = cx.tokens.radius.xl,
.align_items = .stretch,
}, .{});
const header = try ui.hstack(cx, .{
.gap = cx.tokens.space._2,
.align_items = .center,
}, .{});
try header.appendChild(cx.allocator, try ui.iconTint(
cx,
ui.icons.star,
cx.tokens.color.accent,
.{ .width = .fixed(18), .height = .fixed(18) },
));
try header.appendChild(cx.allocator, try ui.text(cx, "Overview", .{}));
try header.appendChild(cx.allocator, try ui.spacer(cx));
try card.appendChild(cx.allocator, header);자식은 생성 시 tuple로 전달할 수도 있습니다. ui.Padding.symmetric(v, h)는 세로와 가로 패딩을 따로 설정합니다.
// Children can also be passed as a tuple at construction time.
const row = try ui.hstack(cx, .{
.width = .fill(),
.padding = ui.Padding.symmetric(cx.tokens.space._2, cx.tokens.space._4),
.gap = cx.tokens.space._3,
}, .{
try ui.text(cx, "Name", .{}),
try ui.spacer(cx),
try ui.text(cx, "42", .{ .color = cx.tokens.color.fg_secondary }),
});
// Any node can take a click handler.
// (model: *Model from cx.bindState; Model.select is fn (*Model) void)
_ = ui.clickable(row, cx.on(Model, model, Model.select));크기와 좌표
너비와 높이는 ui.Sizing 유니언이며, 네 멤버마다 축약형이 있습니다.
.fixed(120).{ .px = 120 }고정 픽셀.fill().{ .grow = .{} }남은 공간을 차지. grow는 min / max 페이로드를 가짐(예: .{ .grow = .{ .min = 200 } }).pct(50).{ .percent = 50 }부모 content box(padding 제외)의 백분율(0–100).{ .fit = .{} }내용에 따라 결정. box의 기본값이며 min / max도 가능레이아웃 결과는 노드 필드로 저장되지 않습니다. node.rectFromWorldOrFallback()는 마지막 레이아웃 패스에서 얻은, 부모 왼쪽 위 기준의 사각형을 반환합니다. 창 좌표가 필요하면 node.globalRect()를 사용하세요. 조상 체인을 따라 위치, translate, sticky 오프셋을 누적합니다.
// Parent-relative rect from the last layout pass.
const local = node.rectFromWorldOrFallback();
// Window coordinates: accumulates ancestors, translate and sticky offsets.
const screen = node.globalRect();
std.log.info("{d}x{d} at ({d}, {d})", .{ screen.w, screen.h, screen.x, screen.y });
_ = local;노드 업데이트
노드를 직접 바꿀 때는 파이프라인의 어느 부분을 다시 실행할지 프레임워크가 알아야 합니다. setStyle을 우선 사용하세요. 컴파일 시점에 필드로부터 dirty 레벨을 고르므로 수동 markLayoutDirty / markRenderDirty가 필요 없습니다.
.sizingwidth, height자신과 부모 재레이아웃.layoutpadding, margin, gap, direction, justify, align_items, min/max_*이 노드 재레이아웃.interactionopacity, translate_*, corner_radius, border, z_index, cursor히트 인덱스 갱신 후 다시 그리기.renderbackground, shadow, gradient, outline, text_color페인트만 재생성.nonetab_index, layout_isolation프레임 작업 없음// setStyle picks the dirty level from the field at compile time.
node.setStyle(cx.allocator, .width, .fixed(240)); // sizing
node.setStyle(cx.allocator, .gap, 12); // layout
node.setStyle(cx.allocator, .background, next); // render
// Low-frequency fields live in StyleExt and need a real allocator.
node.setStyle(cx.allocator, .z_index, 10);
// Replace text: dupes the content, frees the previous owned copy,
// and re-measures only if the content actually changed.
try node.setTextContent(cx.allocator, "Updated");
// Hide without unmounting: display:none leaves layout, paint,
// hit-testing, Tab order and the a11y tree; state stays alive.
panel.setDisplay(.none);
panel.setDisplay(.flex); // back, same nodes
// Recolor an icon node whether it is icon-table or image (svgTint) backed.
_ = icon.setTint(cx.tokens.color.accent); // false: node has neithersetText는 이전과 새 텍스트 시그니처를 비교합니다. 측정이 바뀌면 sizing-dirty, 외형만 바뀌면 render-dirty, 동일하면 아무것도 하지 않습니다. 따라서 텍스트를 업데이트한 뒤 markRenderDirty를 호출할 필요가 없습니다.
서브트리를 잠시 숨기려면 node.setDisplay(.none)를 호출하세요(또는 BoxStyle에 .display = .none 설정). 노드와 서브트리는 공간을 차지하지 않고 gap에도 포함되지 않으며 그려지지 않고, 히트 테스트, Tab 순회, 접근성 트리에서도 빠집니다. 노드와 상태는 살아 있으며 .flex로 되돌릴 수 있어 언마운트 후 재구성할 필요가 없습니다. setDisplay는 노드와 부모의 레이아웃을 스스로 dirty로 표시합니다. opacity를 0으로만 해도 히트 테스트는 멈추지만, 노드는 여전히 공간을 차지하고 Tab 순서에 남습니다.