상태와 이벤트
프레임을 넘어 유지되어야 하는 상태는 Cx가 소유하게 하고, 사용자 입력에는 타입 안전한 handler로 응답합니다.
바인딩된 상태
cx.bindState(T, init)는 Cx 상태 저장소에 T를 만들고 프레임이 바뀌어도 유효한 *T를 반환합니다. 이어서 cx.on이 그 메서드를 어떤 컴포넌트든 받을 수 있는 HandlerRef로 바꿉니다. 상태 ID를 직접 할당할 필요가 없습니다.
const Counter = struct {
allocator: std.mem.Allocator,
value: u32 = 0,
label: ?*ui.Node = null,
buffer: [32]u8 = undefined,
fn increment(self: *Counter) void {
self.value += 1;
const label = self.label orelse return;
const text = std.fmt.bufPrint(
&self.buffer,
"Clicked {d} times",
.{self.value},
) catch return;
// Copies the text, marks it owned, frees the previous owned copy,
// and marks the node dirty only if the text actually changed.
label.setTextContent(self.allocator, text) catch return;
}
};
const counter = try cx.bindState(Counter, .{ .allocator = cx.allocator });
const on_click = cx.on(Counter, counter, Counter.increment);콜백에서 텍스트를 바꿀 때는 node.setTextContent(allocator, text)를 사용합니다. 내용을 복사해 owned로 표시하고, 이전의 owned 내용을 해제하며, 텍스트가 실제로 바뀌었을 때만 노드를 dirty로 표시합니다. 수동 markRenderDirty는 필요 없습니다. 그래서 struct에 allocator 필드를 둡니다.
컴포넌트에 연결
컴포넌트의 콜백 필드는 모두 ?HandlerRef입니다. 앞에서 얻은 on_click을 Button에 넘기고, 갱신할 텍스트 노드를 상태 struct에 저장합니다.
const label = try ui.text(cx, "Clicked 0 times", .{
.color = cx.tokens.color.fg_primary,
});
counter.label = label;
const button = try ui.widgets.Button(.{
.label = "Increment",
.variant = .primary,
.on_click = on_click,
}).mount(scope, cx);
try root.appendChild(cx.allocator, button);
try root.appendChild(cx.allocator, label);값을 전달받는 콜백(Checkbox나 Switch의 선택 상태, Input이나 Tabs의 텍스트 또는 id)에는 ui.Cx.boolHandlerFrom 같은 값 전달 생성자를 사용합니다.
const Prefs = struct {
notify: bool = false,
fn setNotify(self: *Prefs, checked: bool) void {
self.notify = checked;
}
};
const prefs = try cx.bindState(Prefs, .{});
const notify = try ui.widgets.Checkbox(.{
.label_text = "Notify me",
.on_change = ui.Cx.boolHandlerFrom(Prefs, prefs, Prefs.setNotify),
}).mount(scope, cx);클릭 한 번의 경로
플랫폼이 보내는 것은 누름과 뗌이지 ‘클릭’이 아닙니다. 히트 테스트는 Z 순서로 앞에서부터 자식을 훑어 최상위 노드를 찾습니다. 누름과 뗌이 같은 요소에서 일어나면 EventDispatcher가 click을 합성해 capture → target → bubble 세 단계로 전달하고, 노드의 on_click HandlerRef를 호출해 최종적으로 바인딩한 메서드에 도달합니다.
.ignored처리하지 않음. 전파를 계속하고 기본 동작을 허용.handled처리함. 전파는 계속하되 기본 동작을 막음.stop처리함. 전파를 중단Handler 만들기
cx.on(State, s, State.m)fn (*State) void버튼 클릭 같은 ‘일어났다’ 이벤트ui.Cx.boolHandlerFrom(…)fn (*State, bool) voidCheckbox, Switch, Accordionui.Cx.strHandlerFrom(…)fn (*State, []const u8) voidInput, Textarea, Tabs, Radio. 슬라이스는 호출 중에만 유효ui.clickable(node, handler)—아무 노드에나 on_click 연결저수준 이벤트
버튼, 입력 필드 같은 컴포넌트는 흔한 상호작용을 이미 감싸고 있습니다. ui.events.Event는 커스텀 상호작용을 구현할 때만 필요합니다. 마우스(누름, 뗌, click, double_click, 이동, 진입/이탈, 스크롤, 확대/축소, 파일 드래그), 키보드, text_input, ime_preedit / ime_commit, 포커스를 다룹니다. 키보드 명령은 ui.actions, 포커스 이동은 ui.focus를 우선 사용하십시오.
ui.widgets.* callbacksui.hooks.useHover / useFocusRingui.actionsui.interaction.drag / range_hoverui.events.Event수명 주기와 정리
바인딩된 상태는 Cx가 소유하며 Cx가 해제될 때 함께 해제됩니다. T가 pub deinit(*T)를 선언하면 프레임워크가 해제 직전에 자동으로 호출합니다. ArrayList, HashMap, 할당한 버퍼의 정리는 여기에 두면 됩니다. Signal, Memo, Effect, onCleanup 콜백은 Scope에 속하며 scope.dispose()와 함께 해제됩니다.
const EditorState = struct {
allocator: std.mem.Allocator,
lines: std.ArrayList([]u8) = .{},
// Must be pub: the state store detects it and calls it when the Cx deinits.
pub fn deinit(self: *EditorState) void {
for (self.lines.items) |line| self.allocator.free(line);
self.lines.deinit(self.allocator);
}
};
// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(EditorState, .{ .allocator = cx.allocator });
// Do NOT also call scope.onCleanup(EditorState, editor, EditorState.deinit):
// the framework already calls deinit, so that would free everything twice.scope.onCleanup은 두 경우에만 사용합니다. 정리 함수가 pub deinit이 아닐 때, 또는 리소스가 Cx 전체가 아니라 특정 Scope(페이지)를 따라 해제되어야 할 때입니다.
// A resource that must follow the page (Scope), not the whole Cx.
const PageCache = struct {
allocator: std.mem.Allocator,
hits: std.StringHashMapUnmanaged(u32) = .{},
// Not named deinit, so the state store will not call it again.
fn release(self: *PageCache) void {
self.hits.deinit(self.allocator);
self.hits = .{};
}
};
const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);- ✓
초깃값은 리소스를 소유하지 않습니다.
bindState에는 제로 값이나 포인터, allocator 같은 빌린 필드만 넘기고, 할당이 필요한 것은*T를 얻은 뒤에 만듭니다. - ✓
allocator를 필드로 보관합니다.
deinit(*T)는 인자가 하나뿐이므로 해제에 쓸 allocator는 struct 안에 있어야 합니다. - ✓
죽은 노드를 붙잡지 마십시오. Node 포인터는 소속 트리와 Scope가 살아 있는 동안에만 유효합니다. 페이지가 언마운트되거나 재구성된 뒤에는 상태를 통해 옛 노드에 접근하지 마십시오.