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 内の小さなレイアウトヘルパーです。