Gestures
ui.interaction싱글, 더블, 트리플 클릭과 길게 누르기의 제스처 중재.
BEHAVIOR CONTRACT
하나의 히트 영역에서 여러 제스처를 시간 창으로 중재해 하나의 결과만 발생합니다.
RECORDED INTERACTION
harness가 제스처 영역을 더블 클릭하면 double 카운터가 1 증가합니다.
pub fn buildMultiClick(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
const a = cx.allocator;
const c = try col(cx, 12);
try c.appendChild(a, try label(cx, "Gesture arena — tap/double/triple counts + press-and-hold long_press (500ms) + pointer-cancel state"));
const pad = try ui.box(cx, .{
.width = .{ .px = 320 },
.height = .{ .px = 140 },
.background = light.color.bg_secondary,
.align_items = .center,
.justify = .center,
}, .{});
(try pad.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
pad.meta.ownership.meta.test_id = "story.multiclick.pad";
try pad.appendChild(a, try ui.text(cx, "click / double-click / hold", .{ .font_size = 13, .color = light.color.fg_secondary }));
try c.appendChild(a, pad);
const single_label = try ui.text(cx, "single: 0", .{ .font_size = 13, .color = light.color.fg_primary });
single_label.meta.ownership.meta.test_id = "story.multiclick.single";
const double_label = try ui.text(cx, "double: 0", .{ .font_size = 13, .color = light.color.fg_primary });
double_label.meta.ownership.meta.test_id = "story.multiclick.double";
const triple_label = try ui.text(cx, "triple: 0", .{ .font_size = 13, .color = light.color.fg_primary });
triple_label.meta.ownership.meta.test_id = "story.multiclick.triple";
const long_label = try ui.text(cx, "long: idle", .{ .font_size = 13, .color = light.color.fg_primary });
long_label.meta.ownership.meta.test_id = "story.multiclick.long";
try c.appendChild(a, single_label);
try c.appendChild(a, double_label);
try c.appendChild(a, triple_label);
try c.appendChild(a, long_label);
const st = try cx.bindState(MultiClickStory, .{
.cx = cx,
.single_label = single_label,
.double_label = double_label,
.triple_label = triple_label,
.long_label = long_label,
});
st.alive = true;
st.rec_ids[0] = try cx.registerGesture(pad, .tap, .{}, MultiClickStory.onGesture, @ptrCast(st));
st.rec_ids[1] = try cx.registerGesture(pad, .double_tap, .{}, MultiClickStory.onGesture, @ptrCast(st));
st.rec_ids[2] = try cx.registerGesture(pad, .triple_tap, .{}, MultiClickStory.onGesture, @ptrCast(st));
st.rec_ids[3] = try cx.registerGesture(pad, .long_press, .{}, MultiClickStory.onGesture, @ptrCast(st));
pad.meta.per_frame.hooks.slots.anim_state = @ptrCast(st);
pad.meta.per_frame.hooks.before_render.main = MultiClickStory.tickHook;
const rc = try a.create(MultiClickCleanup);
rc.* = .{ .cx = cx, .st = st };
try scope.registerResource(@ptrCast(rc), MultiClickCleanup.destroy);
return c;
}Storybook에 있는 이 story의 원본 코드입니다(zenit 5f9add5+wip 2026-09-30). W는 ui.widgets이며, col / row / label은 Storybook 내부의 작은 레이아웃 헬퍼입니다.