2897c867
Add pure ScaleTracker for wl_output scale tracking
a73x 2026-04-09 10:04
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
diff --git a/build.zig b/build.zig index e74c775..42d5c65 100644 --- a/build.zig +++ b/build.zig @@ -9,6 +9,12 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); const scale_tracker_mod = b.createModule(.{ .root_source_file = b.path("src/scale_tracker.zig"), .target = target, .optimize = optimize, }); // Lazy-fetch the ghostty dependency. On the first invocation this // materializes the package; subsequent builds use the local cache. const ghostty_dep = b.lazyDependency("ghostty", .{}); @@ -37,6 +43,7 @@ pub fn build(b: *std.Build) void { .link_libc = true, }); wayland_mod.addImport("wayland", wayland_generated_mod); wayland_mod.addImport("scale_tracker", scale_tracker_mod); wayland_mod.linkSystemLibrary("wayland-client", .{}); wayland_mod.linkSystemLibrary("xkbcommon", .{}); _ = wayland_dep; // referenced via Scanner @@ -101,6 +108,17 @@ pub fn build(b: *std.Build) void { }); test_step.dependOn(&b.addRunArtifact(pty_tests).step); // Test scale_tracker.zig const scale_tracker_test_mod = b.createModule(.{ .root_source_file = b.path("src/scale_tracker.zig"), .target = target, .optimize = optimize, }); const scale_tracker_tests = b.addTest(.{ .root_module = scale_tracker_test_mod, }); test_step.dependOn(&b.addRunArtifact(scale_tracker_tests).step); // Test main.zig (and transitively vt.zig via its import) const main_test_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), diff --git a/src/scale_tracker.zig b/src/scale_tracker.zig new file mode 100644 index 0000000..87c2e45 --- /dev/null +++ b/src/scale_tracker.zig @@ -0,0 +1,137 @@ const std = @import("std"); pub const OutputId = u32; pub const ScaleTracker = struct { alloc: std.mem.Allocator, scales: std.AutoHashMapUnmanaged(OutputId, i32), entered: std.AutoHashMapUnmanaged(OutputId, void), pub fn init(alloc: std.mem.Allocator) ScaleTracker { return .{ .alloc = alloc, .scales = .empty, .entered = .empty, }; } pub fn deinit(self: *ScaleTracker) void { self.scales.deinit(self.alloc); self.entered.deinit(self.alloc); } pub fn addOutput(self: *ScaleTracker, id: OutputId) !void { try self.scales.put(self.alloc, id, 1); } pub fn setOutputScale(self: *ScaleTracker, id: OutputId, scale: i32) void { if (self.scales.getPtr(id)) |slot| slot.* = scale; } pub fn removeOutput(self: *ScaleTracker, id: OutputId) void { _ = self.scales.remove(id); _ = self.entered.remove(id); } pub fn enterOutput(self: *ScaleTracker, id: OutputId) !void { try self.entered.put(self.alloc, id, {}); } pub fn leaveOutput(self: *ScaleTracker, id: OutputId) void { _ = self.entered.remove(id); } pub fn bufferScale(self: *const ScaleTracker) i32 { var max_scale: i32 = 1; var it = self.entered.iterator(); while (it.next()) |entry| { const id = entry.key_ptr.*; if (self.scales.get(id)) |s| { if (s > max_scale) max_scale = s; } } return max_scale; } }; test "new tracker reports default scale of 1" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); try std.testing.expectEqual(@as(i32, 1), t.bufferScale()); } test "entered output scale is reflected in bufferScale" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); try t.addOutput(1); t.setOutputScale(1, 2); try t.enterOutput(1); try std.testing.expectEqual(@as(i32, 2), t.bufferScale()); } test "not-yet-entered output does not change bufferScale" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); try t.addOutput(7); t.setOutputScale(7, 3); try std.testing.expectEqual(@as(i32, 1), t.bufferScale()); } test "bufferScale is max across entered outputs" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); try t.addOutput(1); try t.addOutput(2); t.setOutputScale(1, 1); t.setOutputScale(2, 2); try t.enterOutput(1); try t.enterOutput(2); try std.testing.expectEqual(@as(i32, 2), t.bufferScale()); } test "leaving an output drops its contribution" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); try t.addOutput(1); try t.addOutput(2); t.setOutputScale(1, 2); t.setOutputScale(2, 3); try t.enterOutput(1); try t.enterOutput(2); try std.testing.expectEqual(@as(i32, 3), t.bufferScale()); t.leaveOutput(2); try std.testing.expectEqual(@as(i32, 2), t.bufferScale()); } test "removing an unknown output is a no-op" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); t.removeOutput(999); try std.testing.expectEqual(@as(i32, 1), t.bufferScale()); } test "removeOutput also removes it from entered set" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); try t.addOutput(5); t.setOutputScale(5, 4); try t.enterOutput(5); try std.testing.expectEqual(@as(i32, 4), t.bufferScale()); t.removeOutput(5); try std.testing.expectEqual(@as(i32, 1), t.bufferScale()); } test "setOutputScale on unknown id is a no-op" { var t = ScaleTracker.init(std.testing.allocator); defer t.deinit(); t.setOutputScale(999, 5); try std.testing.expectEqual(@as(i32, 1), t.bufferScale()); }