Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** <a href="https://patreon.com/domferr"><img src="https://img.shields.io/badge/Patreons-F96854?logo=patreon&logoColor=white)" height="14px"/><a/> and **everyone** who donated on <a href="https://ko-fi.com/domferr"><img src="https://img.shields.io/badge/_Ko--fi-794bc4?logo=ko-fi&logoColor=white" height="14px"/><a/>! :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**!
Expand Down Expand Up @@ -108,6 +108,34 @@ Move window through the tiles using keyboard shortcuts (<kbd>SUPER</kbd>+<kbd>

<p align="right"><b>Go to Usage</b> <a href="#usage">⬆️</a></p>

### 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.

<p align="right"><b>Go to Usage</b> <a href="#usage">⬆️</a></p>

### Edge Tiling ###

You can tile a window by moving it to the edge.
Expand Down
44 changes: 30 additions & 14 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { Gio, GLib, Meta } from './gi/ext';
import { logger } from './utils/logger';
import {
filterUnfocusableWindows,
getGlobalWindowList,
getMonitors,
getWindows,
squaredEuclideanDistance,
Expand Down Expand Up @@ -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) {
Expand Down
96 changes: 96 additions & 0 deletions src/utils/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}