Something went wrong. Try again.
window manager for the river wayland compositor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294// SPDX-FileCopyrightText: © 2026 mtmn//// SPDX-License-Identifier: BSD-3-Clause// Module type aliases use PascalCase per Zig convention.// zwanzig-disable: identifier-style
const Window = @This();
const std = @import("std");const river = @import("wayland").client.river;
const Output = @import("Output.zig");const WindowManager = @import("WindowManager.zig");
const gpa = std.heap.c_allocator;
/// Never null while the window lives: river ignores every request but/// destroy after the closed event, so no request needs a guard.object: *river.WindowV1,manager: *WindowManager,node: ?*river.NodeV1 = null,closed: bool = false,tags: u32 = 1,placed: bool = false,output: ?*Output = null,float: bool = false,fullscreen: bool = false,/// Fullscreen change queued by a client request, applied at the start of/// the next manage sequence where the fullscreen request is legal.pending_fullscreen: ?bool = null,focused: bool = false,app_id: ?[]const u8 = null,title: ?[]const u8 = null,width: i32 = 800,height: i32 = 600,x: i32 = 0,y: i32 = 0,/// Last tiled state sent with set_tiled and inform_maximized, to avoid/// redundant requests.tiled_sent: ?bool = null,/// Last decoration mode sent with use_csd or use_ssd, so the same request is/// not sent twice. True means CSD.csd_sent: ?bool = null,/// app_id and title events arrive outside a manage sequence, where rule/// requests are a protocol error. The event handlers set these flags and/// manageStart applies them. rules_pending re-runs every rule./// decorations_pending re-runs only csd rules.rules_pending: bool = false,decorations_pending: bool = false,/// Last border state sent to the compositor, to avoid redundant requests.rendered_border_width: i32 = -1,rendered_border_color: [4]u32 = .{ 0, 0, 0, 0 },/// river considers new windows shown until the first hide request.shown: bool = true,/// True while a size proposal from the last manage sequence is awaiting the/// compositor's confirmation. The reclaim logic uses it to tell the/// compositor echoing our own proposal apart from the client resizing/// itself.proposal_outstanding: bool = false,/// Last size sent with propose_dimensions. Each proposal makes river send a/// dimensions event and reconfigure the client, so an unchanged size is not/// proposed again. Cleared when the size changes for any other reason.proposed: ?[2]i32 = null,/// Last position sent with set_position, to avoid redundant requests.rendered_position: ?[2]i32 = null,/// Size of the tile the layout assigned, or zero when the window is not/// tiled. Content is clipped to it, so a buffer larger than the tile, from/// a client that is slow to shrink or one with a larger minimum size, never/// covers its neighbours.tile_clip: [2]i32 = .{ 0, 0 },/// Last content clip size sent, to avoid redundant requests. Zero means/// clipping is off, which is river's initial state.rendered_clip: [2]i32 = .{ 0, 0 },/// How the client prefers to be presented. Games ask for async to tear/// rather than wait for vsync. Only honoured while tearing is allowed.presentation_hint: river.OutputV1.PresentationMode = .vsync,/// Set on creation, cleared after the first manage sequence that sees/// the window. Lets manageStart give new windows focus.just_created: bool = true,/// Creation order, counting up from 1. If several windows open in one/// manage sequence, the newest takes focus wherever it is in the stack.serial: u64 = 0,
pub fn init(window: *Window, object: *river.WindowV1, manager: *WindowManager, serial: u64) void { window.* = .{ .object = object, .manager = manager, .serial = serial }; object.setListener(*Window, handleEvent, window);}
pub fn deinit(window: *Window) void { if (window.node) |node| { node.destroy(); window.node = null; } window.object.destroy(); if (window.app_id) |app_id| gpa.free(app_id); if (window.title) |title| gpa.free(title); window.app_id = null; window.title = null;}
pub fn configure(window: *Window, width: i32, height: i32) void { const size: [2]i32 = .{ @max(width, 1), @max(height, 1) }; if (window.proposed) |proposed| { if (proposed[0] == size[0] and proposed[1] == size[1]) return; } window.proposed = size; window.width = size[0]; window.height = size[1]; window.proposal_outstanding = true; window.object.proposeDimensions(size[0], size[1]);}
/// Forget the last proposed size and rendered position so both are sent/// again. river leaves them undefined after a window exits fullscreen.pub fn resendGeometry(window: *Window) void { window.proposed = null; window.rendered_position = null;}
/// A placed, tiled window is under layout control: the manager owns its/// geometry and must reclaim the tile if the client changes it.pub fn controllable(window: *const Window) bool { return !window.float and !window.fullscreen and window.placed;}
/// Decide whether a reported size means the client resized itself outside the/// tile grid. True means the manager must request a re-arrange so the next/// manage sequence re-proposes the tile geometry.////// A report that confirms a size we just proposed is the compositor echoing/// our own arrange, not a client resize, so it never triggers a reclaim. A/// window the manager does not control (floating, fullscreen or unplaced) is/// exempt: its geometry is its own to choose.pub fn shouldReclaim( proposal_outstanding: bool, is_controllable: bool, width: i32, height: i32, reported_width: i32, reported_height: i32,) bool { if (proposal_outstanding) return false; if (!is_controllable) return false; return width != reported_width or height != reported_height;}
test "off-grid client resize is reclaimed" { const testing = std.testing; // A placed tiled window whose reported size differs from the stored one // was resized by the client: reclaim it. try testing.expect(shouldReclaim(false, true, 1920, 1080, 640, 480)); try testing.expect(shouldReclaim(false, true, 1920, 1080, 1921, 1080)); try testing.expect(shouldReclaim(false, true, 1920, 1080, 1920, 1079));}
test "confirmation of our own proposal is not reclaimed" { const testing = std.testing; // proposal_outstanding means this report echoes the size we proposed. try testing.expect(!shouldReclaim(true, true, 1920, 1080, 1920, 1080));}
test "unchanged size is not reclaimed" { const testing = std.testing; try testing.expect(!shouldReclaim(false, true, 1920, 1080, 1920, 1080));}
test "uncontrolled windows are not reclaimed" { const testing = std.testing; // Floating, fullscreen or unplaced windows keep their own size. try testing.expect(!shouldReclaim(false, false, 1920, 1080, 640, 480));}
/// Stacking is left to the caller, which restacks only when the order of/// visible windows changes.pub fn render(window: *Window, border_width: i32, border_color: [4]u32) void { const node = window.node orelse return; const position: [2]i32 = .{ window.x, window.y }; const moved = if (window.rendered_position) |rendered| rendered[0] != position[0] or rendered[1] != position[1] else true; if (moved) { node.setPosition(position[0], position[1]); window.rendered_position = position; } if (window.rendered_clip[0] != window.tile_clip[0] or window.rendered_clip[1] != window.tile_clip[1]) { // Borders follow the clipped content, so they stay on the tile. if (window.object.getVersion() >= river.WindowV1.set_content_clip_box_since_version) { window.object.setContentClipBox(0, 0, window.tile_clip[0], window.tile_clip[1]); } window.rendered_clip = window.tile_clip; } if (window.rendered_border_width != border_width or !std.mem.eql(u32, &window.rendered_border_color, &border_color)) { window.object.setBorders( .{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color[0], border_color[1], border_color[2], border_color[3], ); window.rendered_border_width = border_width; window.rendered_border_color = border_color; } if (!window.shown) { window.object.show(); window.shown = true; }}
pub fn hide(window: *Window) void { if (!window.shown) return; window.object.hide(); window.shown = false;}
fn handleEvent(_: *river.WindowV1, event: river.WindowV1.Event, window: *Window) void { switch (event) { .closed => { window.closed = true; if (window.node) |node| { node.destroy(); window.node = null; } }, .dimensions => |dimensions| { const width = @max(dimensions.width, 1); const height = @max(dimensions.height, 1); const reclaim = shouldReclaim( window.proposal_outstanding, window.controllable(), window.width, window.height, width, height, ); // A size we did not propose, such as a fullscreen or // interactive resize, makes the last proposal stale. if (!window.proposal_outstanding) window.proposed = null; window.proposal_outstanding = false; window.width = width; window.height = height; if (reclaim) window.manager.requestRearrange(window); }, .app_id => |app_id| { const had_app_id = window.app_id != null; if (window.app_id) |old| gpa.free(old); window.app_id = if (app_id.app_id) |id| gpa.dupe(u8, std.mem.sliceTo(id, 0)) catch null else null; // The first manage sequence may run before the app_id arrives, // so rules evaluated then missed it. Re-apply once the identity // is known. Later changes only update decorations. Both run in // the manage sequence that follows this event. if (window.node != null) { if (!had_app_id and window.app_id != null) { window.rules_pending = true; } else { window.decorations_pending = true; } } }, .title => |title| { if (window.title) |old| gpa.free(old); window.title = if (title.title) |name| gpa.dupe(u8, std.mem.sliceTo(name, 0)) catch null else null; if (window.node != null) window.decorations_pending = true; }, .presentation_hint => |presentation| window.presentation_hint = presentation.hint, .fullscreen_requested => window.pending_fullscreen = true, .exit_fullscreen_requested => window.pending_fullscreen = false, .dimensions_hint, .parent, .decoration_hint, .pointer_move_requested, .pointer_resize_requested, .touch_move_requested, .touch_resize_requested, .show_window_menu_requested, .maximize_requested, .unmaximize_requested, .minimize_requested, .unreliable_pid, .identifier, .capture_sessions, => {}, }}