a73x

4f3b2926

Track wl_surface enter/leave on Window

a73x   2026-04-09 10:18

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>

diff --git a/build.zig b/build.zig
index 8e236e9..cfab411 100644
--- a/build.zig
+++ b/build.zig
@@ -120,6 +120,22 @@ pub fn build(b: *std.Build) void {
    });
    test_step.dependOn(&b.addRunArtifact(scale_tracker_tests).step);

    // Test wayland.zig
    const wayland_test_mod = b.createModule(.{
        .root_source_file = b.path("src/wayland.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
    });
    wayland_test_mod.addImport("wayland", wayland_generated_mod);
    wayland_test_mod.addImport("scale_tracker", scale_tracker_mod);
    wayland_test_mod.linkSystemLibrary("wayland-client", .{});
    wayland_test_mod.linkSystemLibrary("xkbcommon", .{});
    const wayland_tests = b.addTest(.{
        .root_module = wayland_test_mod,
    });
    test_step.dependOn(&b.addRunArtifact(wayland_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/wayland.zig b/src/wayland.zig
index f19aa7a..476ec45 100644
--- a/src/wayland.zig
+++ b/src/wayland.zig
@@ -241,6 +241,10 @@ pub const Window = struct {
    surface: *wl.Surface,
    xdg_surface: *xdg.Surface,
    xdg_toplevel: *xdg.Toplevel,
    tracker: *ScaleTracker,
    outputs: *std.ArrayListUnmanaged(*Output),
    scale_generation: u64 = 0,
    applied_buffer_scale: i32 = 1,
    configured: bool = false,
    should_close: bool = false,
    width: u32 = 800,
@@ -256,6 +260,28 @@ pub const Window = struct {
    pub fn setTitle(self: *Window, title: ?[:0]const u8) void {
        self.xdg_toplevel.setTitle((title orelse "waystty"));
    }

    pub fn bufferScale(self: *const Window) i32 {
        return self.tracker.bufferScale();
    }

    pub fn handleSurfaceEnter(self: *Window, wl_out: *wl.Output) void {
        for (self.outputs.items) |out| {
            if (out.wl_output == wl_out) {
                self.tracker.enterOutput(out.name) catch {};
                return;
            }
        }
    }

    pub fn handleSurfaceLeave(self: *Window, wl_out: *wl.Output) void {
        for (self.outputs.items) |out| {
            if (out.wl_output == wl_out) {
                self.tracker.leaveOutput(out.name);
                return;
            }
        }
    }
};

pub const Connection = struct {
@@ -335,9 +361,13 @@ pub const Connection = struct {
            .surface = try compositor.createSurface(),
            .xdg_surface = undefined,
            .xdg_toplevel = undefined,
            .tracker = &self.scale_tracker,
            .outputs = &self.outputs,
        };
        errdefer window.surface.destroy();

        window.surface.setListener(*Window, surfaceListener, window);

        window.xdg_surface = try wm_base.getXdgSurface(window.surface);
        errdefer window.xdg_surface.destroy();

@@ -519,6 +549,23 @@ fn xdgSurfaceListener(surface: *xdg.Surface, event: xdg.Surface.Event, window: *
    }
}

fn surfaceListener(_: *wl.Surface, event: wl.Surface.Event, window: *Window) void {
    switch (event) {
        .enter => |e| {
            const wl_out = e.output orelse return;
            window.handleSurfaceEnter(wl_out);
            window.scale_generation += 1;
        },
        .leave => |e| {
            const wl_out = e.output orelse return;
            window.handleSurfaceLeave(wl_out);
            window.scale_generation += 1;
        },
        .preferred_buffer_scale => {},
        .preferred_buffer_transform => {},
    }
}

fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void {
    switch (event) {
        .configure => |cfg| {
@@ -655,3 +702,20 @@ test "drainSelectionPipeThenRoundtrip drains large payload before roundtrip" {
    try std.testing.expect(roundtrip_called);
    try std.testing.expectEqualStrings(payload, text);
}

test "Window.bufferScale reflects ScaleTracker entered outputs" {
    var tracker = ScaleTracker.init(std.testing.allocator);
    defer tracker.deinit();

    try tracker.addOutput(1);
    try tracker.addOutput(2);
    tracker.setOutputScale(1, 1);
    tracker.setOutputScale(2, 2);

    // Simulate the bits Window.bufferScale delegates to.
    try tracker.enterOutput(2);
    try std.testing.expectEqual(@as(i32, 2), tracker.bufferScale());

    tracker.leaveOutput(2);
    try std.testing.expectEqual(@as(i32, 1), tracker.bufferScale());
}