From 5183deb9aedaec574137fce797160485d377377b Mon Sep 17 00:00:00 2001 From: Squibid Date: Tue, 14 Apr 2026 20:56:23 -0400 Subject: [PATCH 01/10] set and get output state in one go --- runtime/master.lua | 2 +- src/View.zig | 7 +- src/lua/Output.zig | 238 +++++++++++++++------------------------------ 3 files changed, 86 insertions(+), 161 deletions(-) diff --git a/runtime/master.lua b/runtime/master.lua index 84748de..2e27f64 100644 --- a/runtime/master.lua +++ b/runtime/master.lua @@ -91,7 +91,7 @@ local default_config = { M.tile_tag = function(tag_id) local tag = M.state.tags[tag_id] - local area = mez.output.get_available_area(0) + local area = mez.output.get_state(0).available_area if tag.master == nil then return end diff --git a/src/View.zig b/src/View.zig index e1c485e..80e7e55 100644 --- a/src/View.zig +++ b/src/View.zig @@ -283,9 +283,10 @@ fn handleUnmap(listener: *wl.Listener(void)) void { server.events.exec("ViewUnmapPre", .{view.id}); - if (server.getDefaultSeat().focused_surface) |fs| { - if (fs == .view and fs.view == view) { - server.getDefaultSeat().focusSurface(null); + var iter = server.seats.iterator(.forward); + while (iter.next()) |seat| { + if (seat.focused_surface) |fs| { + if (fs == .view and fs.view == view) seat.focusSurface(null); } } diff --git a/src/lua/Output.zig b/src/lua/Output.zig index d93e12b..0bede7e 100644 --- a/src/lua/Output.zig +++ b/src/lua/Output.zig @@ -4,11 +4,21 @@ const zlua = @import("zlua"); const Output = @import("../Output.zig"); const LuaUtils = @import("LuaUtils.zig"); +const Utils = @import("../Utils.zig"); const Seat = @import("Seat.zig"); const server = &@import("../main.zig").server; const wlr = @import("wlroots"); +const wl = @import("wayland").server.wl; const posix = std.posix; +const gpa = std.heap.c_allocator; + +const Mode = struct { + width: i32, + height: i32, + refresh: i32, + preferred: bool, +}; fn output_id_err(L: *zlua.Lua) noreturn { L.raiseErrorStr("The output id must be >= 0 and < inf", .{}); @@ -56,96 +66,66 @@ pub fn get_focused_id(L: *zlua.Lua) i32 { return 1; } -/// ---Get refresh rate for the output +const get_output_state = struct { + rate: i32, + scale: f32, + resolution: wlr.Box, + available_area: wlr.Box, + transform: [:0]const u8, + make: [:0]const u8, + serial: [:0]const u8, + model: [:0]const u8, + description: [:0]const u8, + name: [:0]const u8, + modes: []Mode, +}; + +/// ---Get the state of an output /// ---@param output_id integer 0 maps to focused output -/// ---@return integer? -pub fn get_rate(L: *zlua.Lua) i32 { +/// ---@return get_output_state? +pub fn get_state(L: *zlua.Lua) i32 { const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); if (output) |o| { - L.pushInteger(@intCast(o.wlr_output.refresh)); - return 1; - } - - L.pushNil(); - return 1; -} - -/// ---Set the scale for the output -/// ---@param output_id integer 0 maps to focused output -/// ---@param scale number -pub fn set_scale(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - - if (output) |o| { - var state: wlr.Output.State = .init(); - defer state.finish(); - - // We don't allow scales below 0 - const new_scale: f32 = @floatCast(L.checkNumber(2)); - state.setScale(if (new_scale <= 0) o.wlr_output.scale else new_scale); - _ = o.wlr_output.commitState(&state); - - o.arrangeLayers(); - } - - return 0; -} - -/// ---Get the scale for the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return number? scale -pub fn get_scale(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - - if (output) |o| { - L.pushNumber(o.wlr_output.scale); - return 1; - } - - return 0; -} - -/// ---Get resolution in pixels of the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return { width: integer, height: integer }? -pub fn get_resolution(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); - - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - if (output) |o| { - L.newTable(); - - L.pushInteger(@intCast(o.wlr_output.width)); - L.setField(-2, "width"); - - L.pushInteger(@intCast(o.wlr_output.height)); - L.setField(-2, "height"); - - return 1; - } - - L.pushNil(); - return 1; -} - -/// ---Get the serial for the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return string? -pub fn get_serial(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); - - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - if (output) |o| { - if (o.wlr_output.serial == null) { + const output_layout = server.root.output_layout.get(o.wlr_output) orelse { L.pushNil(); return 1; - } + }; - _ = L.pushString(std.mem.span(o.wlr_output.serial.?)); + const modes: []Mode = gpa.alloc(Mode, o.wlr_output.modes.length()) catch Utils.oomPanic(); + defer gpa.free(modes); + var iter = o.wlr_output.modes.iterator(.forward); + + L.pushAny(get_output_state { + .scale = o.wlr_output.scale, + .resolution = .{ + .width = o.wlr_output.width, + .height = o.wlr_output.height, + .x = output_layout.x, + .y = output_layout.y, + }, + .rate = o.wlr_output.refresh, + .available_area = o.non_exclusive_area, + .transform = @tagName(o.wlr_output.transform), + .make = std.mem.span(o.wlr_output.make orelse "(null)"), + .serial = std.mem.span(o.wlr_output.serial orelse "(null)"), + .model = std.mem.span(o.wlr_output.model orelse "(null)"), + .description = std.mem.span(o.wlr_output.description orelse "(null)"), + .name = std.mem.span(o.wlr_output.name), + .modes = blk: { + var i: u32 = 0; // I wonder how many modes a display can have + while (iter.next()) |mode| : (i += 1) { + modes[i] = Mode{ + .width = mode.width, + .height = mode.height, + .refresh = mode.refresh, + .preferred = mode.preferred, + }; + } + break: blk modes; + }, + }) catch unreachable; return 1; } @@ -153,98 +133,42 @@ pub fn get_serial(L: *zlua.Lua) i32 { return 1; } -/// ---Get the make for the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return string? -pub fn get_make(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); - - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - if (output) |o| { - if (o.wlr_output.make == null) { - L.pushNil(); - return 1; - } - - _ = L.pushString(std.mem.span(o.wlr_output.make.?)); - return 1; - } +/// all setter data is optional +const set_output_state = struct { + position: ?wlr.Box, // TODO(squibid): impl + scale: ?f32, + transform: ?wl.Output.Transform, + mode: ?Mode, +}; - L.pushNil(); - return 1; -} - -/// ---Get the model for the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return string? -pub fn get_model(L: *zlua.Lua) i32 { +pub fn set_state(L: *zlua.Lua) i32 { const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); if (output) |o| { - if (o.wlr_output.model == null) { - L.pushNil(); - return 1; - } + const lua_state = L.toAny(set_output_state, 2) catch unreachable; - _ = L.pushString(std.mem.span(o.wlr_output.model.?)); - return 1; - } + var new_state: wlr.Output.State = .init(); + defer new_state.finish(); - L.pushNil(); - return 1; -} - -/// ---Get the description for the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return string? -pub fn get_description(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); + if (lua_state.scale) |v| new_state.setScale(if (v <= 0) o.wlr_output.scale else v); + if (lua_state.transform) |v| new_state.setTransform(v); + if (lua_state.mode) |v| new_state.setCustomMode(v.width, v.height, v.refresh); - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - if (output) |o| { - if (o.wlr_output.description == null) { - L.pushNil(); - return 1; + if (!o.wlr_output.testState(&new_state)) { + L.raiseErrorStr("Output state is not usable! `{any}`", .{ new_state }); } - _ = L.pushString(std.mem.span(o.wlr_output.description.?)); - return 1; - } - - L.pushNil(); - return 1; -} - -/// ---Get the name of the output -/// ---@param output_id integer 0 maps to focused output -/// ---@return string -pub fn get_name(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); + _ = o.wlr_output.commitState(&new_state); - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - if (output) |o| { - _ = L.pushString(std.mem.span(o.wlr_output.name)); - return 1; + o.arrangeLayers(); + return 0; } L.pushNil(); return 1; } -/// ---Get the space not exclusively occupied -/// ---@param output_id integer 0 maps to focused output -/// ---@return Box? -pub fn get_available_area(L: *zlua.Lua) i32 { - const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(1)) catch output_id_err(L); - const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); - - if (output == null) return 0; - - L.pushAny(output.?.non_exclusive_area) catch unreachable; - return 1; -} - /// ---Get the id of the output's fullscreened view if it exists /// ---@param output_id integer 0 maps to focused output /// ---@return integer? From 908d0b44ed82627dc307895e3772d426f5805ee9 Mon Sep 17 00:00:00 2001 From: Squibid Date: Thu, 23 Apr 2026 23:08:48 -0400 Subject: [PATCH 02/10] fix output state setting till we update zlua --- src/lua/Output.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lua/Output.zig b/src/lua/Output.zig index f1f9d0c..38e9666 100644 --- a/src/lua/Output.zig +++ b/src/lua/Output.zig @@ -170,12 +170,13 @@ pub fn get_state(L: *zlua.Lua) i32 { return 1; } +// TODO: remove the default values once we switch to a commit after 22cad3a /// all setter data is optional const set_output_state = struct { - position: ?wlr.Box, // TODO(squibid): impl - scale: ?f32, - transform: ?wl.Output.Transform, - mode: ?Mode, + position: ?wlr.Box = null, // TODO(squibid): impl + scale: ?f32 = null, + transform: ?wl.Output.Transform = null, + mode: ?Mode = null, }; pub fn set_state(L: *zlua.Lua) i32 { From 113827a88621acec752dd13a40bc4ab3c62e3b50 Mon Sep 17 00:00:00 2001 From: Squibid Date: Mon, 27 Apr 2026 17:35:36 -0400 Subject: [PATCH 03/10] set the position of the output --- src/lua/Output.zig | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/lua/Output.zig b/src/lua/Output.zig index 38e9666..08cd67e 100644 --- a/src/lua/Output.zig +++ b/src/lua/Output.zig @@ -106,8 +106,9 @@ pub fn get_views(L: *zlua.Lua) i32 { const get_output_state = struct { rate: i32, scale: f32, - resolution: wlr.Box, + resolution: struct { width: c_int = 0, height: c_int = 0 }, available_area: wlr.Box, + position: struct { x: c_int, y: c_int }, transform: [:0]const u8, make: [:0]const u8, serial: [:0]const u8, @@ -125,11 +126,7 @@ pub fn get_state(L: *zlua.Lua) i32 { const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); if (output) |o| { - const output_layout = server.root.output_layout.get(o.wlr_output) orelse { - L.pushNil(); - return 1; - }; - + const output_layout = server.root.output_layout.get(o.wlr_output); const modes: []Mode = gpa.alloc(Mode, o.wlr_output.modes.length()) catch Utils.oomPanic(); defer gpa.free(modes); var iter = o.wlr_output.modes.iterator(.forward); @@ -139,8 +136,10 @@ pub fn get_state(L: *zlua.Lua) i32 { .resolution = .{ .width = o.wlr_output.width, .height = o.wlr_output.height, - .x = output_layout.x, - .y = output_layout.y, + }, + .position = .{ + .x = if (output_layout) |l| l.x else 0, + .y = if (output_layout) |l| l.y else 0, }, .rate = o.wlr_output.refresh, .available_area = o.non_exclusive_area, @@ -173,7 +172,7 @@ pub fn get_state(L: *zlua.Lua) i32 { // TODO: remove the default values once we switch to a commit after 22cad3a /// all setter data is optional const set_output_state = struct { - position: ?wlr.Box = null, // TODO(squibid): impl + position: ?struct { x: c_int = 0, y: c_int = 0 } = null, scale: ?f32 = null, transform: ?wl.Output.Transform = null, mode: ?Mode = null, @@ -184,7 +183,9 @@ pub fn set_state(L: *zlua.Lua) i32 { const output: ?*Output = if (output_id == 0) server.getDefaultSeat().focused_output else server.root.outputById(output_id); if (output) |o| { - const lua_state = L.toAny(set_output_state, 2) catch unreachable; + const lua_state = L.toAny(set_output_state, 2) catch |err| { + L.raiseErrorStr("Output state is not usable! `%s`", .{ @errorName(err).ptr }); + }; var new_state: wlr.Output.State = .init(); defer new_state.finish(); @@ -192,9 +193,18 @@ pub fn set_state(L: *zlua.Lua) i32 { if (lua_state.scale) |v| new_state.setScale(if (v <= 0) o.wlr_output.scale else v); if (lua_state.transform) |v| new_state.setTransform(v); if (lua_state.mode) |v| new_state.setCustomMode(v.width, v.height, v.refresh); + // TEST: this, make sure it actually changes where the output is placed + // if it doesn't we'll have to remove the output from the layout and + // then add it back with the new position. + if (lua_state.position) |v| { + if (server.root.output_layout.get(o.wlr_output)) |layout| { + layout.x = v.x; + layout.y = v.y; + } + } if (!o.wlr_output.testState(&new_state)) { - L.raiseErrorStr("Output state is not usable! `{any}`", .{ new_state }); + L.raiseErrorStr("Output state is not usable!", .{}); } _ = o.wlr_output.commitState(&new_state); From 6bd3f35f392d44033dcc837ad69b0af1e45b6378 Mon Sep 17 00:00:00 2001 From: Squibid Date: Mon, 11 May 2026 18:53:02 -0400 Subject: [PATCH 04/10] wip --- src/Output.zig | 4 ---- src/Root.zig | 2 -- src/Server.zig | 13 +++++++++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Output.zig b/src/Output.zig index aa4902d..1213956 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -92,10 +92,6 @@ pub fn init(wlr_output: *wlr.Output) ?*Output { } // TODO: Allow user to define output positions - const layout_output = try server.root.output_layout.addAuto(self.wlr_output); - server.root.scene_output_layout.addOutput(layout_output, self.scene_output); - self.arrangeLayers(); - self.setFocused(); self.wlr_output.data = self; diff --git a/src/Root.zig b/src/Root.zig index d43afab..9a526ad 100644 --- a/src/Root.zig +++ b/src/Root.zig @@ -18,7 +18,6 @@ const Utils = @import("Utils.zig"); scene_node_data: SceneNode.Data, scene: *wlr.Scene, -scene_output_layout: *wlr.SceneOutputLayout, output_layout: *wlr.OutputLayout, output_manager: *wlr.OutputManagerV1, output_power_manager: *wlr.OutputPowerManagerV1, @@ -45,7 +44,6 @@ pub fn init(self: *Root) void { .output_manager = try wlr.OutputManagerV1.create(server.wl_server), .output_power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server), .output_layout = output_layout, - .scene_output_layout = try scene.attachOutputLayout(output_layout), }; if (server.linux_dmabuf) |dmabuf| self.scene.setLinuxDmabufV1(dmabuf); diff --git a/src/Server.zig b/src/Server.zig index 7ccb698..491d3ab 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -286,8 +286,17 @@ fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), device: *wlr.InputDe }); } -fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void { - _ = Output.init(wlr_output); +fn handleNewOutput(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void { + const self: *Server = @fieldParentPtr("new_output", listener); + const output = Output.init(wlr_output) orelse { + std.log.err("Failed to create new output", .{}); + return; + }; + + // TODO: Allow user to define output positions + _ = self.root.output_layout.addAuto(output.wlr_output) catch { + std.log.err("failed to add output to the output layout :(", .{}); + }; } fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void { From ca6e5b8314d54870570f83998aef1b6816be6ab4 Mon Sep 17 00:00:00 2001 From: Squibid Date: Tue, 12 May 2026 13:42:38 -0400 Subject: [PATCH 05/10] make the scene node iterator do correct recusion --- src/SceneNode.zig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/SceneNode.zig b/src/SceneNode.zig index 33c788f..642208c 100644 --- a/src/SceneNode.zig +++ b/src/SceneNode.zig @@ -21,8 +21,12 @@ pub fn Iterator(comptime direction: wl.list.Direction) type { node_iter: wl.list.Head(wlr.SceneNode, .link).Iterator(direction), i: u32, pub fn next(self: *@This()) ?*Data { - std.debug.assert(@intFromPtr(self.trees[self.i]) != 0); - if (self.trees[self.i].children.length() == 0) return self.next(); + if (self.i >= self.trees.len) return null; + self.node_iter = self.trees[self.i].children.iterator(direction); + if (self.trees[self.i].children.length() == 0) { + self.i += 1; + return self.next(); + } while (self.node_iter.next()) |node| { if (node.data == null) continue; @@ -30,9 +34,6 @@ pub fn Iterator(comptime direction: wl.list.Direction) type { } self.i += 1; - if (self.i >= self.trees.len) return null; - - self.node_iter = self.trees[self.i].children.iterator(direction); return self.next(); } }; From 506d4dd9dedf7b9c39ad2f933bdba7cdbace7423 Mon Sep 17 00:00:00 2001 From: Squibid Date: Tue, 12 May 2026 14:23:35 -0400 Subject: [PATCH 06/10] refactor the output state and focus status out of the output struct --- src/Output.zig | 30 ++++++++---------------------- src/Seat.zig | 4 ---- src/Server.zig | 4 ++++ src/lua/View.zig | 2 +- 4 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/Output.zig b/src/Output.zig index 1213956..970e951 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -25,12 +25,10 @@ const Layers = struct { overlay: *wlr.SceneTree, }; -focused: bool, id: u64, fullscreens: std.ArrayList(*View), wlr_output: *wlr.Output, -state: wlr.Output.State, tree: *wlr.SceneTree, scene_node_data: SceneNodeData, scene_output: *wlr.SceneOutput, @@ -49,7 +47,6 @@ pub fn init(wlr_output: *wlr.Output) ?*Output { const self = try gpa.create(Output); self.* = .{ - .focused = false, .id = @intFromPtr(wlr_output), .wlr_output = wlr_output, .tree = try server.root.scene.tree.createSceneTree(), @@ -66,7 +63,6 @@ pub fn init(wlr_output: *wlr.Output) ?*Output { .scene_output = try server.root.scene.createSceneOutput(wlr_output), .scene_node_data = SceneNodeData{ .output = self }, - .state = wlr.Output.State.init() }; wlr_output.events.frame.add(&self.frame); @@ -80,23 +76,20 @@ pub fn init(wlr_output: *wlr.Output) ?*Output { return null; } - self.state.setEnabled(true); + self.wlr_output.data = self; + self.tree.node.data = &self.scene_node_data; - if (wlr_output.preferredMode()) |mode| { - self.state.setMode(mode); - } + var state = wlr.Output.State.init(); + defer state.finish(); - if (!wlr_output.commitState(&self.state)) { + state.setEnabled(true); + if (wlr_output.preferredMode()) |mode| state.setMode(mode); + + if (!wlr_output.commitState(&state)) { std.log.err("Unable to commit state to output {s}", .{wlr_output.name}); return null; } - // TODO: Allow user to define output positions - self.setFocused(); - - self.wlr_output.data = self; - self.tree.node.data = &self.scene_node_data; - server.events.exec("OutputInitPost", .{self.id}, "After a new output is initialized. You're probably looking for OutputStateChange."); return self; @@ -109,8 +102,6 @@ pub fn deinit(self: *Output) void { self.request_state.link.remove(); self.destroy.link.remove(); - self.state.finish(); - self.wlr_output.destroy(); server.events.exec("OutputDeinitPost", .{}, "After an output is de-initialized."); @@ -119,12 +110,7 @@ pub fn deinit(self: *Output) void { } pub fn setFocused(self: *Output) void { - if (server.getDefaultSeat().focused_output) |prev_output| { - prev_output.focused = false; - } - server.getDefaultSeat().focused_output = self; - self.focused = true; } const SurfaceAtResult = struct { diff --git a/src/Seat.zig b/src/Seat.zig index 3778c7d..184971d 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -169,10 +169,6 @@ pub fn focusSurface(self: *Seat, to_focus: ?FocusData) void { } pub fn focusOutput(self: *Seat, output: *Output) void { - if (self.focused_output) |prev_output| { - prev_output.focused = false; - } - self.focused_output = output; } diff --git a/src/Server.zig b/src/Server.zig index 491d3ab..6896024 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -297,6 +297,10 @@ fn handleNewOutput(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) _ = self.root.output_layout.addAuto(output.wlr_output) catch { std.log.err("failed to add output to the output layout :(", .{}); }; + + if (self.getDefaultSeat().focused_output == null) { + self.getDefaultSeat().focusOutput(output); + } } fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void { diff --git a/src/lua/View.zig b/src/lua/View.zig index 3d5b9ea..0a0f624 100644 --- a/src/lua/View.zig +++ b/src/lua/View.zig @@ -32,7 +32,7 @@ pub fn get_all_ids(L: *zlua.Lua) i32 { } const output: *Output = @ptrCast(@alignCast(o.output.data.?)); - if (!output.state.enabled) continue; + if (!output.wlr_output.enabled) continue; // Only search the content and fullscreen layers for views var iter = SceneNode.iterator(@constCast(&[_]*wlr.SceneTree{ From 3f1134e79d0632bbae4e139359038057ef44e717 Mon Sep 17 00:00:00 2001 From: Squibid Date: Sat, 16 May 2026 17:49:40 -0400 Subject: [PATCH 07/10] better typing in the lua output api states --- src/lua/Output.zig | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/lua/Output.zig b/src/lua/Output.zig index 78f6bbb..f7324f8 100644 --- a/src/lua/Output.zig +++ b/src/lua/Output.zig @@ -17,7 +17,7 @@ const gpa = std.heap.c_allocator; const Mode = struct { width: i32, height: i32, - refresh: i32, + refresh: f64, preferred: bool, }; @@ -98,16 +98,16 @@ pub fn get_views(L: *zlua.Lua) i32 { } const get_output_state = struct { - rate: i32, + refresh: f64, scale: f32, resolution: struct { width: c_int = 0, height: c_int = 0 }, available_area: wlr.Box, position: struct { x: c_int, y: c_int }, transform: [:0]const u8, - make: [:0]const u8, - serial: [:0]const u8, - model: [:0]const u8, - description: [:0]const u8, + make: ?[:0]const u8, + serial: ?[:0]const u8, + model: ?[:0]const u8, + description: ?[:0]const u8, name: [:0]const u8, modes: []Mode, }; @@ -135,13 +135,13 @@ pub fn get_state(L: *zlua.Lua) i32 { .x = if (output_layout) |l| l.x else 0, .y = if (output_layout) |l| l.y else 0, }, - .rate = o.wlr_output.refresh, + .refresh = @as(f64, @floatFromInt(o.wlr_output.refresh)) / 1000.0, .available_area = o.non_exclusive_area, .transform = @tagName(o.wlr_output.transform), - .make = std.mem.span(o.wlr_output.make orelse "(null)"), - .serial = std.mem.span(o.wlr_output.serial orelse "(null)"), - .model = std.mem.span(o.wlr_output.model orelse "(null)"), - .description = std.mem.span(o.wlr_output.description orelse "(null)"), + .make = if (o.wlr_output.make) |m| std.mem.span(m) else null, + .serial = if (o.wlr_output.make) |s| std.mem.span(s) else null, + .model = if (o.wlr_output.model) |m| std.mem.span(m) else null, + .description = if (o.wlr_output.description) |d| std.mem.span(d) else null, .name = std.mem.span(o.wlr_output.name), .modes = blk: { var i: u32 = 0; // I wonder how many modes a display can have @@ -149,7 +149,7 @@ pub fn get_state(L: *zlua.Lua) i32 { modes[i] = Mode{ .width = mode.width, .height = mode.height, - .refresh = mode.refresh, + .refresh = @as(f64, @floatFromInt(mode.refresh)) / 1000.0, .preferred = mode.preferred, }; } @@ -186,10 +186,7 @@ pub fn set_state(L: *zlua.Lua) i32 { if (lua_state.scale) |v| new_state.setScale(if (v <= 0) o.wlr_output.scale else v); if (lua_state.transform) |v| new_state.setTransform(v); - if (lua_state.mode) |v| new_state.setCustomMode(v.width, v.height, v.refresh); - // TEST: this, make sure it actually changes where the output is placed - // if it doesn't we'll have to remove the output from the layout and - // then add it back with the new position. + if (lua_state.mode) |v| new_state.setCustomMode(v.width, v.height, @intFromFloat(v.refresh * 1000)); if (lua_state.position) |v| { if (server.root.output_layout.get(o.wlr_output)) |layout| { layout.x = v.x; @@ -197,11 +194,13 @@ pub fn set_state(L: *zlua.Lua) i32 { } } - if (!o.wlr_output.testState(&new_state)) { + if (!o.wlr_output.testState(&new_state) + or !o.wlr_output.commitState(&new_state)) { L.raiseErrorStr("Output state is not usable!", .{}); } - _ = o.wlr_output.commitState(&new_state); + // keep the output manager in sync with the output + server.root.configureOutputs(); o.arrangeLayers(); return 0; From 6066a5b006ea213687987a85e7cc2c7a5f5e7f67 Mon Sep 17 00:00:00 2001 From: Squibid Date: Sat, 16 May 2026 17:50:17 -0400 Subject: [PATCH 08/10] add focusing to the lua output api --- src/lua/Output.zig | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lua/Output.zig b/src/lua/Output.zig index f7324f8..58c31d3 100644 --- a/src/lua/Output.zig +++ b/src/lua/Output.zig @@ -67,6 +67,26 @@ pub fn get_focused_id(L: *zlua.Lua) i32 { return 1; } +/// ---Remove focus from current output, and set to given id +/// ---@param seat_id integer Id of the seat to be focused, 0 for default seat +/// ---@param output_id integer? Id of the output to be focused +pub fn set_focused(L: *zlua.Lua) i32 { + const seat = if (!L.isNil(1)) blk: { + const seat_id = LuaUtils.coerceInteger(u32, L.checkInteger(1)) catch Seat.seat_id_err(L); + break :blk LuaUtils.seatFromId(seat_id) orelse { + L.pushNil(); + return 1; + }; + } else server.getDefaultSeat(); + const output_id = LuaUtils.coerceInteger(u64, L.checkInteger(2)) catch output_id_err(L); + if (server.root.outputById(output_id)) |output| { + seat.focusOutput(output); + } + + L.pushNil(); + return 1; +} + /// Returns all the ids of views within an output /// ---@param output_id integer 0 maps to focused output /// ---@return integer[]? From c0f887abc32ae1f349ed9eee8f810f6bb18358c2 Mon Sep 17 00:00:00 2001 From: Squibid Date: Sat, 16 May 2026 17:58:22 -0400 Subject: [PATCH 09/10] rework outputs... - The output already has a tree in the scene_output it doesn't need another one. - Outputs aren't focused by default on connection anymore, that is now controlled by the server. - Output configurations (used in programs such as wlr-randr) are now configured in the root - Layers are now cropped to their outputs size - arranging the layers of an output won't affect other layers in other outputs --- src/Output.zig | 112 ++++++++++++++++++++++--------------------------- src/Root.zig | 20 +++++++++ src/Server.zig | 13 +++++- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/src/Output.zig b/src/Output.zig index 970e951..f59db30 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -8,6 +8,7 @@ const std = @import("std"); const Utils = @import("Utils.zig"); const Server = @import("Server.zig"); +const Root = @import("Root.zig"); const View = @import("View.zig"); const LayerSurface = @import("LayerSurface.zig"); @@ -29,9 +30,8 @@ id: u64, fullscreens: std.ArrayList(*View), wlr_output: *wlr.Output, -tree: *wlr.SceneTree, -scene_node_data: SceneNodeData, scene_output: *wlr.SceneOutput, +scene_node_data: SceneNodeData, non_exclusive_area: wlr.Box, layers: Layers, @@ -44,50 +44,46 @@ destroy: wl.Listener(*wlr.Output) = .init(handleDestroy), pub fn init(wlr_output: *wlr.Output) ?*Output { errdefer Utils.oomPanic(); + if (!wlr_output.initRender(server.allocator, server.renderer)) { + std.log.err("Unable to start output {s}", .{wlr_output.name}); + return null; + } + const self = try gpa.create(Output); + errdefer self.deinit(); self.* = .{ .id = @intFromPtr(wlr_output), .wlr_output = wlr_output, - .tree = try server.root.scene.tree.createSceneTree(), .fullscreens = std.ArrayList(*View).initCapacity(gpa, 8) catch Utils.oomPanic(), .non_exclusive_area = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, + .scene_output = try server.root.scene.createSceneOutput(wlr_output), + .scene_node_data = SceneNodeData{ .output = self }, .layers = .{ - .background = try self.tree.createSceneTree(), - .bottom = try self.tree.createSceneTree(), - .content = try self.tree.createSceneTree(), - .top = try self.tree.createSceneTree(), - .overlay = try self.tree.createSceneTree(), + .background = try self.scene_output.scene.tree.createSceneTree(), + .bottom = try self.scene_output.scene.tree.createSceneTree(), + .content = try self.scene_output.scene.tree.createSceneTree(), + .top = try self.scene_output.scene.tree.createSceneTree(), + .overlay = try self.scene_output.scene.tree.createSceneTree(), }, - - .scene_output = try server.root.scene.createSceneOutput(wlr_output), - .scene_node_data = SceneNodeData{ .output = self }, }; + self.wlr_output.data = self; + wlr_output.events.frame.add(&self.frame); - wlr_output.events.request_state.add(&self.request_state); wlr_output.events.destroy.add(&self.destroy); - - errdefer deinit(self); - - if (!wlr_output.initRender(server.allocator, server.renderer)) { - std.log.err("Unable to start output {s}", .{wlr_output.name}); - return null; - } - - self.wlr_output.data = self; - self.tree.node.data = &self.scene_node_data; + wlr_output.events.request_state.add(&self.request_state); var state = wlr.Output.State.init(); defer state.finish(); - state.setEnabled(true); if (wlr_output.preferredMode()) |mode| state.setMode(mode); + state.setEnabled(true); + if (!wlr_output.commitState(&state)) { - std.log.err("Unable to commit state to output {s}", .{wlr_output.name}); - return null; + std.log.err("Unable to commit state to output {s}", .{ wlr_output.name }); } server.events.exec("OutputInitPost", .{self.id}, "After a new output is initialized. You're probably looking for OutputStateChange."); @@ -101,12 +97,11 @@ pub fn deinit(self: *Output) void { self.frame.link.remove(); self.request_state.link.remove(); self.destroy.link.remove(); - self.wlr_output.destroy(); - server.events.exec("OutputDeinitPost", .{}, "After an output is de-initialized."); - gpa.destroy(self); + + server.events.exec("OutputDeinitPost", .{}, "After an output is de-initialized."); } pub fn setFocused(self: *Output) void { @@ -187,49 +182,36 @@ fn handleRequestState( listener: *wl.Listener(*wlr.Output.event.RequestState), event: *wlr.Output.event.RequestState, ) void { - const output: *Output = @fieldParentPtr("request_state", listener); + const self: *Output = @fieldParentPtr("request_state", listener); - if (!output.wlr_output.commitState(event.state)) { + if (!self.wlr_output.commitState(event.state)) { std.log.warn("failed to set output state {}", .{event.state}); // nothing should've changed, so we don't do anything return; } - // update the config with all monitors and send it to the output_manager - const config = wlr.OutputConfigurationV1.create() catch Utils.oomPanic(); - var iter = server.root.scene.outputs.iterator(.forward); - while (iter.next()) |out| { - _ = wlr.OutputConfigurationV1.Head.create(config, out.output) catch Utils.oomPanic(); - } - server.root.output_manager.setConfiguration(config); - - // make sure the layers are behaving - arrangeLayers(output); + Root.configureOutputs(&server.root); + self.arrangeLayers(); - server.events.exec("OutputStateChange", .{output.id}, "After an outputs state has been changed."); + server.events.exec("OutputStateChange", .{self.id}, "After an outputs state has been changed."); } -fn handleFrame(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void { - const scene_output = server.root.scene.getSceneOutput(wlr_output); +fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { + const self: *Output = @fieldParentPtr("frame", listener); - if (scene_output == null) { - std.log.err("Unable to get scene output to render", .{}); - return; + if (!self.scene_output.commit(null)) { + std.log.warn("setting output state failed for output: {}", .{ self.id }); } - // std.log.info("Rendering commited scene output\n", .{}); - _ = scene_output.?.commit(null); - - var now = posix.clock_gettime(posix.CLOCK.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported"); - scene_output.?.sendFrameDone(&now); + var now = posix.clock_gettime(posix.CLOCK.MONOTONIC) catch { + std.debug.panic("CLOCK_MONOTONIC not supported", .{}); + }; + self.scene_output.sendFrameDone(&now); } fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { - std.log.debug("Handling destroy", .{}); const output: *Output = @fieldParentPtr("destroy", listener); - std.log.debug("removing output: {s}", .{output.wlr_output.name}); - output.frame.link.remove(); output.request_state.link.remove(); output.destroy.link.remove(); @@ -251,12 +233,10 @@ pub fn arrangeLayers(self: *Output) void { inline for (@typeInfo(zwlr.LayerShellV1.Layer).@"enum".fields) |comptime_layer| { const layer: *wlr.SceneTree = @field(self.layers, comptime_layer.name); - var it = layer.children.safeIterator(.forward); + var it = layer.children.iterator(.forward); while (it.next()) |node| { if (node.data == null) continue; - - // if (@as(?*SceneNodeData, @alignCast(@ptrCast(node.data)))) |node_data| { const scene_node_data: *SceneNodeData = @ptrCast(@alignCast(node.data.?)); const layer_surface: *LayerSurface = switch (scene_node_data.*) { @@ -264,6 +244,7 @@ pub fn arrangeLayers(self: *Output) void { else => continue, }; + if (layer_surface.output.wlr_output != self.wlr_output) continue; if (!layer_surface.wlr_layer_surface.initialized) continue; // TEST: river seems to try and prevent clients from taking an @@ -273,13 +254,20 @@ pub fn arrangeLayers(self: *Output) void { layer_surface.scene_layer_surface.configure( &full_box, - &self.non_exclusive_area, + &self.non_exclusive_area ); - // TEST: are these calls useless? - // const x = layer_surface.scene_layer_surface.tree.node.x; - // const y = layer_surface.scene_layer_surface.tree.node.y; - // layer_surface.scene_layer_surface.tree.node.setPosition(x, y); + // set the position of the new layersurface relative to the output + // it belongs to + const x = layer_surface.output.scene_output.x; + const y = layer_surface.output.scene_output.y; + layer_surface.scene_layer_surface.tree.node.setPosition(x, y); + layer_surface.scene_layer_surface.tree.node.subsurfaceTreeSetClip(&.{ + .x = 0, + .y = 0, + .width = layer_surface.output.wlr_output.width, + .height = layer_surface.output.wlr_output.height, + }); } } } diff --git a/src/Root.zig b/src/Root.zig index 9a526ad..2840bc6 100644 --- a/src/Root.zig +++ b/src/Root.zig @@ -73,6 +73,26 @@ pub fn deinit(self: *Root) void { self.scene.tree.node.destroy(); } +pub fn configureOutputs(self: *const Root) void { + // update the config with all monitors and send it to the output_manager + const config = wlr.OutputConfigurationV1.create() catch Utils.oomPanic(); + + // TODO: do we ommit disabled monitors here? + var iter = self.scene.outputs.iterator(.forward); + while (iter.next()) |scene_output| { + const config_head = wlr.OutputConfigurationV1.Head.create(config, scene_output.output) catch Utils.oomPanic(); + + if (self.output_layout.get(scene_output.output)) |_| { + _ = self.output_layout.addAuto(scene_output.output) catch Utils.oomPanic(); + } + + config_head.state.x = scene_output.x; + config_head.state.y = scene_output.y; + } + + self.output_manager.setConfiguration(config); +} + // Search output_layout's outputs, and each outputs views pub fn viewById(self: *Root, id: u64) ?*View { var output_it = self.output_layout.outputs.iterator(.forward); diff --git a/src/Server.zig b/src/Server.zig index 6896024..19988a8 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -294,13 +294,22 @@ fn handleNewOutput(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) }; // TODO: Allow user to define output positions - _ = self.root.output_layout.addAuto(output.wlr_output) catch { - std.log.err("failed to add output to the output layout :(", .{}); + const layout_output = self.root.output_layout.addAuto(output.wlr_output) catch { + std.log.err("failed to add output to the output layout", .{}); + return; }; + output.scene_output.setPosition(layout_output.x, layout_output.y); + + // FIXME: without this the lua api can crash mez very easily. Thankfully we + // don't have a case for not having any output selected, but it'd still be + // better if we didn't crash. if (self.getDefaultSeat().focused_output == null) { self.getDefaultSeat().focusOutput(output); } + + Root.configureOutputs(&self.root); + output.arrangeLayers(); } fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void { From 7b1364eb81121417a91e45eecedf31d09d7fb265 Mon Sep 17 00:00:00 2001 From: Squibid Date: Sun, 17 May 2026 12:38:54 -0400 Subject: [PATCH 10/10] add comment to test layersurface output reparenting --- src/Output.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Output.zig b/src/Output.zig index f59db30..720e931 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -244,7 +244,9 @@ pub fn arrangeLayers(self: *Output) void { else => continue, }; + // TEST: should we set the layersurface to the correct output? if (layer_surface.output.wlr_output != self.wlr_output) continue; + if (!layer_surface.wlr_layer_surface.initialized) continue; // TEST: river seems to try and prevent clients from taking an