Gestures
ui.interactionArbitrage des gestes pour clic simple, double, triple et appui long.
BEHAVIOR CONTRACT
Les gestes sur une même zone sont arbitrés par fenêtre temporelle et un seul l'emporte.
RECORDED INTERACTION
Le harness double-clique sur la zone ; le compteur double s'incrémente.
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;
}Le code source tel quel de la story dans le Storybook (zenit 5f9add5+wip 2026-09-30). W désigne ui.widgets ; col / row / label sont de petits helpers de mise en page du Storybook.