Drag
ui.interactionZiehen und ein horizontaler Größenänderungsgriff.
BEHAVIOR CONTRACT
Pointer-Capture, Schwellenwert, grab / grabbing-Cursor und Klickunterdrückung übernimmt ui.interaction.
RECORDED INTERACTION
Der Harness zieht die Box; der Zustand wechselt idle → dragging.
pub fn buildDrag(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, "Drag — ui.interaction.drag primitive (threshold 4px, Escape cancels, arrows nudge when focused)"));
// ── 1. 自由移动方块(both 轴 + click 计数 + 键盘等价)──
const arena = try ui.box(cx, .{
.width = .{ .px = 420 },
.height = .{ .px = 180 },
.background = light.color.bg_secondary,
}, .{});
(try arena.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(12);
const subject = try ui.box(cx, .{
.width = .{ .px = 72 },
.height = .{ .px = 72 },
.background = light.color.accent,
.align_items = .center,
.justify = .center,
}, .{});
(try subject.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(10);
subject.style.cursor = .grab;
subject.meta.ownership.meta.test_id = "story.drag.box";
subject.setFocusable(true);
try subject.appendChild(a, try ui.text(cx, "drag", .{ .font_size = 13, .color = ui.Color.rgba(255, 255, 255, 255) }));
try arena.appendChild(a, subject);
const status_label = try ui.text(cx, "state: idle", .{ .font_size = 13, .color = light.color.fg_primary });
status_label.meta.ownership.meta.test_id = "story.drag.status";
const clicks_label = try ui.text(cx, "clicks: 0 (drag suppresses click)", .{ .font_size = 13, .color = light.color.fg_primary });
clicks_label.meta.ownership.meta.test_id = "story.drag.clicks";
// ── 2. 水平 resize handle ──
const resize_row = try row(cx, 0);
const panel = try ui.box(cx, .{
.width = .{ .px = 240 },
.height = .{ .px = 80 },
.background = light.color.bg_tertiary,
.align_items = .center,
.justify = .center,
}, .{});
panel.meta.ownership.meta.test_id = "story.drag.panel";
(try panel.style.ensureExtFallible(a)).corner_radius = ui.CornerRadius.uniform(8);
const width_label = try ui.text(cx, "width: 240px", .{ .font_size = 13, .color = light.color.fg_primary });
width_label.meta.ownership.meta.test_id = "story.drag.width";
try panel.appendChild(a, width_label);
const handle = try ui.box(cx, .{
.width = .{ .px = 10 },
.height = .{ .px = 80 },
.background = light.color.border,
}, .{});
handle.style.cursor = .col_resize;
handle.meta.ownership.meta.test_id = "story.drag.handle";
try resize_row.appendChild(a, panel);
try resize_row.appendChild(a, handle);
const st = try cx.bindState(DragStory, .{
.cx = cx,
.subject = subject,
.status_label = status_label,
.clicks_label = clicks_label,
.panel = panel,
.width_label = width_label,
});
_ = try ui.interaction.drag.Binding.attach(scope, cx, subject, .{
.activation_distance = 4,
.active_cursor = .grabbing,
}, DragStory.onMove, st);
_ = try ui.interaction.drag.Binding.attach(scope, cx, handle, .{
.axis = .horizontal,
.active_cursor = .col_resize,
}, DragStory.onResize, st);
// click 计数走独立的 on_click 槽(drag binding 占用 on_event,两者可组合;
// 越阈值/取消后 dispatcher 会抑制 click —— 计数不会涨)
subject.behavior.events.on_click = .{ .callback = DragStory.onClick, .context = @ptrCast(st) };
// 键盘等价操作走独立的 on_key_down 槽(key_context 与 event_context 分离)
subject.behavior.events.on_key_down = DragStory.onKeyDown;
subject.behavior.events.key_context = @ptrCast(st);
try c.appendChild(a, arena);
try c.appendChild(a, status_label);
try c.appendChild(a, clicks_label);
try c.appendChild(a, try label(cx, "Horizontal resize — drag the handle; delta is axis-projected"));
try c.appendChild(a, resize_row);
return c;
}Der unveränderte Quellcode der Story aus dem Storybook (zenit 5f9add5+wip 2026-09-30). W steht für ui.widgets; col / row / label sind kleine Layout-Helfer des Storybooks.