Something went wrong. Try again.
window manager for the river wayland compositor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110// SPDX-FileCopyrightText: © 2026 mtmn//// SPDX-License-Identifier: BSD-3-Clause
//! Focus rules with no Wayland objects, so we can unit test them.//! WindowManager.manageStart and Seat pass in the state they track.
const std = @import("std");
/// Focus-follows-cursor modes. Normal focuses a window when the pointer/// enters it. Always focuses whatever window is under the pointer, even if/// the pointer has not moved. In every mode, a new window keeps focus until/// the pointer moves.pub const FollowsCursor = enum { disabled, normal, always,};
/// Return true when a new window should replace the current best choice/// for focus. The newest visible window wins. Stack order does not matter,/// so attach top and attach bottom behave the same.pub fn newer(visible: bool, serial: u64, best: ?u64) bool { if (!visible) return false; const current = best orelse return true; return serial > current;}
/// Return the index of the newest visible window. Uses the same loop as/// WindowManager.newestCreated.fn newest(serials: []const u64, visible: []const bool) ?usize { std.debug.assert(serials.len == visible.len); var best: ?usize = null; for (serials, visible, 0..) |serial, is_visible, index| { if (newer(is_visible, serial, if (best) |b| serials[b] else null)) best = index; } return best;}
/// Return true when the window under the pointer should take focus.////// held: a new window is keeping focus. It opened in this manage sequence,/// or the pointer has not moved since it opened./// fresh: the pointer entered this window since it last took focus.////// The caller marks the hover as used whatever this returns. If a window/// slides under a still pointer while a new window keeps focus, that hover/// cannot take focus later.pub fn hoverTakesFocus(mode: FollowsCursor, held: bool, has_hover: bool, visible: bool, fresh: bool) bool { if (held) return false; if (!has_hover or !visible) return false; return switch (mode) { .disabled => false, .always => true, .normal => fresh, };}
/// Return true when the pointer has really moved, which ends the hold on a/// new window. The first report has nothing to compare with, so it is not/// movement. When laguna warps the pointer, it stores the target as the/// last position. The report that follows the warp then matches it.pub fn movementReleasesHold(previous: ?[2]i32, current: [2]i32) bool { const last = previous orelse return false; return last[0] != current[0] or last[1] != current[1];}
const testing = std.testing;
test "newest visible window wins regardless of stack order" { // With attach bottom, the newest window is last in the stack. try testing.expectEqual(@as(?usize, 2), newest(&.{ 3, 4, 5 }, &.{ true, true, true })); // With attach top, the newest window is first. try testing.expectEqual(@as(?usize, 0), newest(&.{ 5, 4, 3 }, &.{ true, true, true })); // A new window on a hidden tag does not take focus. try testing.expectEqual(@as(?usize, 1), newest(&.{ 3, 4, 5 }, &.{ false, true, false })); try testing.expectEqual(@as(?usize, null), newest(&.{ 3, 4 }, &.{ false, false })); try testing.expectEqual(@as(?usize, null), newest(&.{}, &.{}));}
test "held focus beats any hover" { for ([_]FollowsCursor{ .disabled, .normal, .always }) |mode| { // Covers the manage sequence the window opens in, and a later one // where another window slides under a still pointer. try testing.expect(!hoverTakesFocus(mode, true, true, true, true)); try testing.expect(!hoverTakesFocus(mode, true, true, true, false)); }}
test "hover follow policy once released" { try testing.expect(!hoverTakesFocus(.disabled, false, true, true, true)); try testing.expect(hoverTakesFocus(.normal, false, true, true, true)); try testing.expect(!hoverTakesFocus(.normal, false, true, true, false)); try testing.expect(hoverTakesFocus(.always, false, true, true, false)); try testing.expect(hoverTakesFocus(.always, false, true, true, true)); try testing.expect(!hoverTakesFocus(.always, false, false, true, true)); try testing.expect(!hoverTakesFocus(.always, false, true, false, true));}
test "only real pointer movement releases the hold" { try testing.expect(movementReleasesHold(.{ 10, 10 }, .{ 11, 10 })); try testing.expect(movementReleasesHold(.{ 10, 10 }, .{ 10, 9 })); try testing.expect(!movementReleasesHold(.{ 10, 10 }, .{ 10, 10 })); // The first report has nothing to compare with. try testing.expect(!movementReleasesHold(null, .{ 10, 10 })); // The report after a warp matches the stored warp target. const warp_target: [2]i32 = .{ 500, 300 }; try testing.expect(!movementReleasesHold(warp_target, .{ 500, 300 }));}