// SPDX-FileCopyrightText: © 2026 mtmn // // SPDX-License-Identifier: BSD-3-Clause // Analyzer does not resolve method-call syntax to free functions. // Module type aliases use PascalCase per Zig convention. const Output = @This(); const std = @import("std"); const wl = @import("wayland").client.wl; const river = @import("wayland").client.river; const Layout = @import("Layout.zig"); const Osd = @import("Osd.zig"); const gpa = std.heap.c_allocator; const log = std.log.scoped(.laguna); object: *river.OutputV1, registry: *wl.Registry, layer_output: ?*river.LayerShellOutputV1 = null, /// Bound only to learn the connector name, which focus-output and /// send-to-output accept as a target. wl_output: ?*wl.Output = null, connector: ?[]const u8 = null, /// Integer wl_output scale. The tag OSD redraws when it changes. scale: i32 = 1, /// Set by WindowManager so the wl_output scale event can reach the OSD. osd: ?*Osd = null, x: i32 = 0, y: i32 = 0, width: i32 = 1, height: i32 = 1, removed: bool = false, tags: u32 = 1, previous_tags: u32 = 1, attach_mode: ?Layout.Attach = null, usable: Layout.Rect = .{ .x = 0, .y = 0, .width = 1, .height = 1 }, usable_valid: bool = false, main_count: u32 = 1, main_ratio: f32 = 0.6, main_location: Layout.Location = .left, /// Last presentation mode sent. river starts every output in vsync mode. presentation_sent: river.OutputV1.PresentationMode = .vsync, pub fn init(output: *Output, object: *river.OutputV1, registry: *wl.Registry) void { output.* = .{ .object = object, .registry = registry }; object.setListener(*Output, handleEvent, output); } pub fn bindLayerShell(output: *Output, layer_shell: *river.LayerShellV1) void { if (output.layer_output != null) return; const layer_output = layer_shell.getOutput(output.object) catch return; output.layer_output = layer_output; layer_output.setListener(*Output, handleLayerEvent, output); } pub fn deinit(output: *Output) void { if (output.layer_output) |layer_output| { layer_output.destroy(); output.layer_output = null; } if (output.wl_output) |wl_output| { wl_output.release(); output.wl_output = null; } if (output.connector) |connector| gpa.free(connector); output.connector = null; output.object.destroy(); } /// river sends the matching wl_output global name exactly once, after the /// global was announced. river's wlroots advertises wl_output version 4, /// the first version with the name event. fn bindWlOutput(output: *Output, global_name: u32) void { if (output.wl_output != null) return; const wl_output = output.registry.bind(global_name, wl.Output, 4) catch { log.err("cannot bind wl_output", .{}); return; }; output.wl_output = wl_output; wl_output.setListener(*Output, handleWlOutputEvent, output); } fn handleWlOutputEvent(_: *wl.Output, event: wl.Output.Event, output: *Output) void { switch (event) { .name => |named| { if (output.connector) |old| gpa.free(old); output.connector = gpa.dupe(u8, std.mem.sliceTo(named.name, 0)) catch |err| blk: { log.warn("cannot store output connector name: {s}", .{@errorName(err)}); break :blk null; }; }, .scale => |scale| { if (scale.factor == output.scale) return; output.scale = scale.factor; if (output.osd) |osd| osd.rescale(output); }, .geometry, .mode, .done, .description => {}, } } fn handleLayerEvent( _: *river.LayerShellOutputV1, event: river.LayerShellOutputV1.Event, output: *Output, ) void { switch (event) { .non_exclusive_area => |area| { output.usable = .{ .x = area.x, .y = area.y, .width = @max(area.width, 0), .height = @max(area.height, 0), }; output.usable_valid = true; }, } } /// Send the presentation mode unless the output already has it. Rendering /// state: call only inside a manage or render sequence. pub fn setPresentationMode(output: *Output, mode: river.OutputV1.PresentationMode) void { if (output.presentation_sent == mode) return; if (output.object.getVersion() < river.OutputV1.set_presentation_mode_since_version) return; output.object.setPresentationMode(mode); output.presentation_sent = mode; } fn handleEvent(_: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void { switch (event) { .removed => output.removed = true, .wl_output => |named| output.bindWlOutput(named.name), .position => |position| { output.x = position.x; output.y = position.y; }, .dimensions => |dimensions| { output.width = @max(dimensions.width, 1); output.height = @max(dimensions.height, 1); }, .capture_sessions => {}, } }