From afe32c67997327e82aba8f70cad66182acb33886 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:03:37 -0500 Subject: [PATCH 1/2] linux: work-715 (codex) Agent-Team: linux Worker: codex --- core/form_factor.zig | 3 + core/sizing_tests.zig | 1 + core/widgets.zig | 71 ++++++++++- core/widgets/Button.zig | 194 +++++++++++++++++++++++++++++++ scripts/lint-allow.zon | 30 +++++ scripts/lint.zig | 43 +++++-- src/editor/explorer/settings.zig | 2 +- 7 files changed, 330 insertions(+), 14 deletions(-) create mode 100644 core/widgets/Button.zig diff --git a/core/form_factor.zig b/core/form_factor.zig index f21235431..02705b6c5 100644 --- a/core/form_factor.zig +++ b/core/form_factor.zig @@ -5,6 +5,9 @@ //! per frame, since a 2-in-1's pointer changes at runtime. `core.platform` holds the live value. const std = @import("std"); +/// Minimum touch hit width and height in points. +pub const min_hit: f32 = 44; + pub const FormFactor = enum { desktop, tablet, mobile }; /// The person's Layout setting (`settings.zon`'s `layout_mode`): `auto` follows what was detected, diff --git a/core/sizing_tests.zig b/core/sizing_tests.zig index 4901906a2..c942c7476 100644 --- a/core/sizing_tests.zig +++ b/core/sizing_tests.zig @@ -10,6 +10,7 @@ //! nothing else: every bug this pair has had was a dvui event-routing or layout-settle rule rather //! than arithmetic, and reading the code caught none of them. test { + _ = @import("widgets/Button.zig"); _ = @import("glass.zig"); _ = @import("widgets/Split.zig"); _ = @import("widgets/ContextWidget.zig"); diff --git a/core/widgets.zig b/core/widgets.zig index c8773046e..68297c4eb 100644 --- a/core/widgets.zig +++ b/core/widgets.zig @@ -19,11 +19,14 @@ const dvui = @import("dvui"); const builtin = @import("builtin"); const icons = @import("icons"); const platform = @import("platform.zig"); +const keyboard = @import("keyboard.zig"); const dialogs = @import("dialogs.zig"); const draw = @import("draw.zig"); const anchor = @import("replay").anchor; const icon_tex = @import("gfx/icon.zig"); +pub const Button = @import("widgets/Button.zig"); + pub const CanvasWidget = @import("widgets/CanvasWidget.zig"); pub const ReorderWidget = @import("widgets/ReorderWidget.zig"); pub const FloatingWindowWidget = @import("widgets/FloatingWindowWidget.zig"); @@ -226,7 +229,7 @@ pub fn textEntryDeinit(te: *dvui.TextEntryWidget) void { const focused = wd.id == dvui.focusedWidgetId(); te.deinit(); // The entry's own scroll area must be closed before scrolling the enclosing area. - @import("keyboard.zig").scrollIntoView(&wd, focused); + keyboard.scrollIntoView(&wd, focused); } /// A context menu's surface, opened at the point the pointer was pressed. Every right-click @@ -476,3 +479,69 @@ pub fn tabCloseButtonOptions(over: dvui.Options) dvui.Options { .ninepatch_press = &dvui.Ninepatch.none, })); } + +// Keep these button helpers in step with dvui's helpers when updating the dvui pin. +/// Draw a labelled button and return whether it was clicked this frame. +/// Uses a centred minimum 44 pt touch target; mouse, drawing and layout follow dvui. +/// The label, interaction options and styling have the same contract as `dvui.button`. +pub fn button(src: std.builtin.SourceLocation, label_str: []const u8, init_opts: Button.InitOptions, opts: dvui.Options) bool { + var bw: Button = undefined; + bw.init(src, init_opts, opts); + bw.processEvents(); + bw.drawBackground(); + const click = bw.clicked(); + dvui.labelNoFmt(@src(), label_str, .{ .align_x = 0.5, .align_y = 0.5 }, opts.strip().override(bw.style()).override(.{ .gravity_x = 0.5, .gravity_y = 0.5 })); + bw.drawFocus(); + bw.deinit(); + + return click; +} + +/// Draw the TinyVG icon in `tvg_bytes`, identified by `name`, and return a click this frame. +/// Uses a centred minimum 44 pt touch target and the cached icon renderer. +/// Interaction, icon and styling options follow `dvui.buttonIcon`. +pub fn buttonIcon(src: std.builtin.SourceLocation, name: []const u8, tvg_bytes: []const u8, init_opts: Button.InitOptions, icon_opts: dvui.IconRenderOptions, opts: dvui.Options) bool { + const defaults = dvui.Options{ .padding = dvui.Rect.all(4), .label = .{ .text = name } }; + var bw: Button = undefined; + bw.init(src, init_opts, defaults.override(opts)); + bw.processEvents(); + bw.drawBackground(); + icon_tex.icon( + @src(), + name, + tvg_bytes, + icon_opts, + opts.strip().override(bw.style()).override(.{ .gravity_x = 0.5, .gravity_y = 0.5, .min_size_content = opts.min_size_content, .expand = .ratio, .color_text = opts.color_text, .role = .none }), + ); + + const click = bw.clicked(); + bw.drawFocus(); + bw.deinit(); + return click; +} + +pub const ButtonLabelAndIconOptions = dvui.ButtonLabelAndIconOptions; + +/// Draw the label and icon described by `combined_opts` and return a click this frame. +/// Uses a centred minimum 44 pt touch target and the cached icon renderer. +/// Combined and styling options follow `dvui.buttonLabelAndIcon`. +pub fn buttonLabelAndIcon(src: std.builtin.SourceLocation, combined_opts: ButtonLabelAndIconOptions, opts: dvui.Options) bool { + var bw: Button = undefined; + bw.init(src, combined_opts.button_opts, opts); + bw.processEvents(); + const options = opts.strip().override(bw.style()).override(.{ .gravity_y = 0.5 }); + bw.drawBackground(); + { + var outer_hbox = dvui.box(src, .{ .dir = .horizontal }, .{ .expand = .horizontal, .gravity_y = 0.5 }); + defer outer_hbox.deinit(); + icon_tex.icon(@src(), combined_opts.icon_label orelse combined_opts.label, combined_opts.tvg_bytes, .{}, options.strip().override(.{ .gravity_x = if (combined_opts.icon_first) 0.0 else 1.0, .color_text = opts.color_text })); + dvui.labelEx(@src(), "{s}", .{combined_opts.label}, .{ .align_x = 0.5 }, options.strip().override(.{ .expand = .both })); + } + + const click = bw.clicked(); + + bw.drawFocus(); + + bw.deinit(); + return click; +} diff --git a/core/widgets/Button.zig b/core/widgets/Button.zig new file mode 100644 index 000000000..04606bc16 --- /dev/null +++ b/core/widgets/Button.zig @@ -0,0 +1,194 @@ +//! dvui's button with a minimum touch hit area independent of its drawn and layout rectangles. +const std = @import("std"); +const dvui = @import("dvui"); +const form_factor = @import("../form_factor.zig"); +const Button = @This(); + +inner: dvui.ButtonWidget, + +pub const InitOptions = dvui.ButtonWidget.InitOptions; + +/// Install a button using dvui interaction and layout options; pair with `deinit`. +/// Call during a frame, then process events before drawing its background and children. +pub fn init(self: *Button, src: std.builtin.SourceLocation, init_opts: InitOptions, opts: dvui.Options) void { + self.inner.init(src, init_opts, opts); +} + +/// The physical click rectangle, centred on the border and never smaller than it. +pub fn hitRect(self: *Button) dvui.Rect.Physical { + const rs = self.data().borderRectScale(); + var rect = rs.r; + if (dvui.pointerKind() == .touch) { + const minimum = form_factor.min_hit * rs.s; + const w = @max(rect.w, minimum); + const h = @max(rect.h, minimum); + rect.x -= (w - rect.w) / 2; + rect.y -= (h - rect.h) / 2; + rect.w = w; + rect.h = h; + } + return rect; +} + +/// Consume this frame's clicks using the touch hit rectangle; call once before drawing. +pub fn processEvents(self: *Button) void { + // TODO(touch-target arbitration): choose the nearest border before consuming overlapping + // targets; processing order wins today, including over a neighbour's border. Resolve before + // migrating tightly packed toolbars. + self.inner.click = dvui.clicked(self.data(), .{ + .rect = self.hitRect(), + .hovered = &self.inner.hover, + .touch_drag = self.inner.init_options.touch_drag, + }); +} + +/// Test an event against dvui's widget event routing, without consuming it. +pub fn matchEvent(self: *Button, e: *dvui.Event) bool { + return self.inner.matchEvent(e); +} + +/// Update hover state using dvui's border rectangle. +pub fn processHover(self: *Button) void { + self.inner.processHover(); +} + +/// Draw the styled border and background without expanding them to the touch target. +pub fn drawBackground(self: *Button) void { + self.inner.drawBackground(); +} + +/// Draw the keyboard focus indicator after drawing the button's children. +pub fn drawFocus(self: *Button) void { + self.inner.drawFocus(); +} + +/// The current dvui style, including hover and press state, for styling children. +pub fn style(self: *Button) dvui.Options { + return self.inner.style(); +} + +/// Whether this button owns keyboard focus in the current window. +pub fn focused(self: *Button) bool { + return self.inner.focused(); +} + +/// Whether this frame's event processing marked the button hovered. +pub fn hovered(self: *Button) bool { + return self.inner.hovered(); +} + +/// Whether this button currently holds pointer capture. +pub fn pressed(self: *Button) bool { + return self.inner.pressed(); +} + +/// Whether event processing activated the button this frame. +pub fn clicked(self: *Button) bool { + return self.inner.clicked(); +} + +/// The inner dvui widget and its layout vtable, valid until `deinit`. +pub fn widget(self: *Button) dvui.Widget { + return self.inner.widget(); +} + +/// Borrow the inner widget data; valid until `deinit`. +pub fn data(self: *Button) *dvui.WidgetData { + return self.inner.data(); +} + +/// Delegate child placement to the inner widget using dvui layout coordinates. +pub fn rectFor(self: *Button, id: dvui.Id, min_size: dvui.Size, e: dvui.Options.Expand, g: dvui.Options.Gravity) dvui.Rect { + return self.inner.rectFor(id, min_size, e, g); +} + +/// Convert a local rectangle to physical screen coordinates using the inner widget. +pub fn screenRectScale(self: *Button, rect: dvui.Rect) dvui.RectScale { + return self.inner.screenRectScale(rect); +} + +/// Report a child's minimum size to the inner widget's layout. +pub fn minSizeForChild(self: *Button, size: dvui.Size) void { + self.inner.minSizeForChild(size); +} + +/// Finish layout and restore the parent; releases a dvui-allocated widget if applicable. +pub fn deinit(self: *Button) void { + self.inner.deinit(); +} + +test "touch clicks six points outside a 32 point border without changing its drawn rect" { + const Frame = struct { + // The test callback writes these on the testing window's thread. + var border: dvui.Rect.Physical = .{}; + // The test callback writes this on the testing window's thread. + var click: bool = false; + + fn draw() !dvui.App.Result { + var button: Button = undefined; + button.init(@src(), .{}, .{ + .rect = .{ .x = 50, .y = 50, .w = 32, .h = 32 }, + .margin = dvui.Rect.all(0), + }); + defer button.deinit(); + button.processEvents(); + button.drawBackground(); + border = button.data().borderRectScale().r; + click = button.clicked(); + return .ok; + } + }; + var t = try dvui.testing.init(.{ .window_size = .{ .w = 200, .h = 200 } }); + defer t.deinit(); + try dvui.testing.settle(Frame.draw); + const original = Frame.border; + const cw = dvui.currentWindow(); + const scale = cw.natural_scale; + const point: dvui.Point.Physical = .{ .x = original.x - 6 * scale, .y = original.center().y }; + const norm: dvui.Point = .{ .x = point.x / (200 * scale), .y = point.y / (200 * scale) }; + _ = try cw.addEventPointer(.{ .button = .touch0, .action = .press, .xynorm = norm }); + _ = try dvui.testing.step(Frame.draw); + _ = try cw.addEventPointer(.{ .button = .touch0, .action = .release, .xynorm = norm }); + _ = try dvui.testing.step(Frame.draw); + try std.testing.expectEqual(.touch, dvui.pointerKind()); + try std.testing.expect(Frame.click); + try std.testing.expectEqual(original, Frame.border); + + _ = try cw.addEventMouseMotion(.{ .pt = point }); + _ = try cw.addEventMouseButton(.left, .press); + _ = try dvui.testing.step(Frame.draw); + _ = try cw.addEventMouseButton(.left, .release); + _ = try dvui.testing.step(Frame.draw); + try std.testing.expectEqual(.mouse, dvui.pointerKind()); + try std.testing.expect(!Frame.click); + try std.testing.expectEqual(original, Frame.border); +} + +test "button verbs draw and accept a touch outside their borders" { + const widgets = @import("../widgets.zig"); + const Frame = struct { + // The draw callback writes the click results on the testing window's thread. + var clicks: [3]bool = .{ false, false, false }; + + fn draw() !dvui.App.Result { + const opts: dvui.Options = .{ .rect = .{ .x = 50, .y = 50, .w = 32, .h = 32 }, .margin = dvui.Rect.all(0) }; + clicks[0] = widgets.button(@src(), "Label", .{}, opts); + clicks[1] = widgets.buttonIcon(@src(), "Icon", dvui.entypo.cross, .{}, .{}, opts.override(.{ .rect = .{ .x = 50, .y = 100, .w = 32, .h = 32 } })); + clicks[2] = widgets.buttonLabelAndIcon(@src(), .{ .label = "Both", .tvg_bytes = dvui.entypo.cross }, opts.override(.{ .rect = .{ .x = 50, .y = 150, .w = 32, .h = 32 } })); + return .ok; + } + }; + var t = try dvui.testing.init(.{ .window_size = .{ .w = 200, .h = 200 } }); + defer t.deinit(); + try dvui.testing.settle(Frame.draw); + const cw = dvui.currentWindow(); + for (0..3) |i| { + // Six points left of each border, at its vertical centre. + const norm: dvui.Point = .{ .x = 44.0 / 200.0, .y = (66.0 + 50.0 * @as(f32, @floatFromInt(i))) / 200.0 }; + _ = try cw.addEventPointer(.{ .button = .touch0, .action = .press, .xynorm = norm }); + _ = try dvui.testing.step(Frame.draw); + _ = try cw.addEventPointer(.{ .button = .touch0, .action = .release, .xynorm = norm }); + _ = try dvui.testing.step(Frame.draw); + for (Frame.clicks, 0..) |activated, j| try std.testing.expectEqual(i == j, activated); + } +} diff --git a/scripts/lint-allow.zon b/scripts/lint-allow.zon index 8e5f8773b..2b5042a93 100644 --- a/scripts/lint-allow.zon +++ b/scripts/lint-allow.zon @@ -103,4 +103,34 @@ .{ .rule = .@"raw-scroll-area", .file = "src/editor/dialogs/FileTypeDefaults.zig", .count = 1 }, .{ .rule = .@"raw-scroll-area", .file = "src/editor/dialogs/PluginUpdates.zig", .count = 1 }, .{ .rule = .@"raw-scroll-area", .file = "tests/integration.zig", .count = 3 }, + .{ .rule = .@"raw-button", .file = "app/ActivityFeed.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "app/layout/Picker.zig", .count = 3 }, + .{ .rule = .@"raw-button", .file = "app/layout/Region.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "app/store/PluginStore.zig", .count = 17 }, + .{ .rule = .@"raw-button", .file = "app/update/update_notify.zig", .count = 4 }, + .{ .rule = .@"raw-button", .file = "core/dialogs.zig", .count = 4 }, + .{ .rule = .@"raw-button", .file = "core/widgets/ContextWidget.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "core/widgets/DockingWidget.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "core/widgets/TreeWidget.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "core/widgets/glass_button.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "plugins/dashboard/src/View.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "plugins/devtools/src/Dashboard.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "plugins/text/src/TextEditor.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "plugins/text/src/widgets/TextEntryWidget.zig", .count = 4 }, + .{ .rule = .@"raw-button", .file = "plugins/workbench/src/Workspace.zig", .count = 4 }, + .{ .rule = .@"raw-button", .file = "plugins/workbench/src/files.zig", .count = 3 }, + .{ .rule = .@"raw-button", .file = "sdk/replay/Snapshot.zig", .count = 4 }, + .{ .rule = .@"raw-button", .file = "sdk/src/Host.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/Accounts.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/Editor.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/FileTypeSettings.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/Infobar.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/KeybindSettings.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "src/editor/LayoutSettings.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/OutputPanel.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/dialogs/SaveQuestion.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "src/editor/explorer/settings.zig", .count = 2 }, + .{ .rule = .@"raw-button", .file = "src/editor/layout_mobile.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "tests/bench/bench_replay.zig", .count = 1 }, + .{ .rule = .@"raw-button", .file = "tests/integration.zig", .count = 2 }, } diff --git a/scripts/lint.zig b/scripts/lint.zig index 70fd3a469..ccfbd7ce9 100644 --- a/scripts/lint.zig +++ b/scripts/lint.zig @@ -12,6 +12,7 @@ const Rule = enum { @"changelog-words", @"raw-transient-surface", @"raw-scroll-area", + @"raw-button", }; const Finding = struct { rule: Rule, line: usize, message: []const u8 }; const Allow = struct { rule: Rule, file: []const u8, count: usize }; @@ -249,7 +250,7 @@ const RawCall = struct { name: []const u8, rule: Rule, message: []const u8, - call_only: bool = false, + next: ?std.zig.Token.Tag = null, }; const raw_calls = blk: { const surfaces = [_][]const u8{ @@ -257,7 +258,7 @@ const raw_calls = blk: { "popup", "PopupWidget", "floatingWindow", "FloatingWindowWidget", "dialog", "toast", "contextMenu", "dropdown", }; - var calls: [surfaces.len + 1]RawCall = undefined; + var calls: [surfaces.len + 5]RawCall = undefined; for (surfaces, 0..) |name, i| calls[i] = .{ .name = name, .rule = .@"raw-transient-surface", @@ -267,7 +268,13 @@ const raw_calls = blk: { .name = "scrollArea", .rule = .@"raw-scroll-area", .message = "dvui.scrollArea bypasses touch scrolling; use core.widgets.ScrollArea.init", - .call_only = true, + .next = .l_paren, + }; + for ([_][]const u8{ "button", "buttonIcon", "buttonLabelAndIcon", "ButtonWidget" }, 0..) |name, i| calls[surfaces.len + 1 + i] = .{ + .name = name, + .rule = .@"raw-button", + .message = "raw dvui button bypasses touch hit targets; use core.widgets.Button or its verbs", + .next = if (i == 3) .equal else .l_paren, }; break :blk calls; }; @@ -286,7 +293,9 @@ fn rawDvuiCalls( const name = tree.tokenSlice(@intCast(i + 2)); for (raw_calls) |banned| { if (!std.mem.eql(u8, name, banned.name)) continue; - if (banned.call_only and (i + 3 >= tags.len or tags[i + 3] != .l_paren)) continue; + if (banned.next) |next| { + if (i + 3 >= tags.len or tags[i + 3] != next) continue; + } const line = tree.tokenLocation(0, @intCast(i)).line + 1; if (silenced(suppressions, banned.rule, line)) break; try findings.append(allocator, .{ @@ -861,11 +870,9 @@ test "dvui.scrollArea is flagged" { test "a lint-allow with a reason silences it" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - for ([_][:0]const u8{ - "// lint-allow(raw-scroll-area): wrapper implementation\n" ++ - "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); }", - "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); } // lint-allow(raw-scroll-area): wrapper implementation", - }) |source| try std.testing.expectEqual(@as(usize, 0), (try analyze(arena.allocator(), source)).len); + const findings = try analyze(arena.allocator(), "// lint-allow(raw-scroll-area): wrapper implementation\n" ++ + "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); }"); + try std.testing.expectEqual(@as(usize, 0), findings.len); } test "core.widgets.ScrollArea.init is not flagged" { @@ -889,7 +896,19 @@ test "a file over its raw-scroll-area budget fails" { try std.testing.expect(try reportFile(&output.writer, "over.zig", findings, &allowances, &seen, false)); try std.testing.expect(seen[0]); try std.testing.expect(std.mem.endsWith(u8, output.written(), "raw-scroll-area: count 2 exceeds allowance 1\n")); - output.clearRetainingCapacity(); - try std.testing.expect(!try reportFile(&output.writer, "over.zig", findings[0..1], &allowances, &seen, false)); - try std.testing.expectEqualStrings("", output.written()); +} + +test "raw-button catches verbs and declarations but allows type references" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const findings = try analyze( + arena.allocator(), + "fn f() void { _ = dvui.button(a,b,c,d); _ = dvui.buttonIcon(a,b,c,d,e,f); _ = dvui.buttonLabelAndIcon(a,b,c); var b: dvui.ButtonWidget = undefined; }\n" ++ + "const InitOptions = dvui.ButtonWidget.InitOptions;\n" ++ + "fn g(b: *dvui.ButtonWidget) void {}\n" ++ + "// lint-allow(raw-button): wrapper owns the dvui implementation\n" ++ + "var b: dvui.ButtonWidget = undefined;", + ); + try std.testing.expectEqual(@as(usize, 4), findings.len); + for (findings) |finding| try std.testing.expectEqual(Rule.@"raw-button", finding.rule); } diff --git a/src/editor/explorer/settings.zig b/src/editor/explorer/settings.zig index afc858629..631895231 100644 --- a/src/editor/explorer/settings.zig +++ b/src/editor/explorer/settings.zig @@ -717,7 +717,7 @@ fn drawFloatWindows() void { drawToggle(&editor.app.settings.float_windows, "Floats in their own windows"); // Taken at launch: changed, it waits for a restart, offered here and in the info bar. if (editor.restartPending()) { - if (dvui.button(@src(), "Restart", .{}, .{ .gravity_y = 0.5, .margin = .{ .x = 6 }, .padding = .{ .x = 8, .w = 8, .y = 2, .h = 2 } })) { + if (core.widgets.button(@src(), "Restart", .{}, .{ .gravity_y = 0.5, .margin = .{ .x = 6 }, .padding = .{ .x = 8, .w = 8, .y = 2, .h = 2 } })) { restart.request(); } } From 5fbc81bd836580d3f48ac7c09c8d14b42249ca66 Mon Sep 17 00:00:00 2001 From: "fizzy-linux[bot]" <346940526+fizzy-linux[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:38:38 -0500 Subject: [PATCH 2/2] linux: work-715 (codex) Agent-Team: linux Worker: codex --- core/widgets/Button.zig | 47 +---------------------------------------- scripts/lint.zig | 11 +++++++--- 2 files changed, 9 insertions(+), 49 deletions(-) diff --git a/core/widgets/Button.zig b/core/widgets/Button.zig index 04606bc16..a7c9aa707 100644 --- a/core/widgets/Button.zig +++ b/core/widgets/Button.zig @@ -35,6 +35,7 @@ pub fn processEvents(self: *Button) void { // TODO(touch-target arbitration): choose the nearest border before consuming overlapping // targets; processing order wins today, including over a neighbour's border. Resolve before // migrating tightly packed toolbars. + // dvui's ButtonWidget uses this same click path; only the hit rectangle differs. self.inner.click = dvui.clicked(self.data(), .{ .rect = self.hitRect(), .hovered = &self.inner.hover, @@ -42,16 +43,6 @@ pub fn processEvents(self: *Button) void { }); } -/// Test an event against dvui's widget event routing, without consuming it. -pub fn matchEvent(self: *Button, e: *dvui.Event) bool { - return self.inner.matchEvent(e); -} - -/// Update hover state using dvui's border rectangle. -pub fn processHover(self: *Button) void { - self.inner.processHover(); -} - /// Draw the styled border and background without expanding them to the touch target. pub fn drawBackground(self: *Button) void { self.inner.drawBackground(); @@ -67,51 +58,16 @@ pub fn style(self: *Button) dvui.Options { return self.inner.style(); } -/// Whether this button owns keyboard focus in the current window. -pub fn focused(self: *Button) bool { - return self.inner.focused(); -} - -/// Whether this frame's event processing marked the button hovered. -pub fn hovered(self: *Button) bool { - return self.inner.hovered(); -} - -/// Whether this button currently holds pointer capture. -pub fn pressed(self: *Button) bool { - return self.inner.pressed(); -} - /// Whether event processing activated the button this frame. pub fn clicked(self: *Button) bool { return self.inner.clicked(); } -/// The inner dvui widget and its layout vtable, valid until `deinit`. -pub fn widget(self: *Button) dvui.Widget { - return self.inner.widget(); -} - /// Borrow the inner widget data; valid until `deinit`. pub fn data(self: *Button) *dvui.WidgetData { return self.inner.data(); } -/// Delegate child placement to the inner widget using dvui layout coordinates. -pub fn rectFor(self: *Button, id: dvui.Id, min_size: dvui.Size, e: dvui.Options.Expand, g: dvui.Options.Gravity) dvui.Rect { - return self.inner.rectFor(id, min_size, e, g); -} - -/// Convert a local rectangle to physical screen coordinates using the inner widget. -pub fn screenRectScale(self: *Button, rect: dvui.Rect) dvui.RectScale { - return self.inner.screenRectScale(rect); -} - -/// Report a child's minimum size to the inner widget's layout. -pub fn minSizeForChild(self: *Button, size: dvui.Size) void { - self.inner.minSizeForChild(size); -} - /// Finish layout and restore the parent; releases a dvui-allocated widget if applicable. pub fn deinit(self: *Button) void { self.inner.deinit(); @@ -121,7 +77,6 @@ test "touch clicks six points outside a 32 point border without changing its dra const Frame = struct { // The test callback writes these on the testing window's thread. var border: dvui.Rect.Physical = .{}; - // The test callback writes this on the testing window's thread. var click: bool = false; fn draw() !dvui.App.Result { diff --git a/scripts/lint.zig b/scripts/lint.zig index ccfbd7ce9..8155e8a56 100644 --- a/scripts/lint.zig +++ b/scripts/lint.zig @@ -870,9 +870,11 @@ test "dvui.scrollArea is flagged" { test "a lint-allow with a reason silences it" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); - const findings = try analyze(arena.allocator(), "// lint-allow(raw-scroll-area): wrapper implementation\n" ++ - "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); }"); - try std.testing.expectEqual(@as(usize, 0), findings.len); + for ([_][:0]const u8{ + "// lint-allow(raw-scroll-area): wrapper implementation\n" ++ + "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); }", + "fn f() void { _ = dvui.scrollArea(@src(), .{}, .{}); } // lint-allow(raw-scroll-area): wrapper implementation", + }) |source| try std.testing.expectEqual(@as(usize, 0), (try analyze(arena.allocator(), source)).len); } test "core.widgets.ScrollArea.init is not flagged" { @@ -896,6 +898,9 @@ test "a file over its raw-scroll-area budget fails" { try std.testing.expect(try reportFile(&output.writer, "over.zig", findings, &allowances, &seen, false)); try std.testing.expect(seen[0]); try std.testing.expect(std.mem.endsWith(u8, output.written(), "raw-scroll-area: count 2 exceeds allowance 1\n")); + output.clearRetainingCapacity(); + try std.testing.expect(!try reportFile(&output.writer, "over.zig", findings[0..1], &allowances, &seen, false)); + try std.testing.expectEqualStrings("", output.written()); } test "raw-button catches verbs and declarations but allows type references" {