From 420228ca097a4904b8195d86b3f80ae699519f30 Mon Sep 17 00:00:00 2001 From: Jesse Dhillon Date: Thu, 8 Jan 2026 12:53:19 -0800 Subject: [PATCH] feat: next/prev switching follows spatial layout, instead of MRU window list --- README.md | 30 ++++++++++++++- src/extension.ts | 44 +++++++++++++++------- src/utils/ui.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2b1aea8f..76669b0f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Have issues, you want to suggest a new feature or contribute? Please open a new |:---|:---|:---|:---| | [⬇️](#layout-editor) **Layout editor** | [⬇️](#smart-resize) **Smart resize** | [⬇️](#tile-with-keyboard) **Tile with Keyboard** | [⬇️](#edge-tiling) **Edge Tiling** | | [⬇️](#tiling-buttons) **Tiling Buttons** | [⬇️](#per-workspace-layout) **Per-workspace layout** | [⬇️](#auto-tiling) **Auto-tiling** | [⬇️](#tiling-context-menu) **Tiling context menu** | -| [⬇️](#smart-border-radius) **Smart border radius** | [⬇️](#windows-suggestions) **Windows Suggestions** +| [⬇️](#smart-border-radius) **Smart border radius** | [⬇️](#windows-suggestions) **Windows Suggestions** | [⬇️](#spatial-focus-switching) **Spatial Focus Switching** | ## 🎉🎉 Tiling Shell's AWESOME Supporters! Thank you to the :star2: **amazing** and **everyone** who donated on ! :medal_sports:Sean, Markus Huggler, Kostja Palović, Mike Empey, Miguel and Jesse Dhillon on Patreon:medal_sports: and Zorin OS, Nick, thy-fi, iatanas0v, Chris, wbezs, DaneshManoharan, Tamas, Ivan Banha and many more on Ko-fi! You are on a mission to **make Linux window management better for everyone**! @@ -108,6 +108,34 @@ Move window through the tiles using keyboard shortcuts (SUPER+

Go to Usage ⬆️

+### Spatial Focus Switching ### + +Navigate between windows using "Focus Next Window" and "Focus Previous Window" keybindings. Unlike the default GNOME behavior which uses Most Recently Used (MRU) order, this feature uses **spatial ordering** based on window positions. + +**How it works:** + +Windows are ordered left-to-right, then top-to-bottom. When two windows have similar horizontal positions (within 50 pixels), they are ordered top-to-bottom instead. This creates a natural reading order that matches the visual layout on screen. + +``` +┌───────┐ ┌───────┐ ┌───────┐ +│ 1 │ │ 3 │ │ 4 │ +└───────┘ │ │ └───────┘ +┌───────┐ │ │ ┌───────┐ +│ 2 │ └───────┘ │ 5 │ +└───────┘ └───────┘ + +Navigation order: 1 → 2 → 3 → 4 → 5 +(Windows 1 and 2 have similar x-coordinates, so 1 comes first as it is closer to the top) +``` + +**Multi-monitor support:** + +Monitors are also sorted spatially (left-to-right, top-to-bottom). When you reach the last window on a monitor and press "Next", focus moves to the first window on the next monitor. The "Wraparound focus" setting controls whether navigation wraps from the last window on the last monitor back to the first window on the first monitor. + +> This feature provides an alternative to directional focus switching (up/down/left/right) for users who prefer simpler next/previous navigation while still maintaining spatially predictable ordering. + +

Go to Usage ⬆️

+ ### Edge Tiling ### You can tile a window by moving it to the edge. diff --git a/src/extension.ts b/src/extension.ts index d96c98b6..5e3d1ee2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,6 +25,7 @@ import { Gio, GLib, Meta } from './gi/ext'; import { logger } from './utils/logger'; import { filterUnfocusableWindows, + getGlobalWindowList, getMonitors, getWindows, squaredEuclideanDistance, @@ -675,34 +676,49 @@ export default class TilingShellExtension extends Extension { ) return; - const windowList = filterUnfocusableWindows( - focus_window.get_workspace().list_windows(), - ); + // Get spatially ordered list of all windows across all monitors + const workspace = focus_window.get_workspace(); + const windowList = getGlobalWindowList(workspace); + + if (windowList.length === 0) return; + const focusParent = focus_window.get_transient_for() || focus_window; const focusedIdx = windowList.findIndex((win) => { // in case we are iterating over a modal dialog for our focused window return win === focusParent; }); - let nextIndex = -1; + if (focusedIdx === -1) return; + + let targetIndex = -1; switch (direction) { case FocusSwitchDirection.PREV: - if (focusedIdx === 0 && Settings.WRAPAROUND_FOCUS) { - windowList[windowList.length - 1].activate( - global.get_current_time(), - ); + if (focusedIdx === 0) { + // At first window globally + if (Settings.WRAPAROUND_FOCUS) { + targetIndex = windowList.length - 1; + } + // else do nothing (targetIndex stays -1) } else { - windowList[focusedIdx - 1].activate( - global.get_current_time(), - ); + targetIndex = focusedIdx - 1; } break; case FocusSwitchDirection.NEXT: - nextIndex = (focusedIdx + 1) % windowList.length; - if (nextIndex > 0 || Settings.WRAPAROUND_FOCUS) - windowList[nextIndex].activate(global.get_current_time()); + if (focusedIdx === windowList.length - 1) { + // At last window globally + if (Settings.WRAPAROUND_FOCUS) { + targetIndex = 0; + } + // else do nothing (targetIndex stays -1) + } else { + targetIndex = focusedIdx + 1; + } break; } + + if (targetIndex >= 0 && targetIndex < windowList.length) { + windowList[targetIndex].activate(global.get_current_time()); + } } private _onKeyboardUntileWindow(kb: KeyBindings, display: Meta.Display) { diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 76cea4ed..524c274c 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -221,3 +221,99 @@ export function squaredEuclideanDistance( (pointA.y - pointB.y) * (pointA.y - pointB.y) ); } + +/** + * Get the center point of a window's frame rect + */ +export function getWindowCenter(window: Meta.Window): { x: number; y: number } { + const rect = window.get_frame_rect(); + return { + x: rect.x + rect.width / 2, + y: rect.y + rect.height / 2, + }; +} + +/** + * Get the center point of a monitor + */ +export function getMonitorCenter(monitor: Monitor): { x: number; y: number } { + return { + x: monitor.x + monitor.width / 2, + y: monitor.y + monitor.height / 2, + }; +} + +/** + * Spatial comparison for sorting: left-to-right, then top-to-bottom within epsilon. + * Returns negative if a comes before b, positive if b comes before a, 0 if equal. + */ +export function spatialCompare( + centerA: { x: number; y: number }, + centerB: { x: number; y: number }, + epsilon: number = 50, +): number { + const dx = centerA.x - centerB.x; + // If x coordinates are within epsilon, sort by y (top to bottom) + if (Math.abs(dx) <= epsilon) { + return centerA.y - centerB.y; + } + // Otherwise sort by x (left to right) + return dx; +} + +/** + * Sort windows spatially: left-to-right, then top-to-bottom within epsilon + */ +export function sortWindowsSpatially( + windows: Meta.Window[], + epsilon: number = 50, +): Meta.Window[] { + return [...windows].sort((a, b) => { + const centerA = getWindowCenter(a); + const centerB = getWindowCenter(b); + return spatialCompare(centerA, centerB, epsilon); + }); +} + +/** + * Sort monitors spatially: left-to-right, then top-to-bottom within epsilon + */ +export function sortMonitorsSpatially( + monitors: Monitor[], + epsilon: number = 50, +): Monitor[] { + return [...monitors].sort((a, b) => { + const centerA = getMonitorCenter(a); + const centerB = getMonitorCenter(b); + return spatialCompare(centerA, centerB, epsilon); + }); +} + +/** + * Get all windows across all monitors, ordered spatially. + * Monitors are sorted spatially, then windows within each monitor are sorted spatially. + * Returns a flat list representing the global window order. + */ +export function getGlobalWindowList( + workspace: Meta.Workspace, + epsilon: number = 50, +): Meta.Window[] { + const monitors = sortMonitorsSpatially(getMonitors(), epsilon); + const allWindows: Meta.Window[] = []; + + for (const monitor of monitors) { + // Get windows on this monitor for the given workspace + const monitorWindows = filterUnfocusableWindows( + workspace.list_windows().filter( + (win) => + win.get_window_type() === Meta.WindowType.NORMAL && + win.get_monitor() === monitor.index, + ), + ); + // Sort windows on this monitor spatially + const sortedWindows = sortWindowsSpatially(monitorWindows, epsilon); + allWindows.push(...sortedWindows); + } + + return allWindows; +}