a73x

4f3b2926

Track wl_surface enter/leave on Window

a73x   2026-04-09 10:18

Commit message
Track wl_surface enter/leave on Window

Wires a wl_surface listener that routes enter/leave events into the
Connection's ScaleTracker, so Window.bufferScale() reflects the current
max scale across outputs the surface is mapped to.

Also wires up a dedicated wayland_tests module in build.zig — previously
the 3 tests in wayland.zig were not being picked up by any test binary.
The new test "Window.bufferScale reflects ScaleTracker entered outputs"
plus the 3 existing ones now actually run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

build.zig
Old New
@@ -120,6 +120,22 @@ pub fn build(b: *std.Build) void {
120 }); 120 });
121 test_step.dependOn(&b.addRunArtifact(scale_tracker_tests).step); 121 test_step.dependOn(&b.addRunArtifact(scale_tracker_tests).step);
122 122
123 // Test wayland.zig
124 const wayland_test_mod = b.createModule(.{
125 .root_source_file = b.path("src/wayland.zig"),
126 .target = target,
127 .optimize = optimize,
128 .link_libc = true,
129 });
130 wayland_test_mod.addImport("wayland", wayland_generated_mod);
131 wayland_test_mod.addImport("scale_tracker", scale_tracker_mod);
132 wayland_test_mod.linkSystemLibrary("wayland-client", .{});
133 wayland_test_mod.linkSystemLibrary("xkbcommon", .{});
134 const wayland_tests = b.addTest(.{
135 .root_module = wayland_test_mod,
136 });
137 test_step.dependOn(&b.addRunArtifact(wayland_tests).step);
138
123 // Test main.zig (and transitively vt.zig via its import) 139 // Test main.zig (and transitively vt.zig via its import)
124 const main_test_mod = b.createModule(.{ 140 const main_test_mod = b.createModule(.{
125 .root_source_file = b.path("src/main.zig"), 141 .root_source_file = b.path("src/main.zig"),
src/wayland.zig
Old New
@@ -241,6 +241,10 @@ pub const Window = struct {
241 surface: *wl.Surface, 241 surface: *wl.Surface,
242 xdg_surface: *xdg.Surface, 242 xdg_surface: *xdg.Surface,
243 xdg_toplevel: *xdg.Toplevel, 243 xdg_toplevel: *xdg.Toplevel,
244 tracker: *ScaleTracker,
245 outputs: *std.ArrayListUnmanaged(*Output),
246 scale_generation: u64 = 0,
247 applied_buffer_scale: i32 = 1,
244 configured: bool = false, 248 configured: bool = false,
245 should_close: bool = false, 249 should_close: bool = false,
246 width: u32 = 800, 250 width: u32 = 800,
@@ -256,6 +260,28 @@ pub const Window = struct {
256 pub fn setTitle(self: *Window, title: ?[:0]const u8) void { 260 pub fn setTitle(self: *Window, title: ?[:0]const u8) void {
257 self.xdg_toplevel.setTitle((title orelse "waystty")); 261 self.xdg_toplevel.setTitle((title orelse "waystty"));
258 } 262 }
263
264 pub fn bufferScale(self: *const Window) i32 {
265 return self.tracker.bufferScale();
266 }
267
268 pub fn handleSurfaceEnter(self: *Window, wl_out: *wl.Output) void {
269 for (self.outputs.items) |out| {
270 if (out.wl_output == wl_out) {
271 self.tracker.enterOutput(out.name) catch {};
272 return;
273 }
274 }
275 }
276
277 pub fn handleSurfaceLeave(self: *Window, wl_out: *wl.Output) void {
278 for (self.outputs.items) |out| {
279 if (out.wl_output == wl_out) {
280 self.tracker.leaveOutput(out.name);
281 return;
282 }
283 }
284 }
259 }; 285 };
260 286
261 pub const Connection = struct { 287 pub const Connection = struct {
@@ -335,9 +361,13 @@ pub const Connection = struct {
335 .surface = try compositor.createSurface(), 361 .surface = try compositor.createSurface(),
336 .xdg_surface = undefined, 362 .xdg_surface = undefined,
337 .xdg_toplevel = undefined, 363 .xdg_toplevel = undefined,
364 .tracker = &self.scale_tracker,
365 .outputs = &self.outputs,
338 }; 366 };
339 errdefer window.surface.destroy(); 367 errdefer window.surface.destroy();
340 368
369 window.surface.setListener(*Window, surfaceListener, window);
370
341 window.xdg_surface = try wm_base.getXdgSurface(window.surface); 371 window.xdg_surface = try wm_base.getXdgSurface(window.surface);
342 errdefer window.xdg_surface.destroy(); 372 errdefer window.xdg_surface.destroy();
343 373
@@ -519,6 +549,23 @@ fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *
519 } 549 }
520 } 550 }
521 551
552 fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void {
553 switch (event) {
554 .enter => |e| {
555 const wl_out = e.output orelse return;
556 window.handleSurfaceEnter(wl_out);
557 window.scale_generation += 1;
558 },
559 .leave => |e| {
560 const wl_out = e.output orelse return;
561 window.handleSurfaceLeave(wl_out);
562 window.scale_generation += 1;
563 },
564 .preferred_buffer_scale => {},
565 .preferred_buffer_transform => {},
566 }
567 }
568
522 fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { 569 fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void {
523 switch (event) { 570 switch (event) {
524 .configure => |cfg| { 571 .configure => |cfg| {
@@ -655,3 +702,20 @@ test "drainSelectionPipeThenRoundtrip drains large payload before roundtrip" {
655 try std.testing.expect(roundtrip_called); 702 try std.testing.expect(roundtrip_called);
656 try std.testing.expectEqualStrings(payload, text); 703 try std.testing.expectEqualStrings(payload, text);
657 } 704 }
705
706 test "Window.bufferScale reflects ScaleTracker entered outputs" {
707 var tracker = ScaleTracker.init(std.testing.allocator);
708 defer tracker.deinit();
709
710 try tracker.addOutput(1);
711 try tracker.addOutput(2);
712 tracker.setOutputScale(1, 1);
713 tracker.setOutputScale(2, 2);
714
715 // Simulate the bits Window.bufferScale delegates to.
716 try tracker.enterOutput(2);
717 try std.testing.expectEqual(@as(i32, 2), tracker.bufferScale());
718
719 tracker.leaveOutput(2);
720 try std.testing.expectEqual(@as(i32, 1), tracker.bufferScale());
721 }