// SPDX-FileCopyrightText: 2025 Ben Buhse // // SPDX-License-Identifier: GPL-3.0-only const Window = @This(); context: *Context, river_window_v1: *river.WindowV1, river_node_v1: *river.NodeV1, app_id: ?[]const u8 = null, title: ?[]const u8 = null, parent: ?*river.WindowV1 = null, rect: utils.Rect = .{}, fullscreen: bool = false, maximized: bool = false, tags: u32 = 0x0001, output: ?*Output, floating: bool = false, floating_rect: utils.Rect = .{}, dimensions_hint: DimensionsHint = .{}, /// Whether or not the window needs its position set /// /// Right now, this is only used when windows are made floating by a rule but don't have their /// dimensions set yet. We need their dimensions to be able to center them properly, so we have /// to wait for that event. /// /// This can't be part of PendingManage because it lasts between manage cycles. needs_position: bool = true, initialized: bool = false, /// State consumed in manage() phase, reset at end of manage(). pending_manage: PendingManage = .{}, /// State consumed in render() phase, reset at end of render(). pending_render: PendingRender = .{}, /// Used to put Windows into a list when calculating the layout active_list_node: DoublyLinkedList.Node = .{}, link: wl.list.Link, pub const PendingManage = struct { dimensions: ?struct { width: u31, height: u31 } = null, fullscreen: ?bool = null, maximized: ?bool = null, tags: ?u32 = null, pending_output: ?PendingOutput = null, floating: ?bool = null, pub const PendingOutput = union(enum) { output: *Output, clear_output, }; }; const PendingRender = struct { position: ?struct { x: i32, y: i32 } = null, focused: ?bool = null, show: ?bool = null, // This could probably be a tagged union and just take *where* to place it (for above/below) place_top: bool = false, }; const DimensionsHint = struct { min_width: u31 = 0, min_height: u31 = 0, max_width: u31 = 0, max_height: u31 = 0, fn clampWidth(hint: DimensionsHint, width: u31) u31 { return math.clamp( width, if (hint.min_width != 0) hint.min_width else 1, if (hint.max_width != 0) hint.max_width else math.maxInt(u31), ); } fn clampHeight(hint: DimensionsHint, height: u31) u31 { return math.clamp( height, if (hint.min_height != 0) hint.min_height else 1, if (hint.max_height != 0) hint.max_height else math.maxInt(u31), ); } }; pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Output) !*Window { var window = try utils.gpa.create(Window); errdefer utils.gpa.destroy(window); window.* = .{ .context = context, .river_window_v1 = river_window_v1, .river_node_v1 = river_window_v1.getNode() catch @panic("Failed to get node"), .output = output, .tags = if (output) |o| o.tags else 0x0001, .link = undefined, // Handled by the wl.list // Ensure borders are applied on the first render cycle, even for windows that // are never explicitly told they are unfocused (e.g. on WM restart). .pending_render = .{ .focused = false }, }; window.river_window_v1.setListener(*Window, windowListener, window); return window; } pub fn destroy(window: *Window) void { if (window.app_id) |app_id| utils.gpa.free(app_id); if (window.title) |title| utils.gpa.free(title); window.river_node_v1.destroy(); window.river_window_v1.destroy(); utils.gpa.destroy(window); } fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, window: *Window) void { assert(window.river_window_v1 == river_window_v1); switch (event) { .closed => { // Clear any pointer operations referencing this window var seat_it = window.context.wm.seats.iterator(.forward); while (seat_it.next()) |seat| { switch (seat.pointer_op) { .move => |op| if (op.window == window) { seat.river_seat_v1.opEnd(); seat.pointer_op = .none; }, .resize => |op| if (op.window == window) { seat.river_seat_v1.opEnd(); seat.pointer_op = .none; }, .none => {}, } if (seat.pending_manage.pointer_move_request == window) seat.pending_manage.pointer_move_request = null; if (seat.pending_manage.pointer_resize_request) |req| { if (req.window == window) seat.pending_manage.pointer_resize_request = null; } } window.nextFocus(); window.link.remove(); window.destroy(); }, .dimensions => |ev| { window.pending_manage.dimensions = .{ .width = @intCast(ev.width), .height = @intCast(ev.height) }; if (window.needs_position) { @branchHint(.unlikely); if (window.output) |output| { window.needs_position = false; window.pending_render.position = .{ .x = (output.geometry.width / 2) - (window.pending_manage.dimensions.?.width / 2), .y = (output.geometry.height / 2) - (window.pending_manage.dimensions.?.height / 2), }; } } }, .dimensions_hint => |ev| { window.dimensions_hint = .{ .min_width = @intCast(ev.min_width), .min_height = @intCast(ev.min_height), .max_width = @intCast(ev.max_width), .max_height = @intCast(ev.max_height), }; }, .app_id => |ev| { if (window.app_id) |app_id| utils.gpa.free(app_id); window.app_id = if (ev.app_id) |aid| utils.gpa.dupe(u8, std.mem.span(aid)) catch @panic("Out of memory") else null; }, .title => |ev| { if (window.title) |title| utils.gpa.free(title); window.title = if (ev.title) |t| utils.gpa.dupe(u8, std.mem.span(t)) catch @panic("Out of memory") else null; // Need to update the bar if this window is focused if (window.context.wm.seats.first()) |seat| { if (seat.focused_window) |focused_window| { if (focused_window == window) { if (window.output) |output| { if (output.bar) |*bar| { bar.pending_render.draw = true; } } } } } }, .parent => |ev| { // Nothing to do if ev.parent is null const parent = ev.parent orelse return; window.parent = parent; // Make window float on top of its parent window.pending_manage.floating = true; const parent_window: *Window = @ptrCast(@alignCast(parent.getUserData() orelse return)); window.pending_render.position = .{ .x = parent_window.rect.x + @divTrunc(parent_window.rect.width, 2), .y = parent_window.rect.y + @divTrunc(parent_window.rect.height, 2), }; }, .fullscreen_requested => |ev| { window.pending_manage.fullscreen = true; // This event allows the window to provide an output preference, // so we should move the window if it wants if (ev.output) |river_output_v1| { if (river_output_v1.getUserData()) |user_data| { const output: *Output = @ptrCast(@alignCast(user_data)); // We have to remove window from current output's windows list first window.link.remove(); output.windows.append(window); window.pending_manage.pending_output = .{ .output = output }; } } }, .exit_fullscreen_requested => window.pending_manage.fullscreen = false, else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); }, } } /// Apply one-time initialization for newly created windows. /// Called before calculatePrimaryStackLayout() so that tag and float /// rules are reflected in the first frame's layout. pub fn initialize(window: *Window) void { if (window.initialized) { // We only need to initialize once per window, // but the method is called on every layout calculation. @branchHint(.likely); return; } window.initialized = true; const river_window_v1 = window.river_window_v1; // TODO: We might want to think about paying attention to the decoration_hint event // If we do, this would need to move, I think? river_window_v1.useSsd(); river_window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true }); river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); const res = window.applyRules(); if (res.tags) |tags| { window.tags = tags; } if (res.float) |should_float| { window.pending_manage.floating = should_float; } } pub fn manage(window: *Window) void { // Updating state since the last manage event defer window.pending_manage = .{}; const pending_manage = window.pending_manage; const river_window_v1 = window.river_window_v1; // Floating status var became_floating = false; if (pending_manage.floating) |floating| blk: { // This needs to be before proposing the new dimensions since we want to save the current ones! // Skip the rest of the block if floating matches what is already set if (floating == window.floating) break :blk; window.floating = floating; if (floating) { became_floating = true; // Let the window know it isn't tiled river_window_v1.setTiled(.{}); if (window.floating_rect.width == 0) { // This window has never floated before, let's give it floating dimensions. // Use 75% of the output's usable size, clamped to the window's dimension hints. if (window.output) |output| { if (window.rect.width == 0) { // The window didn't even *exist* before, i.e. we only got here via a floating window rule. // Don't propose dimensions, let it start how it wants. if (window.pending_manage.dimensions) |dimensions| { // TODO: Is it even possible to make it in here? I need to ask ifreund, probably // We want to center the output; this works even if the proposed dimensions // are 0 since the dimensions are the numerator, the window just wouldn't // be centered. window.pending_render.position = .{ .x = (output.geometry.width / 2) - (dimensions.width / 2), .y = (output.geometry.height / 2) - (dimensions.height / 2), }; } else { window.needs_position = true; } break :blk; } window.floating_rect.width = window.dimensions_hint.clampWidth(@divFloor(output.usable_geometry.width * 3, 4)); window.floating_rect.height = window.dimensions_hint.clampHeight(@divFloor(output.usable_geometry.height * 3, 4)); window.floating_rect.x = output.usable_geometry.x + @divFloor(output.usable_geometry.width, 2) - @divFloor(window.floating_rect.width, 2); window.floating_rect.y = output.usable_geometry.y + @divFloor(output.usable_geometry.height, 2) - @divFloor(window.floating_rect.height, 2); } else { window.floating_rect.width = window.rect.width; window.floating_rect.height = window.rect.height; } } river_window_v1.proposeDimensions(window.floating_rect.width, window.floating_rect.height); window.pending_render.position = .{ .x = window.floating_rect.x, .y = window.floating_rect.y, }; } else { river_window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); // Save floating dimensions in case the window gets floated again window.floating_rect.width = window.rect.width; window.floating_rect.height = window.rect.height; window.floating_rect.x = window.rect.x; window.floating_rect.y = window.rect.y; } } // Layout (pre-computed by WindowManager if (pending_manage.dimensions) |dimensions| { window.rect.width = dimensions.width; window.rect.height = dimensions.height; if (!became_floating) { // We want to skip this if the floating block above already proposed dimensions window.river_window_v1.proposeDimensions(dimensions.width, dimensions.height); } } // Fullscreen and maximize operations if (pending_manage.fullscreen) |fullscreen| blk: { window.fullscreen = fullscreen; if (fullscreen) { // If the window isn't on an output, just skip fullscreening const output = window.output orelse break :blk; window.river_window_v1.fullscreen(output.river_output_v1); window.river_window_v1.informFullscreen(); // We need to do this so the window appears on top of the bar window.pending_render.place_top = true; } else { window.river_window_v1.exitFullscreen(); window.river_window_v1.informNotFullscreen(); } } if (pending_manage.maximized) |maximized| { window.maximized = maximized; if (maximized) { window.river_window_v1.informMaximized(); } else { window.river_window_v1.informUnmaximized(); } } // New tags if (pending_manage.tags) |tags| { window.tags = tags; if (window.output) |o| { if (o.tags & window.tags == 0) { // This window isn't visible anymore, we need to move focus off it window.nextFocus(); } } } // New output if (pending_manage.pending_output) |pending_output| { switch (pending_output) { .output => |output| { window.output = output; }, .clear_output => window.output = null, } } } pub fn render(window: *Window) void { defer window.pending_render = .{}; if (window.pending_render.position) |position| { window.rect.x = position.x; window.rect.y = position.y; window.river_node_v1.setPosition(window.rect.x, window.rect.y); } // Set borders if (!window.fullscreen) { if (window.pending_render.focused) |focused| { if (focused) { window.applyBorders(window.context.config.border_color_focused); } else { window.applyBorders(window.context.config.border_color_unfocused); } } } // Show or hide the Window if (window.pending_render.show) |show| { if (show) { window.river_window_v1.show(); } else { window.river_window_v1.hide(); } } if (window.pending_render.place_top) { window.river_node_v1.placeTop(); } } fn applyBorders(window: *Window, color: utils.RiverColor) void { const border_width = window.context.config.border_width; const all_sides = river.WindowV1.Edges{ .top = true, .bottom = true, .left = true, .right = true }; window.river_window_v1.setBorders(all_sides, border_width, color.red, color.green, color.blue, color.alpha); } // Iterate over all window rules and apply any that match. // Later rules in the list overwrite earlier ones. fn applyRules(window: *Window) struct { float: ?bool = null, tags: ?u32 = null, } { var float: ?bool = null; var tags: ?u32 = null; for (window.context.config.window_rules.items) |rule| { const app_id_matches = if (rule.app_id_glob) |glob| if (window.app_id) |app_id| globber.match(app_id, glob) else false else true; const title_matches = if (rule.title_glob) |glob| if (window.title) |title| globber.match(title, glob) else false else true; if (app_id_matches and title_matches) { switch (rule.action) { .float => |should_float| float = should_float, .tags => |tagmask| tags = tagmask, } } } return .{ .float = float, .tags = tags, }; } /// Check if this window is focused on an output, if it is, then we move the /// output's focus to the next window in its list. fn nextFocus(window: *Window) void { if (window.output) |output| { // Get a new window for the wm to focus var it = window.context.wm.seats.iterator(.forward); while (it.next()) |seat| { if (seat.focused_window == window) { // Find another window to focus and warp pointer there if (output.nextWindow(window)) |next_focus| { if (next_focus != window) { seat.pending_manage.window = .{ .window = next_focus }; seat.pending_manage.should_warp_pointer = true; } else { // This was the only visible window, so nothing to change focus to seat.pending_manage.window = .clear_focus; } } else { // TODO: I believe this should be unreachable, needs further testing. // Regardless, it probably doesn't make sense to clear focus if this // window wasn't focused. seat.pending_manage.window = .clear_focus; } } } } } const std = @import("std"); const assert = std.debug.assert; const math = std.math; const DoublyLinkedList = std.DoublyLinkedList; const wayland = @import("wayland"); const wl = wayland.client.wl; const river = wayland.client.river; const globber = @import("globber.zig"); const utils = @import("utils.zig"); const Context = @import("Context.zig"); const Output = @import("Output.zig"); const Seat = @import("Seat.zig"); const WindowRule = @import("Config.zig").WindowRule; const log = std.log.scoped(.Window);